Skip to content

Commit

Permalink
web/admin: rework initial wizard pages and add grid layout (#9668)
Browse files Browse the repository at this point in the history
* remove @goauthentik/authentik as TS path

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial implementation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* oh yeah

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format earlier changes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* support plain alert

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial attempt at dedupe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make it a base class

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate all wizards

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* create type create mixin to dedupe more, add icon to source create

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add ldap icon

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Optimised images with calibre/image-actions

* match inverting

we should probably replace all icons with coloured ones so we don't need to invert them...I guess

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make everything more explicit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add icons to provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add remaining provider icons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework to not use inheritance

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix unrelated typo

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make app wizard use grid layout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* keep wizard height consistent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
  • Loading branch information
BeryJu and authentik-automation[bot] authored May 22, 2024
1 parent 0ed4bba commit 6c4c535
Show file tree
Hide file tree
Showing 58 changed files with 726 additions and 791 deletions.
79 changes: 79 additions & 0 deletions authentik/core/api/object_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""API Utilities"""

from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import (
BooleanField,
CharField,
)
from rest_framework.request import Request
from rest_framework.response import Response

from authentik.core.api.utils import PassiveSerializer
from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import all_subclasses


class TypeCreateSerializer(PassiveSerializer):
"""Types of an object that can be created"""

name = CharField(required=True)
description = CharField(required=True)
component = CharField(required=True)
model_name = CharField(required=True)

icon_url = CharField(required=False)
requires_enterprise = BooleanField(default=False)


class CreatableType:
"""Class to inherit from to mark a model as creatable, even if the model itself is marked
as abstract"""


class NonCreatableType:
"""Class to inherit from to mark a model as non-creatable even if it is not abstract"""


class TypesMixin:
"""Mixin which adds an API endpoint to list all possible types that can be created"""

@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request, additional: list[dict] | None = None) -> Response:
"""Get all creatable types"""
data = []
for subclass in all_subclasses(self.queryset.model):
instance = None
if subclass._meta.abstract:
if not issubclass(subclass, CreatableType):
continue
# Circumvent the django protection for not being able to instantiate
# abstract models. We need a model instance to access .component
# and further down .icon_url
instance = subclass.__new__(subclass)
# Django re-sets abstract = False so we need to override that
instance.Meta.abstract = True
else:
if issubclass(subclass, NonCreatableType):
continue
instance = subclass()
try:
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": instance.component,
"model_name": subclass._meta.model_name,
"icon_url": getattr(instance, "icon_url", None),
"requires_enterprise": isinstance(
subclass._meta.app_config, EnterpriseConfig
),
}
)
except NotImplementedError:
continue
if additional:
data.extend(additional)
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)
25 changes: 6 additions & 19 deletions authentik/core/api/propertymappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
from rest_framework.viewsets import GenericViewSet

from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
)
from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.models import PropertyMapping
from authentik.events.utils import sanitize_item
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required

Expand Down Expand Up @@ -64,6 +67,7 @@ class Meta:


class PropertyMappingViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
Expand All @@ -83,23 +87,6 @@ class PropertyMappingViewSet(
def get_queryset(self): # pragma: no cover
return PropertyMapping.objects.select_subclasses()

@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable property-mapping types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: PropertyMapping
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)

@permission_required("authentik_core.view_propertymapping")
@extend_schema(
request=PolicyTestSerializer(),
Expand Down
38 changes: 3 additions & 35 deletions authentik/core/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet

from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Provider
from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import all_subclasses


class ProviderSerializer(ModelSerializer, MetaNameSerializer):
Expand Down Expand Up @@ -86,6 +81,7 @@ def filter_backchannel(self, queryset: QuerySet, name, value):


class ProviderViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
Expand All @@ -104,31 +100,3 @@ class ProviderViewSet(

def get_queryset(self): # pragma: no cover
return Provider.objects.select_subclasses()

@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable provider types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Provider
if subclass._meta.abstract:
continue
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
}
)
data.append(
{
"name": _("SAML Provider from Metadata"),
"description": _("Create a SAML Provider by importing its Metadata."),
"component": "ak-provider-saml-import-form",
"model_name": "",
}
)
return Response(TypeCreateSerializer(data, many=True).data)
29 changes: 3 additions & 26 deletions authentik/core/api/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import (
Expand All @@ -27,7 +28,6 @@
set_file,
set_file_url,
)
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
from authentik.rbac.decorators import permission_required

Expand Down Expand Up @@ -74,6 +74,7 @@ class Meta:


class SourceViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
Expand Down Expand Up @@ -132,30 +133,6 @@ def set_icon_url(self, request: Request, slug: str):
source: Source = self.get_object()
return set_file_url(request, source, "icon")

@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable source types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Source
component = ""
if len(subclass.__subclasses__()) > 0:
continue
if subclass._meta.abstract:
component = subclass.__bases__[0]().component
else:
component = subclass().component
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)

@extend_schema(responses={200: UserSettingSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:
Expand Down
22 changes: 10 additions & 12 deletions authentik/core/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
from drf_spectacular.plumbing import build_basic_type
from drf_spectacular.types import OpenApiTypes
from rest_framework.fields import BooleanField, CharField, IntegerField, JSONField
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
from rest_framework.fields import (
CharField,
IntegerField,
JSONField,
SerializerMethodField,
)
from rest_framework.serializers import (
Serializer,
ValidationError,
)


def is_dict(value: Any):
Expand Down Expand Up @@ -68,16 +76,6 @@ def get_meta_model_name(self, obj: Model) -> str:
return f"{obj._meta.app_label}.{obj._meta.model_name}"


class TypeCreateSerializer(PassiveSerializer):
"""Types of an object that can be created"""

name = CharField(required=True)
description = CharField(required=True)
component = CharField(required=True)
model_name = CharField(required=True)
requires_enterprise = BooleanField(default=False)


class CacheSerializer(PassiveSerializer):
"""Generic cache stats for an object"""

Expand Down
4 changes: 4 additions & 0 deletions authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ def launch_url(self) -> str | None:
Can return None for providers that are not URL-based"""
return None

@property
def icon_url(self) -> str | None:
return None

@property
def component(self) -> str:
"""Return component used to edit this object"""
Expand Down
13 changes: 6 additions & 7 deletions authentik/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from guardian.shortcuts import get_anonymous_user

from authentik.core.models import Provider, Source, Token
from authentik.flows.models import Stage
from authentik.lib.utils.reflection import all_subclasses


Expand All @@ -31,27 +30,27 @@ def test_token_expire_no_expire(self):
self.assertFalse(token.is_expired)


def source_tester_factory(test_model: type[Stage]) -> Callable:
def source_tester_factory(test_model: type[Source]) -> Callable:
"""Test source"""

factory = RequestFactory()
request = factory.get("/")

def tester(self: TestModels):
model_class = None
if test_model._meta.abstract: # pragma: no cover
model_class = test_model.__bases__[0]()
if test_model._meta.abstract:
model_class = [x for x in test_model.__bases__ if issubclass(x, Source)][0]()
else:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button(request)
_ = model_class.ui_user_settings()
model_class.ui_login_button(request)
model_class.ui_user_settings()

return tester


def provider_tester_factory(test_model: type[Stage]) -> Callable:
def provider_tester_factory(test_model: type[Provider]) -> Callable:
"""Test provider"""

def tester(self: TestModels):
Expand Down
5 changes: 5 additions & 0 deletions authentik/enterprise/providers/google_workspace/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.db import models
from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from google.oauth2.service_account import Credentials
from rest_framework.serializers import Serializer
Expand Down Expand Up @@ -98,6 +99,10 @@ def google_credentials(self):
).with_subject(self.delegated_subject),
}

@property
def icon_url(self) -> str | None:
return static("authentik/sources/google.svg")

@property
def component(self) -> str:
return "ak-provider-google-workspace-form"
Expand Down
5 changes: 5 additions & 0 deletions authentik/enterprise/providers/microsoft_entra/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from azure.identity.aio import ClientSecretCredential
from django.db import models
from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer

Expand Down Expand Up @@ -87,6 +88,10 @@ def microsoft_credentials(self):
)
}

@property
def icon_url(self) -> str | None:
return static("authentik/sources/azuread.svg")

@property
def component(self) -> str:
return "ak-provider-microsoft-entra-form"
Expand Down
5 changes: 5 additions & 0 deletions authentik/enterprise/providers/rac/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.db import models
from django.db.models import QuerySet
from django.http import HttpRequest
from django.templatetags.static import static
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
Expand Down Expand Up @@ -63,6 +64,10 @@ def launch_url(self) -> str | None:
Can return None for providers that are not URL-based"""
return "goauthentik.io://providers/rac/launch"

@property
def icon_url(self) -> str | None:
return static("authentik/sources/rac.svg")

@property
def component(self) -> str:
return "ak-provider-rac-form"
Expand Down
Loading

0 comments on commit 6c4c535

Please sign in to comment.