diff --git a/authentik/core/templates/partials/form_horizontal.html b/authentik/core/templates/partials/form_horizontal.html
deleted file mode 100644
index 14baa5286..000000000
--- a/authentik/core/templates/partials/form_horizontal.html
+++ /dev/null
@@ -1,115 +0,0 @@
-{% load authentik_utils %}
-{% load i18n %}
-
-{% csrf_token %}
-{% for field in form %}
-{% if field.field.widget|fieldtype == 'HiddenInput' %}
-{{ field }}
-{% else %}
-
-{% endif %}
-{% endfor %}
diff --git a/authentik/core/tests/test_property_mapping_api.py b/authentik/core/tests/test_property_mapping_api.py
index 4912e17ed..74da28f5e 100644
--- a/authentik/core/tests/test_property_mapping_api.py
+++ b/authentik/core/tests/test_property_mapping_api.py
@@ -2,8 +2,10 @@
from json import dumps
from django.urls import reverse
+from rest_framework.serializers import ValidationError
from rest_framework.test import APITestCase
+from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.models import PropertyMapping, User
@@ -19,7 +21,7 @@ class TestPropertyMappingAPI(APITestCase):
self.client.force_login(self.user)
def test_test_call(self):
- """Test Policy's test endpoint"""
+ """Test PropertMappings's test endpoint"""
response = self.client.post(
reverse(
"authentik_api:propertymapping-test", kwargs={"pk": self.mapping.pk}
@@ -32,3 +34,19 @@ class TestPropertyMappingAPI(APITestCase):
response.content.decode(),
{"result": dumps({"foo": "bar"}), "successful": True},
)
+
+ def test_validate(self):
+ """Test PropertyMappings's validation"""
+ # Because the root property-mapping has no write operation, we just instantiate
+ # a serializer and test inline
+ expr = "return True"
+ self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
+ with self.assertRaises(ValidationError):
+ print(PropertyMappingSerializer().validate_expression("/"))
+
+ def test_types(self):
+ """Test PropertyMappigns's types endpoint"""
+ response = self.client.get(
+ reverse("authentik_api:propertymapping-types"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/core/tests/test_providers_api.py b/authentik/core/tests/test_providers_api.py
new file mode 100644
index 000000000..dbfba2594
--- /dev/null
+++ b/authentik/core/tests/test_providers_api.py
@@ -0,0 +1,24 @@
+"""Test providers API"""
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from authentik.core.models import PropertyMapping, User
+
+
+class TestProvidersAPI(APITestCase):
+ """Test providers API"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.mapping = PropertyMapping.objects.create(
+ name="dummy", expression="""return {'foo': 'bar'}"""
+ )
+ self.user = User.objects.get(username="akadmin")
+ self.client.force_login(self.user)
+
+ def test_types(self):
+ """Test Providers's types endpoint"""
+ response = self.client.get(
+ reverse("authentik_api:provider-types"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py
index daa43e672..f600ec67c 100644
--- a/authentik/crypto/api.py
+++ b/authentik/crypto/api.py
@@ -1,4 +1,5 @@
"""Crypto API Views"""
+import django_filters
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate
@@ -95,11 +96,29 @@ class CertificateGenerationSerializer(PassiveSerializer):
validity_days = IntegerField(initial=365)
+class CertificateKeyPairFilter(django_filters.FilterSet):
+ """Filter for certificates"""
+
+ has_key = django_filters.BooleanFilter(
+ label="Only return certificate-key pairs with keys", method="filter_has_key"
+ )
+
+ # pylint: disable=unused-argument
+ def filter_has_key(self, queryset, name, value): # pragma: no cover
+ """Only return certificate-key pairs with keys"""
+ return queryset.exclude(key_data__exact="")
+
+ class Meta:
+ model = CertificateKeyPair
+ fields = ["name"]
+
+
class CertificateKeyPairViewSet(ModelViewSet):
"""CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.all()
serializer_class = CertificateKeyPairSerializer
+ filterset_class = CertificateKeyPairFilter
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
@swagger_auto_schema(
@@ -125,7 +144,7 @@ class CertificateKeyPairViewSet(ModelViewSet):
return Response(serializer.data)
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
- @action(detail=True)
+ @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument
def view_certificate(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs certificate and log access"""
@@ -140,7 +159,7 @@ class CertificateKeyPairViewSet(ModelViewSet):
)
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
- @action(detail=True)
+ @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument
def view_private_key(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs private key and log access"""
diff --git a/authentik/events/api/event.py b/authentik/events/api/event.py
index 0c59cf10b..b13e28c69 100644
--- a/authentik/events/api/event.py
+++ b/authentik/events/api/event.py
@@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ReadOnlyModelViewSet
+from authentik.core.api.utils import TypeCreateSerializer
from authentik.events.models import Event, EventAction
@@ -144,3 +145,18 @@ class EventViewSet(ReadOnlyModelViewSet):
.values("unique_users", "application", "counted_events")
.order_by("-counted_events")[:top_n]
)
+
+ @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
+ @action(detail=False, pagination_class=None, filter_backends=[])
+ def actions(self, request: Request) -> Response:
+ """Get all actions"""
+ data = []
+ for value, name in EventAction.choices:
+ data.append(
+ {
+ "name": name,
+ "description": "",
+ "component": value,
+ }
+ )
+ return Response(TypeCreateSerializer(data, many=True).data)
diff --git a/authentik/events/api/notification_transport.py b/authentik/events/api/notification_transport.py
index 7fdb5d260..7893d705e 100644
--- a/authentik/events/api/notification_transport.py
+++ b/authentik/events/api/notification_transport.py
@@ -63,7 +63,7 @@ class NotificationTransportViewSet(ModelViewSet):
responses={200: NotificationTransportTestSerializer(many=False)},
request_body=no_body,
)
- @action(detail=True, methods=["post"])
+ @action(detail=True, pagination_class=None, filter_backends=[], methods=["post"])
# pylint: disable=invalid-name, unused-argument
def test(self, request: Request, pk=None) -> Response:
"""Send example notification using selected transport. Requires
diff --git a/authentik/events/tests/test_api.py b/authentik/events/tests/test_api.py
index 7a2812e02..b7b4c15fc 100644
--- a/authentik/events/tests/test_api.py
+++ b/authentik/events/tests/test_api.py
@@ -10,11 +10,12 @@ from authentik.events.models import Event, EventAction
class TestEventsAPI(APITestCase):
"""Test Event API"""
- def test_top_n(self):
- """Test top_per_user"""
+ def setUp(self) -> None:
user = User.objects.get(username="akadmin")
self.client.force_login(user)
+ def test_top_n(self):
+ """Test top_per_user"""
event = Event.new(EventAction.AUTHORIZE_APPLICATION)
event.save() # We save to ensure nothing is un-saveable
response = self.client.get(
@@ -22,3 +23,10 @@ class TestEventsAPI(APITestCase):
data={"filter_action": EventAction.AUTHORIZE_APPLICATION},
)
self.assertEqual(response.status_code, 200)
+
+ def test_actions(self):
+ """Test actions"""
+ response = self.client.get(
+ reverse("authentik_api:event-actions"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py
index b2b6d84e4..b92273517 100644
--- a/authentik/flows/api/flows.py
+++ b/authentik/flows/api/flows.py
@@ -98,7 +98,7 @@ class FlowViewSet(ModelViewSet):
@permission_required(None, ["authentik_flows.view_flow_cache"])
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def cache_info(self, request: Request) -> Response:
"""Info about cached flows"""
return Response(data={"count": len(cache.keys("flow_*"))})
@@ -178,7 +178,7 @@ class FlowViewSet(ModelViewSet):
),
},
)
- @action(detail=True)
+ @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument
def export(self, request: Request, slug: str) -> Response:
"""Export flow to .akflow file"""
@@ -189,7 +189,7 @@ class FlowViewSet(ModelViewSet):
return response
@swagger_auto_schema(responses={200: FlowDiagramSerializer()})
- @action(detail=True, methods=["get"])
+ @action(detail=True, pagination_class=None, filter_backends=[], methods=["get"])
# pylint: disable=unused-argument
def diagram(self, request: Request, slug: str) -> Response:
"""Return diagram for flow with slug `slug`, in the format used by flowchart.js"""
@@ -270,7 +270,13 @@ class FlowViewSet(ModelViewSet):
],
responses={200: "Success"},
)
- @action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,))
+ @action(
+ detail=True,
+ pagination_class=None,
+ filter_backends=[],
+ methods=["POST"],
+ parser_classes=(MultiPartParser,),
+ )
# pylint: disable=unused-argument
def set_background(self, request: Request, slug: str):
"""Set Flow background"""
@@ -285,7 +291,7 @@ class FlowViewSet(ModelViewSet):
@swagger_auto_schema(
responses={200: LinkSerializer(many=False)},
)
- @action(detail=True)
+ @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py
index c80a426b5..4d89cb19b 100644
--- a/authentik/flows/api/stages.py
+++ b/authentik/flows/api/stages.py
@@ -1,7 +1,6 @@
"""Flow Stage API Views"""
from typing import Iterable
-from django.urls import reverse
from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins
from rest_framework.decorators import action
@@ -15,7 +14,6 @@ from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSerializer
from authentik.flows.models import Stage
-from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses
LOGGER = get_logger()
@@ -24,12 +22,15 @@ LOGGER = get_logger()
class StageSerializer(ModelSerializer, MetaNameSerializer):
"""Stage Serializer"""
- object_type = SerializerMethodField()
+ component = SerializerMethodField()
flow_set = FlowSerializer(many=True, required=False)
- def get_object_type(self, obj: Stage) -> str:
- """Get object type so that we know which API Endpoint to use to get the full object"""
- return obj._meta.object_name.lower().replace("stage", "")
+ def get_component(self, obj: Stage) -> str:
+ """Get object type so that we know how to edit the object"""
+ # pyright: reportGeneralTypeIssues=false
+ if obj.__class__ == Stage:
+ return ""
+ return obj.component
class Meta:
@@ -37,7 +38,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
fields = [
"pk",
"name",
- "object_type",
+ "component",
"verbose_name",
"verbose_name_plural",
"flow_set",
@@ -61,24 +62,24 @@ class StageViewSet(
return Stage.objects.select_subclasses()
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable stage types"""
data = []
for subclass in all_subclasses(self.queryset.model, False):
+ subclass: Stage
data.append(
{
- "name": verbose_name(subclass),
+ "name": subclass._meta.verbose_name,
"description": subclass.__doc__,
- "link": reverse("authentik_admin:stage-create")
- + f"?type={subclass.__name__}",
+ "component": subclass().component,
}
)
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)
@swagger_auto_schema(responses={200: UserSettingSerializer(many=True)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:
"""Get all stages the user can configure"""
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
diff --git a/authentik/flows/models.py b/authentik/flows/models.py
index 3db3db439..75c0bc930 100644
--- a/authentik/flows/models.py
+++ b/authentik/flows/models.py
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Optional, Type
from uuid import uuid4
from django.db import models
-from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
@@ -60,8 +59,8 @@ class Stage(SerializerModel):
raise NotImplementedError
@property
- def form(self) -> Type[ModelForm]:
- """Return Form class used to edit this object"""
+ def component(self) -> str:
+ """Return component used to edit this object"""
raise NotImplementedError
@property
diff --git a/authentik/flows/tests/test_api.py b/authentik/flows/tests/test_api.py
index 6af98198e..5eaeff42a 100644
--- a/authentik/flows/tests/test_api.py
+++ b/authentik/flows/tests/test_api.py
@@ -37,7 +37,7 @@ class TestFlowsAPI(APITestCase):
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""
obj = DummyStage()
- self.assertEqual(StageSerializer().get_object_type(obj), "dummy")
+ self.assertEqual(StageSerializer().get_component(obj), "ak-stage-dummy-form")
self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage")
def test_api_viewset(self):
@@ -90,3 +90,13 @@ class TestFlowsAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_SHORT_EXPECTED})
+
+ def test_types(self):
+ """Test Stage's types endpoint"""
+ user = User.objects.get(username="akadmin")
+ self.client.force_login(user)
+
+ response = self.client.get(
+ reverse("authentik_api:stage-types"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/flows/tests/test_models.py b/authentik/flows/tests/test_models.py
deleted file mode 100644
index 864518c39..000000000
--- a/authentik/flows/tests/test_models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""flow model tests"""
-from typing import Callable, Type
-
-from django.forms import ModelForm
-from django.test import TestCase
-
-from authentik.flows.models import Stage
-from authentik.flows.stage import StageView
-
-
-class TestStageProperties(TestCase):
- """Generic model properties tests"""
-
-
-def stage_tester_factory(model: Type[Stage]) -> Callable:
- """Test a form"""
-
- def tester(self: TestStageProperties):
- model_inst = model()
- self.assertTrue(issubclass(model_inst.form, ModelForm))
- self.assertTrue(issubclass(model_inst.type, StageView))
-
- return tester
-
-
-for stage_type in Stage.__subclasses__():
- setattr(
- TestStageProperties,
- f"test_stage_{stage_type.__name__}",
- stage_tester_factory(stage_type),
- )
diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py
index eee22f10e..f9d7d79fd 100644
--- a/authentik/flows/transfer/common.py
+++ b/authentik/flows/transfer/common.py
@@ -22,7 +22,7 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"user",
"verbose_name",
"verbose_name_plural",
- "object_type",
+ "component",
"flow_set",
"promptstage_set",
)
diff --git a/authentik/lib/expression/evaluator.py b/authentik/lib/expression/evaluator.py
index 27db18023..3c6c7087c 100644
--- a/authentik/lib/expression/evaluator.py
+++ b/authentik/lib/expression/evaluator.py
@@ -3,8 +3,8 @@ import re
from textwrap import indent
from typing import Any, Iterable, Optional
-from django.core.exceptions import ValidationError
from requests import Session
+from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
diff --git a/authentik/lib/templatetags/__init__.py b/authentik/lib/templatetags/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/authentik/lib/templatetags/authentik_utils.py b/authentik/lib/templatetags/authentik_utils.py
deleted file mode 100644
index a23e59dbc..000000000
--- a/authentik/lib/templatetags/authentik_utils.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""authentik lib Templatetags"""
-
-from django import template
-from django.db.models import Model
-from structlog.stdlib import get_logger
-
-register = template.Library()
-LOGGER = get_logger()
-
-
-@register.filter("fieldtype")
-def fieldtype(field):
- """Return classname"""
- if isinstance(field.__class__, Model) or issubclass(field.__class__, Model):
- return verbose_name(field)
- return field.__class__.__name__
-
-
-@register.filter(name="css_class")
-def css_class(field, css):
- """Add css class to form field"""
- return field.as_widget(attrs={"class": css})
-
-
-@register.filter
-def verbose_name(obj) -> str:
- """Return Object's Verbose Name"""
- if not obj:
- return ""
- if hasattr(obj, "verbose_name"):
- return obj.verbose_name
- return obj._meta.verbose_name
-
-
-@register.filter
-def form_verbose_name(obj) -> str:
- """Return ModelForm's Object's Verbose Name"""
- if not obj:
- return ""
- return verbose_name(obj._meta.model)
diff --git a/authentik/lib/widgets.py b/authentik/lib/widgets.py
deleted file mode 100644
index f2104313b..000000000
--- a/authentik/lib/widgets.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""Utility Widgets"""
-from itertools import groupby
-
-from django.forms.models import ModelChoiceField, ModelChoiceIterator
-
-
-class GroupedModelChoiceIterator(ModelChoiceIterator):
- """ModelChoiceField which groups objects by their verbose_name"""
-
- def __iter__(self):
- if self.field.empty_label is not None:
- yield ("", self.field.empty_label)
- queryset = self.queryset
- # Can't use iterator() when queryset uses prefetch_related()
- if not queryset._prefetch_related_lookups:
- queryset = queryset.iterator()
- # We can't use DB-level sorting as we sort by subclass
- queryset = sorted(queryset, key=lambda x: x._meta.verbose_name)
- for group, objs in groupby(queryset, key=lambda x: x._meta.verbose_name):
- yield (group, [self.choice(obj) for obj in objs])
-
-
-class GroupedModelChoiceField(ModelChoiceField):
- """ModelChoiceField which groups objects by their verbose_name"""
-
- iterator = GroupedModelChoiceIterator
diff --git a/authentik/managed/api.py b/authentik/managed/api.py
new file mode 100644
index 000000000..a3e8196c5
--- /dev/null
+++ b/authentik/managed/api.py
@@ -0,0 +1,8 @@
+"""Serializer mixin for managed models"""
+from rest_framework.fields import CharField
+
+
+class ManagedSerializer:
+ """Managed Serializer"""
+
+ managed = CharField(read_only=True, allow_null=True)
diff --git a/authentik/outposts/api/outpost_service_connections.py b/authentik/outposts/api/outpost_service_connections.py
index fc545e0fa..4c121f610 100644
--- a/authentik/outposts/api/outpost_service_connections.py
+++ b/authentik/outposts/api/outpost_service_connections.py
@@ -1,9 +1,12 @@
"""Outpost API Views"""
from dataclasses import asdict
-from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
-from rest_framework import mixins
+from kubernetes.client.configuration import Configuration
+from kubernetes.config.config_exception import ConfigException
+from kubernetes.config.kube_config import load_kube_config_from_dict
+from rest_framework import mixins, serializers
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.request import Request
@@ -16,7 +19,6 @@ from authentik.core.api.utils import (
PassiveSerializer,
TypeCreateSerializer,
)
-from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses
from authentik.outposts.models import (
DockerServiceConnection,
@@ -28,11 +30,11 @@ from authentik.outposts.models import (
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
"""ServiceConnection Serializer"""
- object_type = SerializerMethodField()
+ component = SerializerMethodField()
- def get_object_type(self, obj: OutpostServiceConnection) -> str:
- """Get object type so that we know which API Endpoint to use to get the full object"""
- return obj._meta.object_name.lower().replace("serviceconnection", "")
+ def get_component(self, obj: OutpostServiceConnection) -> str:
+ """Get object component so that we know how to edit the object"""
+ return obj.component
class Meta:
@@ -41,7 +43,7 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
"pk",
"name",
"local",
- "object_type",
+ "component",
"verbose_name",
"verbose_name_plural",
]
@@ -68,23 +70,24 @@ class ServiceConnectionViewSet(
filterset_fields = ["name"]
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable service connection types"""
data = []
for subclass in all_subclasses(self.queryset.model):
+ subclass: OutpostServiceConnection
+ # pyright: reportGeneralTypeIssues=false
data.append(
{
- "name": verbose_name(subclass),
+ "name": subclass._meta.verbose_name,
"description": subclass.__doc__,
- "link": reverse("authentik_admin:outpost-service-connection-create")
- + f"?type={subclass.__name__}",
+ "component": subclass().component,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@swagger_auto_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
- @action(detail=True)
+ @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument, invalid-name
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
@@ -115,6 +118,24 @@ class DockerServiceConnectionViewSet(ModelViewSet):
class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
"""KubernetesServiceConnection Serializer"""
+ def validate_kubeconfig(self, kubeconfig):
+ """Validate kubeconfig by attempting to load it"""
+ if kubeconfig == {}:
+ if not self.validated_data["local"]:
+ raise serializers.ValidationError(
+ _(
+ "You can only use an empty kubeconfig when connecting to a local cluster."
+ )
+ )
+ # Empty kubeconfig is valid
+ return kubeconfig
+ config = Configuration()
+ try:
+ load_kube_config_from_dict(kubeconfig, client_configuration=config)
+ except ConfigException:
+ raise serializers.ValidationError(_("Invalid kubeconfig"))
+ return kubeconfig
+
class Meta:
model = KubernetesServiceConnection
diff --git a/authentik/outposts/forms.py b/authentik/outposts/forms.py
deleted file mode 100644
index f8805eb75..000000000
--- a/authentik/outposts/forms.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""Outpost forms"""
-from django import forms
-from django.core.exceptions import ValidationError
-from django.utils.translation import gettext_lazy as _
-from kubernetes.client.configuration import Configuration
-from kubernetes.config.config_exception import ConfigException
-from kubernetes.config.kube_config import load_kube_config_from_dict
-
-from authentik.admin.fields import CodeMirrorWidget, YAMLField
-from authentik.crypto.models import CertificateKeyPair
-from authentik.outposts.models import (
- DockerServiceConnection,
- KubernetesServiceConnection,
-)
-
-
-class DockerServiceConnectionForm(forms.ModelForm):
- """Docker service-connection form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["tls_authentication"].queryset = CertificateKeyPair.objects.filter(
- key_data__isnull=False
- )
-
- class Meta:
-
- model = DockerServiceConnection
- fields = ["name", "local", "url", "tls_verification", "tls_authentication"]
- widgets = {
- "name": forms.TextInput,
- "url": forms.TextInput,
- }
- labels = {
- "url": _("URL"),
- "tls_verification": _("TLS Verification Certificate"),
- "tls_authentication": _("TLS Authentication Certificate"),
- }
-
-
-class KubernetesServiceConnectionForm(forms.ModelForm):
- """Kubernetes service-connection form"""
-
- def clean_kubeconfig(self):
- """Validate kubeconfig by attempting to load it"""
- kubeconfig = self.cleaned_data["kubeconfig"]
- if kubeconfig == {}:
- if not self.cleaned_data["local"]:
- raise ValidationError(
- _("You can only use an empty kubeconfig when local is enabled.")
- )
- # Empty kubeconfig is valid
- return kubeconfig
- config = Configuration()
- try:
- load_kube_config_from_dict(kubeconfig, client_configuration=config)
- except ConfigException:
- raise ValidationError(_("Invalid kubeconfig"))
- return kubeconfig
-
- class Meta:
-
- model = KubernetesServiceConnection
- fields = [
- "name",
- "local",
- "kubeconfig",
- ]
- widgets = {
- "name": forms.TextInput,
- "kubeconfig": CodeMirrorWidget,
- }
- field_classes = {
- "kubeconfig": YAMLField,
- }
diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py
index 9b0680010..442f81371 100644
--- a/authentik/outposts/models.py
+++ b/authentik/outposts/models.py
@@ -1,14 +1,13 @@
"""Outpost models"""
from dataclasses import asdict, dataclass, field
from datetime import datetime
-from typing import Iterable, Optional, Type, Union
+from typing import Iterable, Optional, Union
from uuid import uuid4
from dacite import from_dict
from django.core.cache import cache
from django.db import models, transaction
from django.db.models.base import Model
-from django.forms.models import ModelForm
from django.utils.translation import gettext_lazy as _
from docker.client import DockerClient
from docker.errors import DockerException
@@ -132,8 +131,8 @@ class OutpostServiceConnection(models.Model):
raise NotImplementedError
@property
- def form(self) -> Type[ModelForm]:
- """Return Form class used to edit this object"""
+ def component(self) -> str:
+ """Return component used to edit this object"""
raise NotImplementedError
class Meta:
@@ -180,10 +179,8 @@ class DockerServiceConnection(OutpostServiceConnection):
)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.outposts.forms import DockerServiceConnectionForm
-
- return DockerServiceConnectionForm
+ def component(self) -> str:
+ return "ak-service-connection-docker-form"
def __str__(self) -> str:
return f"Docker Service-Connection {self.name}"
@@ -237,10 +234,8 @@ class KubernetesServiceConnection(OutpostServiceConnection):
)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.outposts.forms import KubernetesServiceConnectionForm
-
- return KubernetesServiceConnectionForm
+ def component(self) -> str:
+ return "ak-service-connection-kubernetes-form"
def __str__(self) -> str:
return f"Kubernetes Service-Connection {self.name}"
diff --git a/authentik/admin/views/__init__.py b/authentik/outposts/tests/__init__.py
similarity index 100%
rename from authentik/admin/views/__init__.py
rename to authentik/outposts/tests/__init__.py
diff --git a/authentik/outposts/tests/test_api.py b/authentik/outposts/tests/test_api.py
new file mode 100644
index 000000000..841f52a15
--- /dev/null
+++ b/authentik/outposts/tests/test_api.py
@@ -0,0 +1,24 @@
+"""Test outpost service connection API"""
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from authentik.core.models import PropertyMapping, User
+
+
+class TestOutpostServiceConnectionsAPI(APITestCase):
+ """Test outpost service connection API"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.mapping = PropertyMapping.objects.create(
+ name="dummy", expression="""return {'foo': 'bar'}"""
+ )
+ self.user = User.objects.get(username="akadmin")
+ self.client.force_login(self.user)
+
+ def test_types(self):
+ """Test OutpostServiceConnections's types endpoint"""
+ response = self.client.get(
+ reverse("authentik_api:outpostserviceconnection-types"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/outposts/tests.py b/authentik/outposts/tests/test_sa.py
similarity index 100%
rename from authentik/outposts/tests.py
rename to authentik/outposts/tests/test_sa.py
diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py
index 012fe262d..7cde0b361 100644
--- a/authentik/policies/api/policies.py
+++ b/authentik/policies/api/policies.py
@@ -1,6 +1,5 @@
"""policy API Views"""
from django.core.cache import cache
-from django.urls import reverse
from drf_yasg.utils import no_body, swagger_auto_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
@@ -19,7 +18,6 @@ from authentik.core.api.utils import (
MetaNameSerializer,
TypeCreateSerializer,
)
-from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
from authentik.policies.models import Policy, PolicyBinding
@@ -34,16 +32,16 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
_resolve_inheritance: bool
- object_type = SerializerMethodField()
+ component = SerializerMethodField()
bound_to = SerializerMethodField()
def __init__(self, *args, resolve_inheritance: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self._resolve_inheritance = resolve_inheritance
- def get_object_type(self, obj: Policy) -> str:
- """Get object type so that we know which API Endpoint to use to get the full object"""
- return obj._meta.object_name.lower().replace("policy", "")
+ def get_component(self, obj: Policy) -> str:
+ """Get object component so that we know how to edit the object"""
+ return obj.component
def get_bound_to(self, obj: Policy) -> int:
"""Return objects policy is bound to"""
@@ -66,7 +64,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
"pk",
"name",
"execution_logging",
- "object_type",
+ "component",
"verbose_name",
"verbose_name_plural",
"bound_to",
@@ -96,24 +94,24 @@ class PolicyViewSet(
)
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable policy types"""
data = []
for subclass in all_subclasses(self.queryset.model):
+ subclass: Policy
data.append(
{
- "name": verbose_name(subclass),
+ "name": subclass._meta.verbose_name,
"description": subclass.__doc__,
- "link": reverse("authentik_admin:policy-create")
- + f"?type={subclass.__name__}",
+ "component": subclass().component,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@permission_required("authentik_policies.view_policy_cache")
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
- @action(detail=False)
+ @action(detail=False, pagination_class=None, filter_backends=[])
def cache_info(self, request: Request) -> Response:
"""Info about cached policies"""
return Response(data={"count": len(cache.keys("policy_*"))})
@@ -139,7 +137,7 @@ class PolicyViewSet(
request_body=PolicyTestSerializer(),
responses={200: PolicyTestResultSerializer()},
)
- @action(detail=True, methods=["POST"])
+ @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
# pylint: disable=unused-argument, invalid-name
def test(self, request: Request, pk: str) -> Response:
"""Test policy"""
diff --git a/authentik/policies/dummy/forms.py b/authentik/policies/dummy/forms.py
deleted file mode 100644
index ed6dae995..000000000
--- a/authentik/policies/dummy/forms.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""authentik Policy forms"""
-
-from django import forms
-from django.utils.translation import gettext as _
-
-from authentik.policies.dummy.models import DummyPolicy
-from authentik.policies.forms import PolicyForm
-
-
-class DummyPolicyForm(PolicyForm):
- """DummyPolicyForm Form"""
-
- class Meta:
-
- model = DummyPolicy
- fields = PolicyForm.Meta.fields + ["result", "wait_min", "wait_max"]
- widgets = {
- "name": forms.TextInput(),
- }
- labels = {"result": _("Allow user")}
diff --git a/authentik/policies/dummy/models.py b/authentik/policies/dummy/models.py
index ee78e7a55..a682d0cc2 100644
--- a/authentik/policies/dummy/models.py
+++ b/authentik/policies/dummy/models.py
@@ -1,10 +1,8 @@
"""Dummy policy"""
from random import SystemRandom
from time import sleep
-from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger
@@ -32,10 +30,8 @@ class DummyPolicy(Policy):
return DummyPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.dummy.forms import DummyPolicyForm
-
- return DummyPolicyForm
+ def component(self) -> str: # pragma: no cover
+ return "ak-policy-dummy-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Wait random time then return result"""
diff --git a/authentik/policies/dummy/tests.py b/authentik/policies/dummy/tests.py
index 8d0cefd5c..c433d0d39 100644
--- a/authentik/policies/dummy/tests.py
+++ b/authentik/policies/dummy/tests.py
@@ -2,7 +2,6 @@
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
-from authentik.policies.dummy.forms import DummyPolicyForm
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyRequest
@@ -22,18 +21,3 @@ class TestDummyPolicy(TestCase):
result = policy.passes(self.request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("dummy",))
-
- def test_form(self):
- """test form"""
- form = DummyPolicyForm(
- data={
- "name": "dummy",
- "negate": False,
- "order": 0,
- "timeout": 1,
- "result": True,
- "wait_min": 1,
- "wait_max": 2,
- }
- )
- self.assertTrue(form.is_valid())
diff --git a/authentik/policies/event_matcher/forms.py b/authentik/policies/event_matcher/forms.py
deleted file mode 100644
index e9707f5db..000000000
--- a/authentik/policies/event_matcher/forms.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""authentik Event Matcher Policy forms"""
-
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.policies.event_matcher.models import EventMatcherPolicy
-from authentik.policies.forms import PolicyForm
-
-
-class EventMatcherPolicyForm(PolicyForm):
- """EventMatcherPolicy Form"""
-
- class Meta:
-
- model = EventMatcherPolicy
- fields = PolicyForm.Meta.fields + [
- "action",
- "client_ip",
- "app",
- ]
- widgets = {
- "name": forms.TextInput(),
- "client_ip": forms.TextInput(),
- }
- labels = {"client_ip": _("Client IP")}
diff --git a/authentik/policies/event_matcher/models.py b/authentik/policies/event_matcher/models.py
index 76930c14f..e1aeefb73 100644
--- a/authentik/policies/event_matcher/models.py
+++ b/authentik/policies/event_matcher/models.py
@@ -1,9 +1,6 @@
"""Event Matcher models"""
-from typing import Type
-
from django.apps import apps
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
@@ -63,10 +60,8 @@ class EventMatcherPolicy(Policy):
return EventMatcherPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.event_matcher.forms import EventMatcherPolicyForm
-
- return EventMatcherPolicyForm
+ def component(self) -> str:
+ return "ak-policy-event-matcher-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
if "event" not in request.context:
diff --git a/authentik/policies/expiry/forms.py b/authentik/policies/expiry/forms.py
deleted file mode 100644
index 08631358d..000000000
--- a/authentik/policies/expiry/forms.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""authentik PasswordExpiry Policy forms"""
-
-from django import forms
-from django.utils.translation import gettext as _
-
-from authentik.policies.expiry.models import PasswordExpiryPolicy
-from authentik.policies.forms import PolicyForm
-
-
-class PasswordExpiryPolicyForm(PolicyForm):
- """Edit PasswordExpiryPolicy instances"""
-
- class Meta:
-
- model = PasswordExpiryPolicy
- fields = PolicyForm.Meta.fields + ["days", "deny_only"]
- widgets = {
- "name": forms.TextInput(),
- "order": forms.NumberInput(),
- "days": forms.NumberInput(),
- }
- labels = {"deny_only": _("Only fail the policy, don't set user's password.")}
diff --git a/authentik/policies/expiry/models.py b/authentik/policies/expiry/models.py
index eca0d9d63..dd76d91af 100644
--- a/authentik/policies/expiry/models.py
+++ b/authentik/policies/expiry/models.py
@@ -1,9 +1,7 @@
"""authentik password_expiry_policy Models"""
from datetime import timedelta
-from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
@@ -29,10 +27,8 @@ class PasswordExpiryPolicy(Policy):
return PasswordExpiryPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.expiry.forms import PasswordExpiryPolicyForm
-
- return PasswordExpiryPolicyForm
+ def component(self) -> str:
+ return "ak-policy-password-expiry-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""If password change date is more than x days in the past, call set_unusable_password
diff --git a/authentik/policies/expression/api.py b/authentik/policies/expression/api.py
index 1ef7a53a8..d4975e097 100644
--- a/authentik/policies/expression/api.py
+++ b/authentik/policies/expression/api.py
@@ -2,12 +2,19 @@
from rest_framework.viewsets import ModelViewSet
from authentik.policies.api.policies import PolicySerializer
+from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy
class ExpressionPolicySerializer(PolicySerializer):
"""Group Membership Policy Serializer"""
+ def validate_expression(self, expr: str) -> str:
+ """validate the syntax of the expression"""
+ name = "temp-policy" if not self.instance else self.instance.name
+ PolicyEvaluator(name).validate(expr)
+ return expr
+
class Meta:
model = ExpressionPolicy
fields = PolicySerializer.Meta.fields + ["expression"]
diff --git a/authentik/policies/expression/forms.py b/authentik/policies/expression/forms.py
deleted file mode 100644
index 4bf2c9a5a..000000000
--- a/authentik/policies/expression/forms.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""authentik Expression Policy forms"""
-
-from django import forms
-
-from authentik.admin.fields import CodeMirrorWidget
-from authentik.policies.expression.evaluator import PolicyEvaluator
-from authentik.policies.expression.models import ExpressionPolicy
-from authentik.policies.forms import PolicyForm
-
-
-class ExpressionPolicyForm(PolicyForm):
- """ExpressionPolicy Form"""
-
- template_name = "policy/expression/form.html"
-
- def clean_expression(self):
- """Test Syntax"""
- expression = self.cleaned_data.get("expression")
- PolicyEvaluator(self.instance.name).validate(expression)
- return expression
-
- class Meta:
-
- model = ExpressionPolicy
- fields = PolicyForm.Meta.fields + [
- "expression",
- ]
- widgets = {
- "name": forms.TextInput(),
- "expression": CodeMirrorWidget(mode="python"),
- }
diff --git a/authentik/policies/expression/models.py b/authentik/policies/expression/models.py
index f4a114954..755be2fbd 100644
--- a/authentik/policies/expression/models.py
+++ b/authentik/policies/expression/models.py
@@ -1,8 +1,5 @@
"""authentik expression Policy Models"""
-from typing import Type
-
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
@@ -23,10 +20,8 @@ class ExpressionPolicy(Policy):
return ExpressionPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.expression.forms import ExpressionPolicyForm
-
- return ExpressionPolicyForm
+ def component(self) -> str:
+ return "ak-policy-expression-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
diff --git a/authentik/policies/expression/templates/policy/expression/form.html b/authentik/policies/expression/templates/policy/expression/form.html
deleted file mode 100644
index 540ddf10b..000000000
--- a/authentik/policies/expression/templates/policy/expression/form.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "generic/form.html" %}
-
-{% load i18n %}
-
-{% block beneath_form %}
-
-{% endblock %}
diff --git a/authentik/policies/expression/tests.py b/authentik/policies/expression/tests.py
index ceb80bd3a..1b1b1a279 100644
--- a/authentik/policies/expression/tests.py
+++ b/authentik/policies/expression/tests.py
@@ -1,9 +1,11 @@
"""evaluator tests"""
-from django.core.exceptions import ValidationError
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
+from rest_framework.serializers import ValidationError
+from rest_framework.test import APITestCase
from authentik.policies.exceptions import PolicyException
+from authentik.policies.expression.api import ExpressionPolicySerializer
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.types import PolicyRequest
@@ -60,3 +62,16 @@ class TestEvaluator(TestCase):
evaluator = PolicyEvaluator("test")
with self.assertRaises(ValidationError):
evaluator.validate(template)
+
+
+class TestExpressionPolicyAPI(APITestCase):
+ """Test expression policy's API"""
+
+ def test_validate(self):
+ """Test ExpressionPolicy's validation"""
+ # Because the root property-mapping has no write operation, we just instantiate
+ # a serializer and test inline
+ expr = "return True"
+ self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr)
+ with self.assertRaises(ValidationError):
+ print(ExpressionPolicySerializer().validate_expression("/"))
diff --git a/authentik/policies/forms.py b/authentik/policies/forms.py
deleted file mode 100644
index 656358fbc..000000000
--- a/authentik/policies/forms.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""General fields"""
-
-from django import forms
-
-from authentik.core.models import Group
-from authentik.lib.widgets import GroupedModelChoiceField
-from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
-
-
-class PolicyBindingForm(forms.ModelForm):
- """Form to edit Policy to PolicyBindingModel Binding"""
-
- target = GroupedModelChoiceField(
- queryset=PolicyBindingModel.objects.all().select_subclasses(),
- to_field_name="pbm_uuid",
- )
- policy = GroupedModelChoiceField(
- queryset=Policy.objects.all().order_by("name").select_subclasses(),
- required=False,
- )
- group = forms.ModelChoiceField(
- queryset=Group.objects.all().order_by("name"), required=False
- )
-
- def __init__(self, *args, **kwargs): # pragma: no cover
- super().__init__(*args, **kwargs)
- if "target" in self.initial:
- self.fields["target"].widget = forms.HiddenInput()
-
- class Meta:
-
- model = PolicyBinding
- fields = ["enabled", "policy", "group", "user", "target", "order", "timeout"]
-
-
-class PolicyForm(forms.ModelForm):
- """Base Policy form"""
-
- class Meta:
-
- model = Policy
- fields = ["name", "execution_logging"]
diff --git a/authentik/policies/hibp/forms.py b/authentik/policies/hibp/forms.py
deleted file mode 100644
index 62708ac9a..000000000
--- a/authentik/policies/hibp/forms.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""authentik HaveIBeenPwned Policy forms"""
-
-from django import forms
-
-from authentik.policies.forms import PolicyForm
-from authentik.policies.hibp.models import HaveIBeenPwendPolicy
-
-
-class HaveIBeenPwnedPolicyForm(PolicyForm):
- """Edit HaveIBeenPwendPolicy instances"""
-
- class Meta:
-
- model = HaveIBeenPwendPolicy
- fields = PolicyForm.Meta.fields + ["password_field", "allowed_count"]
- widgets = {
- "name": forms.TextInput(),
- "password_field": forms.TextInput(),
- }
diff --git a/authentik/policies/hibp/models.py b/authentik/policies/hibp/models.py
index de448cd46..4b2032a1d 100644
--- a/authentik/policies/hibp/models.py
+++ b/authentik/policies/hibp/models.py
@@ -1,9 +1,7 @@
"""authentik HIBP Models"""
from hashlib import sha1
-from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from requests import get
from rest_framework.serializers import BaseSerializer
@@ -35,10 +33,8 @@ class HaveIBeenPwendPolicy(Policy):
return HaveIBeenPwendPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.hibp.forms import HaveIBeenPwnedPolicyForm
-
- return HaveIBeenPwnedPolicyForm
+ def component(self) -> str:
+ return "ak-policy-hibp-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
diff --git a/authentik/policies/models.py b/authentik/policies/models.py
index 19045ae78..e7f0a71b6 100644
--- a/authentik/policies/models.py
+++ b/authentik/policies/models.py
@@ -1,9 +1,7 @@
"""Policy base models"""
-from typing import Type
from uuid import uuid4
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
@@ -147,8 +145,8 @@ class Policy(SerializerModel, CreatedUpdatedModel):
objects = InheritanceAutoManager()
@property
- def form(self) -> Type[ModelForm]:
- """Return Form class used to edit this object"""
+ def component(self) -> str:
+ """Return component used to edit this object"""
raise NotImplementedError
def __str__(self):
diff --git a/authentik/policies/password/forms.py b/authentik/policies/password/forms.py
deleted file mode 100644
index df2de293f..000000000
--- a/authentik/policies/password/forms.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""authentik Policy forms"""
-
-from django import forms
-from django.utils.translation import gettext as _
-
-from authentik.policies.forms import PolicyForm
-from authentik.policies.password.models import PasswordPolicy
-
-
-class PasswordPolicyForm(PolicyForm):
- """PasswordPolicy Form"""
-
- class Meta:
-
- model = PasswordPolicy
- fields = PolicyForm.Meta.fields + [
- "password_field",
- "amount_uppercase",
- "amount_lowercase",
- "amount_symbols",
- "length_min",
- "symbol_charset",
- "error_message",
- ]
- widgets = {
- "name": forms.TextInput(),
- "password_field": forms.TextInput(),
- "symbol_charset": forms.TextInput(),
- "error_message": forms.TextInput(),
- }
- labels = {
- "amount_uppercase": _("Minimum amount of Uppercase Characters"),
- "amount_lowercase": _("Minimum amount of Lowercase Characters"),
- "amount_symbols": _("Minimum amount of Symbols Characters"),
- "length_min": _("Minimum Length"),
- }
diff --git a/authentik/policies/password/models.py b/authentik/policies/password/models.py
index e66957cfe..286033eec 100644
--- a/authentik/policies/password/models.py
+++ b/authentik/policies/password/models.py
@@ -1,9 +1,7 @@
"""user field matcher models"""
import re
-from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger
@@ -38,10 +36,8 @@ class PasswordPolicy(Policy):
return PasswordPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.password.forms import PasswordPolicyForm
-
- return PasswordPolicyForm
+ def component(self) -> str:
+ return "ak-policy-password-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
if self.password_field not in request.context:
diff --git a/authentik/policies/reputation/forms.py b/authentik/policies/reputation/forms.py
deleted file mode 100644
index c3adecb16..000000000
--- a/authentik/policies/reputation/forms.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""authentik reputation request forms"""
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.policies.forms import PolicyForm
-from authentik.policies.reputation.models import ReputationPolicy
-
-
-class ReputationPolicyForm(PolicyForm):
- """Form to edit ReputationPolicy"""
-
- class Meta:
-
- model = ReputationPolicy
- fields = PolicyForm.Meta.fields + ["check_ip", "check_username", "threshold"]
- widgets = {
- "name": forms.TextInput(),
- "value": forms.TextInput(),
- }
- labels = {
- "check_ip": _("Check IP"),
- }
diff --git a/authentik/policies/reputation/models.py b/authentik/policies/reputation/models.py
index 2dfaa834e..4f8bfdc6b 100644
--- a/authentik/policies/reputation/models.py
+++ b/authentik/policies/reputation/models.py
@@ -1,9 +1,6 @@
"""authentik reputation request policy"""
-from typing import Type
-
from django.core.cache import cache
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
@@ -30,10 +27,8 @@ class ReputationPolicy(Policy):
return ReputationPolicySerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.policies.reputation.forms import ReputationPolicyForm
-
- return ReputationPolicyForm
+ def component(self) -> str:
+ return "ak-policy-reputation-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
remote_ip = get_client_ip(request.http_request) or "255.255.255.255"
diff --git a/authentik/policies/templates/policies/denied.html b/authentik/policies/templates/policies/denied.html
index eb942e7c6..aa6b08095 100644
--- a/authentik/policies/templates/policies/denied.html
+++ b/authentik/policies/templates/policies/denied.html
@@ -2,7 +2,6 @@
{% load static %}
{% load i18n %}
-{% load authentik_utils %}
{% block card_title %}
{% trans 'Permission denied' %}
diff --git a/authentik/policies/tests/test_models.py b/authentik/policies/tests/test_models.py
deleted file mode 100644
index 3e13b8528..000000000
--- a/authentik/policies/tests/test_models.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""flow model tests"""
-from typing import Callable, Type
-
-from django.forms import ModelForm
-from django.test import TestCase
-
-from authentik.lib.utils.reflection import all_subclasses
-from authentik.policies.models import Policy
-
-
-class TestPolicyProperties(TestCase):
- """Generic model properties tests"""
-
-
-def policy_tester_factory(model: Type[Policy]) -> Callable:
- """Test a form"""
-
- def tester(self: TestPolicyProperties):
- model_inst = model()
- self.assertTrue(issubclass(model_inst.form, ModelForm))
-
- return tester
-
-
-for policy_type in all_subclasses(Policy):
- setattr(
- TestPolicyProperties,
- f"test_policy_{policy_type.__name__}",
- policy_tester_factory(policy_type),
- )
diff --git a/authentik/policies/tests/test_policies_api.py b/authentik/policies/tests/test_policies_api.py
index 8c3abc941..fc20a0177 100644
--- a/authentik/policies/tests/test_policies_api.py
+++ b/authentik/policies/tests/test_policies_api.py
@@ -26,3 +26,10 @@ class TestPoliciesAPI(APITestCase):
self.assertJSONEqual(
response.content.decode(), {"passing": True, "messages": ["dummy"]}
)
+
+ def test_types(self):
+ """Test Policy's types endpoint"""
+ response = self.client.get(
+ reverse("authentik_api:policy-types"),
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/authentik/providers/oauth2/api/provider.py b/authentik/providers/oauth2/api/provider.py
index 2e9046529..4a6e5d05d 100644
--- a/authentik/providers/oauth2/api/provider.py
+++ b/authentik/providers/oauth2/api/provider.py
@@ -1,22 +1,35 @@
"""OAuth2Provider API Views"""
from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.serializers import Serializer
+from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
+from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
-from authentik.providers.oauth2.models import OAuth2Provider
+from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
class OAuth2ProviderSerializer(ProviderSerializer):
"""OAuth2Provider Serializer"""
+ def validate_jwt_alg(self, value):
+ """Ensure that when RS256 is selected, a certificate-key-pair is selected"""
+ if (
+ self.initial_data.get("rsa_key", None) is None
+ and value == JWTAlgorithms.RS256
+ ):
+ raise ValidationError(
+ _("RS256 requires a Certificate-Key-Pair to be selected.")
+ )
+ return value
+
class Meta:
model = OAuth2Provider
@@ -36,7 +49,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
]
-class OAuth2ProviderSetupURLs(Serializer):
+class OAuth2ProviderSetupURLs(PassiveSerializer):
"""OAuth2 Provider Metadata serializer"""
issuer = ReadOnlyField()
@@ -46,12 +59,6 @@ class OAuth2ProviderSetupURLs(Serializer):
provider_info = ReadOnlyField()
logout = ReadOnlyField()
- def create(self, request: Request) -> Response:
- raise NotImplementedError
-
- def update(self, request: Request) -> Response:
- raise NotImplementedError
-
class OAuth2ProviderViewSet(ModelViewSet):
"""OAuth2Provider Viewset"""
diff --git a/authentik/providers/oauth2/api/scope.py b/authentik/providers/oauth2/api/scope.py
index 3c4d6a077..6ddc12310 100644
--- a/authentik/providers/oauth2/api/scope.py
+++ b/authentik/providers/oauth2/api/scope.py
@@ -1,25 +1,19 @@
"""OAuth2Provider API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
-from authentik.core.api.utils import MetaNameSerializer
+from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.providers.oauth2.models import ScopeMapping
-class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer):
+class ScopeMappingSerializer(PropertyMappingSerializer):
"""ScopeMapping Serializer"""
class Meta:
model = ScopeMapping
- fields = [
- "pk",
- "name",
+ fields = PropertyMappingSerializer.Meta.fields + [
"scope_name",
"description",
- "expression",
- "verbose_name",
- "verbose_name_plural",
]
diff --git a/authentik/providers/oauth2/forms.py b/authentik/providers/oauth2/forms.py
deleted file mode 100644
index 92a1dd067..000000000
--- a/authentik/providers/oauth2/forms.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""authentik OAuth2 Provider Forms"""
-
-from django import forms
-from django.core.exceptions import ValidationError
-from django.utils.translation import gettext as _
-
-from authentik.admin.fields import CodeMirrorWidget
-from authentik.core.expression import PropertyMappingEvaluator
-from authentik.crypto.models import CertificateKeyPair
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.providers.oauth2.generators import (
- generate_client_id,
- generate_client_secret,
-)
-from authentik.providers.oauth2.models import (
- JWTAlgorithms,
- OAuth2Provider,
- ScopeMapping,
-)
-
-
-class OAuth2ProviderForm(forms.ModelForm):
- """OAuth2 Provider form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["authorization_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHORIZATION
- )
- self.fields["client_id"].initial = generate_client_id()
- self.fields["client_secret"].initial = generate_client_secret()
- self.fields["rsa_key"].queryset = CertificateKeyPair.objects.exclude(
- key_data__exact=""
- )
- self.fields["property_mappings"].queryset = ScopeMapping.objects.all()
-
- def clean_jwt_alg(self):
- """Ensure that when RS256 is selected, a certificate-key-pair is selected"""
- if (
- self.data["rsa_key"] == ""
- and self.cleaned_data["jwt_alg"] == JWTAlgorithms.RS256
- ):
- raise ValidationError(
- _("RS256 requires a Certificate-Key-Pair to be selected.")
- )
- return self.cleaned_data["jwt_alg"]
-
- class Meta:
- model = OAuth2Provider
- fields = [
- "name",
- "authorization_flow",
- "client_type",
- "client_id",
- "client_secret",
- "token_validity",
- "jwt_alg",
- "property_mappings",
- "rsa_key",
- "redirect_uris",
- "sub_mode",
- "include_claims_in_id_token",
- "issuer_mode",
- ]
- widgets = {
- "name": forms.TextInput(),
- "token_validity": forms.TextInput(),
- }
- labels = {"property_mappings": _("Scopes")}
- help_texts = {
- "property_mappings": _(
- (
- "Select which scopes
can be used by the client. "
- "The client stil has to specify the scope to access the data."
- )
- )
- }
-
-
-class ScopeMappingForm(forms.ModelForm):
- """Form to edit ScopeMappings"""
-
- template_name = "providers/oauth2/property_mapping_form.html"
-
- def clean_expression(self):
- """Test Syntax"""
- expression = self.cleaned_data.get("expression")
- evaluator = PropertyMappingEvaluator()
- evaluator.validate(expression)
- return expression
-
- class Meta:
-
- model = ScopeMapping
- fields = ["name", "scope_name", "description", "expression"]
- widgets = {
- "name": forms.TextInput(),
- "scope_name": forms.TextInput(),
- "description": forms.TextInput(),
- "expression": CodeMirrorWidget(mode="python"),
- }
diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py
index 1539dbf2a..d8594cd11 100644
--- a/authentik/providers/oauth2/models.py
+++ b/authentik/providers/oauth2/models.py
@@ -13,7 +13,6 @@ from uuid import uuid4
from dacite import from_dict
from django.conf import settings
from django.db import models
-from django.forms import ModelForm
from django.http import HttpRequest
from django.utils import dateformat, timezone
from django.utils.translation import gettext_lazy as _
@@ -112,10 +111,8 @@ class ScopeMapping(PropertyMapping):
)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.providers.oauth2.forms import ScopeMappingForm
-
- return ScopeMappingForm
+ def component(self) -> str:
+ return "ak-property-mapping-scope-form"
@property
def serializer(self) -> Type[Serializer]:
@@ -285,18 +282,16 @@ class OAuth2Provider(Provider):
launch_url = urlparse(main_url)
return main_url.replace(launch_url.path, "")
+ @property
+ def component(self) -> str:
+ return "ak-provider-oauth2-form"
+
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
return OAuth2ProviderSerializer
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.providers.oauth2.forms import OAuth2ProviderForm
-
- return OAuth2ProviderForm
-
def __str__(self):
return f"OAuth2 Provider {self.name}"
diff --git a/authentik/providers/oauth2/templates/providers/oauth2/end_session.html b/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
index a71cd59c4..acf5936d5 100644
--- a/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
+++ b/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
@@ -2,7 +2,6 @@
{% load static %}
{% load i18n %}
-{% load authentik_utils %}
{% block head %}
{{ block.super }}
diff --git a/authentik/providers/oauth2/templates/providers/oauth2/property_mapping_form.html b/authentik/providers/oauth2/templates/providers/oauth2/property_mapping_form.html
deleted file mode 100644
index 030095abe..000000000
--- a/authentik/providers/oauth2/templates/providers/oauth2/property_mapping_form.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "generic/form.html" %}
-
-{% load i18n %}
-
-{% block beneath_form %}
-
-{% endblock %}
diff --git a/authentik/providers/oauth2/tests/test_api.py b/authentik/providers/oauth2/tests/test_api.py
new file mode 100644
index 000000000..6ff1dbfb5
--- /dev/null
+++ b/authentik/providers/oauth2/tests/test_api.py
@@ -0,0 +1,37 @@
+"""Test oauth2 provider API"""
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from authentik.core.models import User
+from authentik.flows.models import Flow, FlowDesignation
+from authentik.providers.oauth2.models import JWTAlgorithms
+
+
+class TestOAuth2ProviderAPI(APITestCase):
+ """Test oauth2 provider API"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.user = User.objects.get(username="akadmin")
+ self.client.force_login(self.user)
+
+ def test_validate(self):
+ """Test OAuth2 Provider validation"""
+ response = self.client.post(
+ reverse(
+ "authentik_api:oauth2provider-list",
+ ),
+ data={
+ "name": "test",
+ "jwt_alg": str(JWTAlgorithms.RS256),
+ "authorization_flow": Flow.objects.filter(
+ designation=FlowDesignation.AUTHORIZATION
+ )
+ .first()
+ .pk,
+ },
+ )
+ self.assertJSONEqual(
+ response.content.decode(),
+ {"jwt_alg": ["RS256 requires a Certificate-Key-Pair to be selected."]},
+ )
diff --git a/authentik/providers/proxy/api.py b/authentik/providers/proxy/api.py
index ff9fc94a2..1fc0a56f2 100644
--- a/authentik/providers/proxy/api.py
+++ b/authentik/providers/proxy/api.py
@@ -1,17 +1,16 @@
"""ProxyProvider API Views"""
from drf_yasg.utils import swagger_serializer_method
from rest_framework.fields import CharField, ListField, SerializerMethodField
-from rest_framework.request import Request
-from rest_framework.response import Response
-from rest_framework.serializers import ModelSerializer, Serializer
+from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
+from authentik.core.api.utils import PassiveSerializer
from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.proxy.models import ProxyProvider
-class OpenIDConnectConfigurationSerializer(Serializer):
+class OpenIDConnectConfigurationSerializer(PassiveSerializer):
"""rest_framework Serializer for OIDC Configuration"""
issuer = CharField()
@@ -27,12 +26,6 @@ class OpenIDConnectConfigurationSerializer(Serializer):
subject_types_supported = ListField(child=CharField())
token_endpoint_auth_methods_supported = ListField(child=CharField())
- def create(self, request: Request) -> Response:
- raise NotImplementedError
-
- def update(self, request: Request) -> Response:
- raise NotImplementedError
-
class ProxyProviderSerializer(ProviderSerializer):
"""ProxyProvider Serializer"""
diff --git a/authentik/providers/proxy/forms.py b/authentik/providers/proxy/forms.py
deleted file mode 100644
index e1433d6bc..000000000
--- a/authentik/providers/proxy/forms.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""authentik Proxy Provider Forms"""
-from django import forms
-
-from authentik.crypto.models import CertificateKeyPair
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.providers.proxy.models import ProxyProvider
-
-
-class ProxyProviderForm(forms.ModelForm):
- """Proxy Provider form"""
-
- instance: ProxyProvider
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["authorization_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHORIZATION
- )
- self.fields["certificate"].queryset = CertificateKeyPair.objects.filter(
- key_data__isnull=False
- ).exclude(key_data="")
-
- def save(self, *args, **kwargs):
- actual_save = super().save(*args, **kwargs)
- self.instance.set_oauth_defaults()
- self.instance.save()
- return actual_save
-
- class Meta:
-
- model = ProxyProvider
- fields = [
- "name",
- "authorization_flow",
- "internal_host",
- "internal_host_ssl_validation",
- "external_host",
- "certificate",
- "skip_path_regex",
- "basic_auth_enabled",
- "basic_auth_user_attribute",
- "basic_auth_password_attribute",
- ]
- widgets = {
- "name": forms.TextInput(),
- "internal_host": forms.TextInput(),
- "external_host": forms.TextInput(),
- "basic_auth_user_attribute": forms.TextInput(),
- "basic_auth_password_attribute": forms.TextInput(),
- }
diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py
index fb19ba201..3cba47c17 100644
--- a/authentik/providers/proxy/models.py
+++ b/authentik/providers/proxy/models.py
@@ -5,7 +5,6 @@ from typing import Iterable, Optional, Type
from urllib.parse import urljoin
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
@@ -102,10 +101,8 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
cookie_secret = models.TextField(default=get_cookie_secret)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.providers.proxy.forms import ProxyProviderForm
-
- return ProxyProviderForm
+ def component(self) -> str:
+ return "ak-provider-proxy-form"
@property
def serializer(self) -> Type[Serializer]:
diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py
index 052de8b5c..5ca0f4463 100644
--- a/authentik/providers/saml/api.py
+++ b/authentik/providers/saml/api.py
@@ -1,17 +1,35 @@
"""SAMLProvider API Views"""
+from xml.etree.ElementTree import ParseError # nosec
+
+from defusedxml.ElementTree import fromstring
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
-from rest_framework.fields import ReadOnlyField
+from rest_framework.fields import CharField, FileField, ReadOnlyField
+from rest_framework.parsers import MultiPartParser
+from rest_framework.permissions import AllowAny
+from rest_framework.relations import SlugRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.serializers import ModelSerializer, Serializer
+from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
+from structlog.stdlib import get_logger
+from authentik.api.decorators import permission_required
+from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.providers import ProviderSerializer
-from authentik.core.api.utils import MetaNameSerializer
+from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
+from authentik.flows.models import Flow, FlowDesignation
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
-from authentik.providers.saml.views.metadata import DescriptorDownloadView
+from authentik.providers.saml.processors.metadata import MetadataProcessor
+from authentik.providers.saml.processors.metadata_parser import (
+ ServiceProviderMetadataParser,
+)
+
+LOGGER = get_logger()
class SAMLProviderSerializer(ProviderSerializer):
@@ -33,19 +51,26 @@ class SAMLProviderSerializer(ProviderSerializer):
"signature_algorithm",
"signing_kp",
"verification_kp",
+ "sp_binding",
]
-class SAMLMetadataSerializer(Serializer):
+class SAMLMetadataSerializer(PassiveSerializer):
"""SAML Provider Metadata serializer"""
metadata = ReadOnlyField()
- def create(self, request: Request) -> Response:
- raise NotImplementedError
- def update(self, request: Request) -> Response:
- raise NotImplementedError
+class SAMLProviderImportSerializer(PassiveSerializer):
+ """Import saml provider from XML Metadata"""
+
+ name = CharField(required=True)
+ # Using SlugField because https://github.com/OpenAPITools/openapi-generator/issues/3278
+ authorization_flow = SlugRelatedField(
+ queryset=Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION),
+ slug_field="slug",
+ )
+ file = FileField()
class SAMLProviderViewSet(ModelViewSet):
@@ -55,32 +80,70 @@ class SAMLProviderViewSet(ModelViewSet):
serializer_class = SAMLProviderSerializer
@swagger_auto_schema(responses={200: SAMLMetadataSerializer(many=False)})
- @action(methods=["GET"], detail=True)
+ @action(methods=["GET"], detail=True, permission_classes=[AllowAny])
# pylint: disable=invalid-name, unused-argument
def metadata(self, request: Request, pk: int) -> Response:
"""Return metadata as XML string"""
- provider = self.get_object()
+ # We don't use self.get_object() on purpose as this view is un-authenticated
+ provider = get_object_or_404(SAMLProvider, pk=pk)
try:
- metadata = DescriptorDownloadView.get_metadata(request, provider)
+ metadata = MetadataProcessor(provider, request).build_entity_descriptor()
+ if "download" in request._request.GET:
+ response = HttpResponse(metadata, content_type="application/xml")
+ response[
+ "Content-Disposition"
+ ] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
+ return response
return Response({"metadata": metadata})
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
return Response({"metadata": ""})
+ @permission_required(
+ None,
+ [
+ "authentik_providers_saml.add_samlprovider",
+ "authentik_crypto.add_certificatekeypair",
+ ],
+ )
+ @swagger_auto_schema(
+ request_body=SAMLProviderImportSerializer(),
+ responses={204: "Successfully imported provider", 400: "Bad request"},
+ )
+ @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
+ def import_metadata(self, request: Request) -> Response:
+ """Create provider from SAML Metadata"""
+ data = SAMLProviderImportSerializer(data=request.data)
+ if not data.is_valid():
+ raise ValidationError(data.errors)
+ file = data.validated_data["file"]
+ # Validate syntax first
+ try:
+ fromstring(file.read())
+ except ParseError:
+ raise ValidationError(_("Invalid XML Syntax"))
+ file.seek(0)
+ try:
+ metadata = ServiceProviderMetadataParser().parse(file.read().decode())
+ metadata.to_provider(
+ data.validated_data["name"], data.validated_data["authorization_flow"]
+ )
+ except ValueError as exc: # pragma: no cover
+ LOGGER.warning(str(exc))
+ return ValidationError(
+ _("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
+ )
+ return Response(status=204)
-class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer):
+
+class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
"""SAMLPropertyMapping Serializer"""
class Meta:
model = SAMLPropertyMapping
- fields = [
- "pk",
- "name",
+ fields = PropertyMappingSerializer.Meta.fields + [
"saml_name",
"friendly_name",
- "expression",
- "verbose_name",
- "verbose_name_plural",
]
diff --git a/authentik/providers/saml/forms.py b/authentik/providers/saml/forms.py
deleted file mode 100644
index d70b27b61..000000000
--- a/authentik/providers/saml/forms.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""authentik SAML IDP Forms"""
-
-from xml.etree.ElementTree import ParseError # nosec
-
-from defusedxml.ElementTree import fromstring
-from django import forms
-from django.core.exceptions import ValidationError
-from django.core.validators import FileExtensionValidator
-from django.utils.html import mark_safe
-from django.utils.translation import gettext_lazy as _
-
-from authentik.admin.fields import CodeMirrorWidget
-from authentik.core.expression import PropertyMappingEvaluator
-from authentik.crypto.models import CertificateKeyPair
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
-
-
-class SAMLProviderForm(forms.ModelForm):
- """SAML Provider form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["authorization_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHORIZATION
- )
- self.fields["property_mappings"].queryset = SAMLPropertyMapping.objects.all()
- self.fields["signing_kp"].queryset = CertificateKeyPair.objects.exclude(
- key_data__iexact=""
- )
-
- class Meta:
-
- model = SAMLProvider
- fields = [
- "name",
- "authorization_flow",
- "acs_url",
- "issuer",
- "sp_binding",
- "audience",
- "signing_kp",
- "verification_kp",
- "property_mappings",
- "name_id_mapping",
- "assertion_valid_not_before",
- "assertion_valid_not_on_or_after",
- "session_valid_not_on_or_after",
- "digest_algorithm",
- "signature_algorithm",
- ]
- widgets = {
- "name": forms.TextInput(),
- "audience": forms.TextInput(),
- "issuer": forms.TextInput(),
- "assertion_valid_not_before": forms.TextInput(),
- "assertion_valid_not_on_or_after": forms.TextInput(),
- "session_valid_not_on_or_after": forms.TextInput(),
- }
-
-
-class SAMLPropertyMappingForm(forms.ModelForm):
- """SAML Property Mapping form"""
-
- template_name = "providers/saml/property_mapping_form.html"
-
- def clean_expression(self):
- """Test Syntax"""
- expression = self.cleaned_data.get("expression")
- evaluator = PropertyMappingEvaluator()
- evaluator.validate(expression)
- return expression
-
- class Meta:
-
- model = SAMLPropertyMapping
- fields = ["name", "saml_name", "friendly_name", "expression"]
- widgets = {
- "name": forms.TextInput(),
- "saml_name": forms.TextInput(),
- "friendly_name": forms.TextInput(),
- "expression": CodeMirrorWidget(mode="python"),
- }
- help_texts = {
- "saml_name": mark_safe(
- _(
- "URN OID used by SAML. This is optional. "
- '
Reference.'
- " If this property mapping is used for NameID Property, "
- "this field is discarded."
- )
- ),
- }
-
-
-class SAMLProviderImportForm(forms.Form):
- """Create a SAML Provider from SP Metadata."""
-
- provider_name = forms.CharField()
- authorization_flow = forms.ModelChoiceField(
- queryset=Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION)
- )
- metadata = forms.FileField(
- validators=[FileExtensionValidator(allowed_extensions=["xml"])]
- )
-
- def clean_metadata(self):
- """Check if the flow is valid XML"""
- metadata = self.cleaned_data["metadata"].read()
- try:
- fromstring(metadata)
- except ParseError:
- raise ValidationError(_("Invalid XML Syntax"))
- self.cleaned_data["metadata"].seek(0)
- return self.cleaned_data["metadata"]
diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py
index 77e900444..b1d77dac9 100644
--- a/authentik/providers/saml/models.py
+++ b/authentik/providers/saml/models.py
@@ -3,7 +3,6 @@ from typing import Optional, Type
from urllib.parse import urlparse
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@@ -171,10 +170,8 @@ class SAMLProvider(Provider):
return SAMLProviderSerializer
@property
- def form(self) -> Type[ModelForm]:
- from authentik.providers.saml.forms import SAMLProviderForm
-
- return SAMLProviderForm
+ def component(self) -> str:
+ return "ak-provider-saml-form"
def __str__(self):
return f"SAML Provider {self.name}"
@@ -192,10 +189,8 @@ class SAMLPropertyMapping(PropertyMapping):
friendly_name = models.TextField(default=None, blank=True, null=True)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.providers.saml.forms import SAMLPropertyMappingForm
-
- return SAMLPropertyMappingForm
+ def component(self) -> str:
+ return "ak-property-mapping-saml-form"
@property
def serializer(self) -> Type[Serializer]:
diff --git a/authentik/providers/saml/templates/providers/saml/import.html b/authentik/providers/saml/templates/providers/saml/import.html
deleted file mode 100644
index d4c72334c..000000000
--- a/authentik/providers/saml/templates/providers/saml/import.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends base_template|default:"generic/form.html" %}
-
-{% load i18n %}
-
-{% block above_form %}
-
-{% trans 'Import SAML Metadata' %}
-
-{% endblock %}
-
-{% block action %}
-{% trans 'Import Metadata' %}
-{% endblock %}
diff --git a/authentik/providers/saml/templates/providers/saml/property_mapping_form.html b/authentik/providers/saml/templates/providers/saml/property_mapping_form.html
deleted file mode 100644
index 030095abe..000000000
--- a/authentik/providers/saml/templates/providers/saml/property_mapping_form.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "generic/form.html" %}
-
-{% load i18n %}
-
-{% block beneath_form %}
-
-{% endblock %}
diff --git a/authentik/providers/saml/tests/test_api.py b/authentik/providers/saml/tests/test_api.py
new file mode 100644
index 000000000..3e34edacc
--- /dev/null
+++ b/authentik/providers/saml/tests/test_api.py
@@ -0,0 +1,115 @@
+"""SAML Provider API Tests"""
+from tempfile import TemporaryFile
+
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from authentik.core.models import Application, User
+from authentik.flows.models import Flow, FlowDesignation
+from authentik.providers.saml.models import SAMLProvider
+from authentik.providers.saml.tests.test_metadata import METADATA_SIMPLE
+
+
+class TestSAMLProviderAPI(APITestCase):
+ """SAML Provider API Tests"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.user = User.objects.get(username="akadmin")
+ self.client.force_login(self.user)
+
+ def test_metadata(self):
+ """Test metadata export (normal)"""
+ provider = SAMLProvider.objects.create(
+ name="test",
+ authorization_flow=Flow.objects.get(
+ slug="default-provider-authorization-implicit-consent"
+ ),
+ )
+ Application.objects.create(name="test", provider=provider, slug="test")
+ response = self.client.get(
+ reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
+ )
+ self.assertEqual(200, response.status_code)
+
+ def test_metadata_download(self):
+ """Test metadata export (download)"""
+ provider = SAMLProvider.objects.create(
+ name="test",
+ authorization_flow=Flow.objects.get(
+ slug="default-provider-authorization-implicit-consent"
+ ),
+ )
+ Application.objects.create(name="test", provider=provider, slug="test")
+ response = self.client.get(
+ reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
+ + "?download",
+ )
+ self.assertEqual(200, response.status_code)
+ self.assertIn("Content-Disposition", response)
+
+ def test_metadata_invalid(self):
+ """Test metadata export (invalid)"""
+ # Provider without application
+ provider = SAMLProvider.objects.create(
+ name="test",
+ authorization_flow=Flow.objects.get(
+ slug="default-provider-authorization-implicit-consent"
+ ),
+ )
+ response = self.client.get(
+ reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
+ )
+ self.assertEqual(200, response.status_code)
+
+ def test_import_success(self):
+ """Test metadata import (success case)"""
+ with TemporaryFile() as metadata:
+ metadata.write(METADATA_SIMPLE.encode())
+ metadata.seek(0)
+ response = self.client.post(
+ reverse("authentik_api:samlprovider-import-metadata"),
+ {
+ "file": metadata,
+ "name": "test",
+ "authorization_flow": Flow.objects.filter(
+ designation=FlowDesignation.AUTHORIZATION
+ )
+ .first()
+ .slug,
+ },
+ format="multipart",
+ )
+ self.assertEqual(204, response.status_code)
+ # We don't test the actual object being created here, that has its own tests
+
+ def test_import_failed(self):
+ """Test metadata import (invalid xml)"""
+ with TemporaryFile() as metadata:
+ metadata.write(b"invalid")
+ metadata.seek(0)
+ response = self.client.post(
+ reverse("authentik_api:samlprovider-import-metadata"),
+ {
+ "file": metadata,
+ "name": "test",
+ "authorization_flow": Flow.objects.filter(
+ designation=FlowDesignation.AUTHORIZATION
+ )
+ .first()
+ .slug,
+ },
+ format="multipart",
+ )
+ self.assertEqual(400, response.status_code)
+
+ def test_import_invalid(self):
+ """Test metadata import (invalid input)"""
+ response = self.client.post(
+ reverse("authentik_api:samlprovider-import-metadata"),
+ {
+ "name": "test",
+ },
+ format="multipart",
+ )
+ self.assertEqual(400, response.status_code)
diff --git a/authentik/providers/saml/urls.py b/authentik/providers/saml/urls.py
index 2b27fcdc3..26a2169a4 100644
--- a/authentik/providers/saml/urls.py
+++ b/authentik/providers/saml/urls.py
@@ -1,7 +1,7 @@
"""authentik SAML IDP URLs"""
from django.urls import path
-from authentik.providers.saml.views import metadata, sso
+from authentik.providers.saml.views import sso
urlpatterns = [
# SSO Bindings
@@ -21,9 +21,4 @@ urlpatterns = [
sso.SAMLSSOBindingInitView.as_view(),
name="sso-init",
),
- path(
- "
/metadata/",
- metadata.DescriptorDownloadView.as_view(),
- name="metadata",
- ),
]
diff --git a/authentik/providers/saml/views/metadata.py b/authentik/providers/saml/views/metadata.py
deleted file mode 100644
index 3cf24b2db..000000000
--- a/authentik/providers/saml/views/metadata.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""authentik SAML IDP Views"""
-
-from django.contrib import messages
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpRequest, HttpResponse
-from django.shortcuts import get_object_or_404
-from django.utils.translation import gettext_lazy as _
-from django.views import View
-from django.views.generic.edit import FormView
-from structlog.stdlib import get_logger
-
-from authentik.core.models import Application, Provider
-from authentik.lib.views import bad_request_message
-from authentik.providers.saml.forms import SAMLProviderImportForm
-from authentik.providers.saml.models import SAMLProvider
-from authentik.providers.saml.processors.metadata import MetadataProcessor
-from authentik.providers.saml.processors.metadata_parser import (
- ServiceProviderMetadataParser,
-)
-
-LOGGER = get_logger()
-
-
-class DescriptorDownloadView(View):
- """Replies with the XML Metadata IDSSODescriptor."""
-
- @staticmethod
- def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
- """Return rendered XML Metadata"""
- return MetadataProcessor(provider, request).build_entity_descriptor()
-
- def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
- """Replies with the XML Metadata IDSSODescriptor."""
- application = get_object_or_404(Application, slug=application_slug)
- provider: SAMLProvider = get_object_or_404(
- SAMLProvider, pk=application.provider_id
- )
- try:
- metadata = DescriptorDownloadView.get_metadata(request, provider)
- except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
- return bad_request_message(
- request, "Provider is not assigned to an application."
- )
- else:
- response = HttpResponse(metadata, content_type="application/xml")
- response[
- "Content-Disposition"
- ] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
- return response
-
-
-class MetadataImportView(LoginRequiredMixin, FormView):
- """Import Metadata from XML, and create provider"""
-
- form_class = SAMLProviderImportForm
- template_name = "providers/saml/import.html"
- success_url = "/"
-
- def dispatch(self, request, *args, **kwargs):
- if not request.user.is_superuser:
- return self.handle_no_permission()
- return super().dispatch(request, *args, **kwargs)
-
- def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
- try:
- metadata = ServiceProviderMetadataParser().parse(
- form.cleaned_data["metadata"].read().decode()
- )
- metadata.to_provider(
- form.cleaned_data["provider_name"],
- form.cleaned_data["authorization_flow"],
- )
- messages.success(self.request, _("Successfully created Provider"))
- except ValueError as exc:
- LOGGER.warning(str(exc))
- messages.error(
- self.request,
- _("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
- )
- return super().form_invalid(form)
- return super().form_valid(form)
diff --git a/authentik/root/settings.py b/authentik/root/settings.py
index 39fe5439b..584a6385c 100644
--- a/authentik/root/settings.py
+++ b/authentik/root/settings.py
@@ -78,7 +78,6 @@ AUTHENTICATION_BACKENDS = [
# Application definition
INSTALLED_APPS = [
- "django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
diff --git a/authentik/root/urls.py b/authentik/root/urls.py
index 95c0a1646..1dd9a9a3a 100644
--- a/authentik/root/urls.py
+++ b/authentik/root/urls.py
@@ -1,9 +1,7 @@
"""authentik URL Configuration"""
from django.conf import settings
from django.conf.urls.static import static
-from django.contrib import admin
from django.urls import include, path
-from django.views.generic import RedirectView
from structlog.stdlib import get_logger
from authentik.core.views import error
@@ -11,13 +9,6 @@ from authentik.lib.utils.reflection import get_apps
from authentik.root.monitoring import LiveView, MetricsView, ReadyView
LOGGER = get_logger()
-admin.autodiscover()
-admin.site.login = RedirectView.as_view(
- pattern_name="authentik_flows:default-authentication"
-)
-admin.site.logout = RedirectView.as_view(
- pattern_name="authentik_flows:default-invalidation"
-)
handler400 = error.BadRequestView.as_view()
handler403 = error.ForbiddenView.as_view()
@@ -54,7 +45,6 @@ for _authentik_app in get_apps():
)
urlpatterns += [
- path("administration/django/", admin.site.urls),
path("metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py
index 01aa37d1c..cb89f0e44 100644
--- a/authentik/sources/ldap/api.py
+++ b/authentik/sources/ldap/api.py
@@ -8,11 +8,11 @@ from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer
-from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer
+from authentik.core.api.utils import PassiveSerializer
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@@ -70,18 +70,13 @@ class LDAPSourceViewSet(ModelViewSet):
)
-class LDAPPropertyMappingSerializer(ModelSerializer, MetaNameSerializer):
+class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""
class Meta:
model = LDAPPropertyMapping
- fields = [
- "pk",
- "name",
- "expression",
+ fields = PropertyMappingSerializer.Meta.fields + [
"object_field",
- "verbose_name",
- "verbose_name_plural",
]
diff --git a/authentik/sources/ldap/forms.py b/authentik/sources/ldap/forms.py
deleted file mode 100644
index ae2088421..000000000
--- a/authentik/sources/ldap/forms.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""authentik LDAP Forms"""
-
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.admin.fields import CodeMirrorWidget
-from authentik.core.expression import PropertyMappingEvaluator
-from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
-
-
-class LDAPSourceForm(forms.ModelForm):
- """LDAPSource Form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["property_mappings"].queryset = LDAPPropertyMapping.objects.all()
- self.fields[
- "property_mappings_group"
- ].queryset = LDAPPropertyMapping.objects.all()
-
- class Meta:
-
- model = LDAPSource
- fields = [
- # we don't use all common fields, as we don't use flows for this
- "name",
- "slug",
- "enabled",
- "policy_engine_mode",
- # -- start of our custom fields
- "server_uri",
- "start_tls",
- "bind_cn",
- "bind_password",
- "base_dn",
- "sync_users",
- "sync_users_password",
- "sync_groups",
- "property_mappings",
- "property_mappings_group",
- "additional_user_dn",
- "additional_group_dn",
- "user_object_filter",
- "group_object_filter",
- "group_membership_field",
- "object_uniqueness_field",
- "sync_parent_group",
- ]
- labels = {"property_mappings_group": _("Group property mappings")}
- widgets = {
- "name": forms.TextInput(),
- "server_uri": forms.TextInput(),
- "bind_cn": forms.TextInput(),
- "bind_password": forms.TextInput(),
- "base_dn": forms.TextInput(),
- "additional_user_dn": forms.TextInput(),
- "additional_group_dn": forms.TextInput(),
- "user_object_filter": forms.TextInput(),
- "group_object_filter": forms.TextInput(),
- "group_membership_field": forms.TextInput(),
- "object_uniqueness_field": forms.TextInput(),
- }
-
-
-class LDAPPropertyMappingForm(forms.ModelForm):
- """LDAP Property Mapping form"""
-
- template_name = "ldap/property_mapping_form.html"
-
- def clean_expression(self):
- """Test Syntax"""
- expression = self.cleaned_data.get("expression")
- evaluator = PropertyMappingEvaluator()
- evaluator.validate(expression)
- return expression
-
- class Meta:
-
- model = LDAPPropertyMapping
- fields = ["name", "object_field", "expression"]
- widgets = {
- "name": forms.TextInput(),
- "ldap_property": forms.TextInput(),
- "object_field": forms.TextInput(),
- "expression": CodeMirrorWidget(mode="python"),
- }
- help_texts = {
- "object_field": _("Field of the user object this value is written to.")
- }
diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py
index 9d74eb73d..bdea6abf0 100644
--- a/authentik/sources/ldap/models.py
+++ b/authentik/sources/ldap/models.py
@@ -2,7 +2,6 @@
from typing import Optional, Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, Connection, Server
from rest_framework.serializers import Serializer
@@ -73,10 +72,8 @@ class LDAPSource(Source):
)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.ldap.forms import LDAPSourceForm
-
- return LDAPSourceForm
+ def component(self) -> str:
+ return "ak-source-ldap-form"
@property
def serializer(self) -> Type[Serializer]:
@@ -119,10 +116,8 @@ class LDAPPropertyMapping(PropertyMapping):
object_field = models.TextField()
@property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.ldap.forms import LDAPPropertyMappingForm
-
- return LDAPPropertyMappingForm
+ def component(self) -> str:
+ return "ak-property-mapping-ldap-form"
@property
def serializer(self) -> Type[Serializer]:
diff --git a/authentik/sources/ldap/templates/ldap/property_mapping_form.html b/authentik/sources/ldap/templates/ldap/property_mapping_form.html
deleted file mode 100644
index 030095abe..000000000
--- a/authentik/sources/ldap/templates/ldap/property_mapping_form.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "generic/form.html" %}
-
-{% load i18n %}
-
-{% block beneath_form %}
-
-{% endblock %}
diff --git a/authentik/sources/oauth/api/source.py b/authentik/sources/oauth/api/source.py
index 0534088eb..0d4b65029 100644
--- a/authentik/sources/oauth/api/source.py
+++ b/authentik/sources/oauth/api/source.py
@@ -1,10 +1,28 @@
"""OAuth Source Serializer"""
from django.urls.base import reverse_lazy
-from rest_framework.fields import SerializerMethodField
+from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
+from rest_framework.decorators import action
+from rest_framework.fields import BooleanField, CharField, SerializerMethodField
+from rest_framework.request import Request
+from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
+from authentik.core.api.utils import PassiveSerializer
from authentik.sources.oauth.models import OAuthSource
+from authentik.sources.oauth.types.manager import MANAGER
+
+
+class SourceTypeSerializer(PassiveSerializer):
+ """Serializer for SourceType"""
+
+ name = CharField(required=True)
+ slug = CharField(required=True)
+ urls_customizable = BooleanField()
+ request_token_url = CharField(read_only=True, allow_null=True)
+ authorization_url = CharField(read_only=True, allow_null=True)
+ access_token_url = CharField(read_only=True, allow_null=True)
+ profile_url = CharField(read_only=True, allow_null=True)
class OAuthSourceSerializer(SourceSerializer):
@@ -22,6 +40,13 @@ class OAuthSourceSerializer(SourceSerializer):
return relative_url
return self.context["request"].build_absolute_uri(relative_url)
+ type = SerializerMethodField()
+
+ @swagger_serializer_method(serializer_or_field=SourceTypeSerializer)
+ def get_type(self, instace: OAuthSource) -> SourceTypeSerializer:
+ """Get source's type configuration"""
+ return SourceTypeSerializer(instace.type).data
+
class Meta:
model = OAuthSource
fields = SourceSerializer.Meta.fields + [
@@ -33,8 +58,8 @@ class OAuthSourceSerializer(SourceSerializer):
"consumer_key",
"consumer_secret",
"callback_url",
+ "type",
]
- extra_kwargs = {"consumer_secret": {"write_only": True}}
class OAuthSourceViewSet(ModelViewSet):
@@ -43,3 +68,12 @@ class OAuthSourceViewSet(ModelViewSet):
queryset = OAuthSource.objects.all()
serializer_class = OAuthSourceSerializer
lookup_field = "slug"
+
+ @swagger_auto_schema(responses={200: SourceTypeSerializer(many=True)})
+ @action(detail=False, pagination_class=None, filter_backends=[])
+ def source_types(self, request: Request) -> Response:
+ """Get all creatable source types"""
+ data = []
+ for source_type in MANAGER.get():
+ data.append(SourceTypeSerializer(source_type).data)
+ return Response(data)
diff --git a/authentik/sources/oauth/forms.py b/authentik/sources/oauth/forms.py
deleted file mode 100644
index bdcd17ab1..000000000
--- a/authentik/sources/oauth/forms.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""authentik oauth_client forms"""
-
-from django import forms
-
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.sources.oauth.models import OAuthSource
-from authentik.sources.oauth.types.manager import MANAGER
-
-
-class OAuthSourceForm(forms.ModelForm):
- """OAuthSource Form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["authentication_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHENTICATION
- )
- self.fields["authentication_flow"].required = True
- self.fields["enrollment_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.ENROLLMENT
- )
- self.fields["enrollment_flow"].required = True
- if hasattr(self.Meta, "overrides"):
- for overide_field, overide_value in getattr(self.Meta, "overrides").items():
- self.fields[overide_field].initial = overide_value
- self.fields[overide_field].widget.attrs["readonly"] = "readonly"
-
- class Meta:
-
- model = OAuthSource
- fields = [
- "name",
- "slug",
- "enabled",
- "policy_engine_mode",
- "authentication_flow",
- "enrollment_flow",
- "provider_type",
- "request_token_url",
- "authorization_url",
- "access_token_url",
- "profile_url",
- "consumer_key",
- "consumer_secret",
- ]
- widgets = {
- "name": forms.TextInput(),
- "consumer_key": forms.TextInput(),
- "consumer_secret": forms.TextInput(),
- "provider_type": forms.Select(choices=MANAGER.get_name_tuple()),
- }
-
-
-class GitHubOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for GitHub"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "github",
- "request_token_url": "",
- "authorization_url": "https://github.com/login/oauth/authorize",
- "access_token_url": "https://github.com/login/oauth/access_token",
- "profile_url": "https://api.github.com/user",
- }
-
-
-class TwitterOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for Twitter"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "twitter",
- "request_token_url": "https://api.twitter.com/oauth/request_token",
- "authorization_url": "https://api.twitter.com/oauth/authenticate",
- "access_token_url": "https://api.twitter.com/oauth/access_token",
- "profile_url": (
- "https://api.twitter.com/1.1/account/"
- "verify_credentials.json?include_email=true"
- ),
- }
-
-
-class FacebookOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for Facebook"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "facebook",
- "request_token_url": "",
- "authorization_url": "https://www.facebook.com/v7.0/dialog/oauth",
- "access_token_url": "https://graph.facebook.com/v7.0/oauth/access_token",
- "profile_url": "https://graph.facebook.com/v7.0/me?fields=id,name,email",
- }
-
-
-class DiscordOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for Discord"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "discord",
- "request_token_url": "",
- "authorization_url": "https://discord.com/api/oauth2/authorize",
- "access_token_url": "https://discord.com/api/oauth2/token",
- "profile_url": "https://discord.com/api/users/@me",
- }
-
-
-class GoogleOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for Google"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "google",
- "request_token_url": "",
- "authorization_url": "https://accounts.google.com/o/oauth2/auth",
- "access_token_url": "https://accounts.google.com/o/oauth2/token",
- "profile_url": "https://www.googleapis.com/oauth2/v1/userinfo",
- }
-
-
-class AzureADOAuthSourceForm(OAuthSourceForm):
- """OAuth Source form with pre-determined URL for AzureAD"""
-
- class Meta(OAuthSourceForm.Meta):
-
- overrides = {
- "provider_type": "azure-ad",
- "request_token_url": "",
- "authorization_url": "https://login.microsoftonline.com/common/oauth2/authorize",
- "access_token_url": "https://login.microsoftonline.com/common/oauth2/token",
- "profile_url": "https://graph.windows.net/myorganization/me?api-version=1.6",
- }
diff --git a/authentik/sources/oauth/migrations/0002_auto_20200520_1108.py b/authentik/sources/oauth/migrations/0002_auto_20200520_1108.py
index 7452ef5b3..6c3bba15a 100644
--- a/authentik/sources/oauth/migrations/0002_auto_20200520_1108.py
+++ b/authentik/sources/oauth/migrations/0002_auto_20200520_1108.py
@@ -47,4 +47,11 @@ class Migration(migrations.Migration):
verbose_name="Profile URL",
),
),
+ migrations.AlterModelOptions(
+ name="oauthsource",
+ options={
+ "verbose_name": "OAuth Source",
+ "verbose_name_plural": "OAuth Sources",
+ },
+ ),
]
diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py
index cb9024b84..8fc39f4de 100644
--- a/authentik/sources/oauth/models.py
+++ b/authentik/sources/oauth/models.py
@@ -1,8 +1,7 @@
"""OAuth Client models"""
-from typing import Optional, Type
+from typing import TYPE_CHECKING, Optional, Type
from django.db import models
-from django.forms import ModelForm
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -11,6 +10,9 @@ from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer
+if TYPE_CHECKING:
+ from authentik.sources.oauth.types.manager import SourceType
+
class OAuthSource(Source):
"""Login using a Generic OAuth provider."""
@@ -43,10 +45,15 @@ class OAuthSource(Source):
consumer_secret = models.TextField()
@property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import OAuthSourceForm
+ def type(self) -> "SourceType":
+ """Return the provider instance for this source"""
+ from authentik.sources.oauth.types.manager import MANAGER
- return OAuthSourceForm
+ return MANAGER.find_type(self)
+
+ @property
+ def component(self) -> str:
+ return "ak-source-oauth-form"
@property
def serializer(self) -> Type[Serializer]:
@@ -79,19 +86,13 @@ class OAuthSource(Source):
class Meta:
- verbose_name = _("Generic OAuth Source")
- verbose_name_plural = _("Generic OAuth Sources")
+ verbose_name = _("OAuth Source")
+ verbose_name_plural = _("OAuth Sources")
class GitHubOAuthSource(OAuthSource):
"""Social Login using GitHub.com or a GitHub-Enterprise Instance."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import GitHubOAuthSourceForm
-
- return GitHubOAuthSourceForm
-
class Meta:
abstract = True
@@ -102,12 +103,6 @@ class GitHubOAuthSource(OAuthSource):
class TwitterOAuthSource(OAuthSource):
"""Social Login using Twitter.com"""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import TwitterOAuthSourceForm
-
- return TwitterOAuthSourceForm
-
class Meta:
abstract = True
@@ -118,12 +113,6 @@ class TwitterOAuthSource(OAuthSource):
class FacebookOAuthSource(OAuthSource):
"""Social Login using Facebook.com."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import FacebookOAuthSourceForm
-
- return FacebookOAuthSourceForm
-
class Meta:
abstract = True
@@ -134,12 +123,6 @@ class FacebookOAuthSource(OAuthSource):
class DiscordOAuthSource(OAuthSource):
"""Social Login using Discord."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import DiscordOAuthSourceForm
-
- return DiscordOAuthSourceForm
-
class Meta:
abstract = True
@@ -150,12 +133,6 @@ class DiscordOAuthSource(OAuthSource):
class GoogleOAuthSource(OAuthSource):
"""Social Login using Google or Gsuite."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import GoogleOAuthSourceForm
-
- return GoogleOAuthSourceForm
-
class Meta:
abstract = True
@@ -166,12 +143,6 @@ class GoogleOAuthSource(OAuthSource):
class AzureADOAuthSource(OAuthSource):
"""Social Login using Azure AD."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import AzureADOAuthSourceForm
-
- return AzureADOAuthSourceForm
-
class Meta:
abstract = True
@@ -182,12 +153,6 @@ class AzureADOAuthSource(OAuthSource):
class OpenIDOAuthSource(OAuthSource):
"""Login using a Generic OpenID-Connect compliant provider."""
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.oauth.forms import OAuthSourceForm
-
- return OAuthSourceForm
-
class Meta:
abstract = True
diff --git a/authentik/sources/oauth/types/azure_ad.py b/authentik/sources/oauth/types/azure_ad.py
index 697bc94fa..1e23516d4 100644
--- a/authentik/sources/oauth/types/azure_ad.py
+++ b/authentik/sources/oauth/types/azure_ad.py
@@ -3,11 +3,10 @@ from typing import Any
from uuid import UUID
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
-@MANAGER.source(kind=RequestKind.CALLBACK, name="Azure AD")
class AzureADOAuthCallback(OAuthCallback):
"""AzureAD OAuth2 Callback"""
@@ -26,3 +25,18 @@ class AzureADOAuthCallback(OAuthCallback):
"email": mail,
"name": info.get("displayName"),
}
+
+
+@MANAGER.type()
+class AzureADType(SourceType):
+ """Azure AD Type definition"""
+
+ callback_view = AzureADOAuthCallback
+ name = "Azure AD"
+ slug = "azure-ad"
+
+ urls_customizable = True
+
+ authorization_url = "https://login.microsoftonline.com/common/oauth2/authorize"
+ access_token_url = "https://login.microsoftonline.com/common/oauth2/token" # nosec
+ profile_url = "https://graph.windows.net/myorganization/me?api-version=1.6"
diff --git a/authentik/sources/oauth/types/discord.py b/authentik/sources/oauth/types/discord.py
index b50aafa77..00bac79fd 100644
--- a/authentik/sources/oauth/types/discord.py
+++ b/authentik/sources/oauth/types/discord.py
@@ -2,12 +2,11 @@
from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
-@MANAGER.source(kind=RequestKind.REDIRECT, name="Discord")
class DiscordOAuthRedirect(OAuthRedirect):
"""Discord OAuth2 Redirect"""
@@ -17,7 +16,6 @@ class DiscordOAuthRedirect(OAuthRedirect):
}
-@MANAGER.source(kind=RequestKind.CALLBACK, name="Discord")
class DiscordOAuth2Callback(OAuthCallback):
"""Discord OAuth2 Callback"""
@@ -32,3 +30,17 @@ class DiscordOAuth2Callback(OAuthCallback):
"email": info.get("email", None),
"name": info.get("username"),
}
+
+
+@MANAGER.type()
+class DiscordType(SourceType):
+ """Discord Type definition"""
+
+ callback_view = DiscordOAuth2Callback
+ redirect_view = DiscordOAuthRedirect
+ name = "Discord"
+ slug = "discord"
+
+ authorization_url = "https://discord.com/api/oauth2/authorize"
+ access_token_url = "https://discord.com/api/oauth2/token" # nosec
+ profile_url = "https://discord.com/api/users/@me"
diff --git a/authentik/sources/oauth/types/facebook.py b/authentik/sources/oauth/types/facebook.py
index 3956413da..ab27d6b6f 100644
--- a/authentik/sources/oauth/types/facebook.py
+++ b/authentik/sources/oauth/types/facebook.py
@@ -5,12 +5,11 @@ from facebook import GraphAPI
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
-@MANAGER.source(kind=RequestKind.REDIRECT, name="Facebook")
class FacebookOAuthRedirect(OAuthRedirect):
"""Facebook OAuth2 Redirect"""
@@ -28,7 +27,6 @@ class FacebookOAuth2Client(OAuth2Client):
return api.get_object("me", fields="id,name,email")
-@MANAGER.source(kind=RequestKind.CALLBACK, name="Facebook")
class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback"""
@@ -45,3 +43,17 @@ class FacebookOAuth2Callback(OAuthCallback):
"email": info.get("email"),
"name": info.get("name"),
}
+
+
+@MANAGER.type()
+class FacebookType(SourceType):
+ """Facebook Type definition"""
+
+ callback_view = FacebookOAuth2Callback
+ redirect_view = FacebookOAuthRedirect
+ name = "Facebook"
+ slug = "facebook"
+
+ authorization_url = "https://www.facebook.com/v7.0/dialog/oauth"
+ access_token_url = "https://graph.facebook.com/v7.0/oauth/access_token" # nosec
+ profile_url = "https://graph.facebook.com/v7.0/me?fields=id,name,email"
diff --git a/authentik/sources/oauth/types/github.py b/authentik/sources/oauth/types/github.py
index 420fa6ba1..c830d4919 100644
--- a/authentik/sources/oauth/types/github.py
+++ b/authentik/sources/oauth/types/github.py
@@ -2,11 +2,10 @@
from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
-@MANAGER.source(kind=RequestKind.CALLBACK, name="GitHub")
class GitHubOAuth2Callback(OAuthCallback):
"""GitHub OAuth2 Callback"""
@@ -21,3 +20,18 @@ class GitHubOAuth2Callback(OAuthCallback):
"email": info.get("email"),
"name": info.get("name"),
}
+
+
+@MANAGER.type()
+class GitHubType(SourceType):
+ """GitHub Type definition"""
+
+ callback_view = GitHubOAuth2Callback
+ name = "GitHub"
+ slug = "github"
+
+ urls_customizable = True
+
+ authorization_url = "https://github.com/login/oauth/authorize"
+ access_token_url = "https://github.com/login/oauth/access_token" # nosec
+ profile_url = "https://api.github.com/user"
diff --git a/authentik/sources/oauth/types/google.py b/authentik/sources/oauth/types/google.py
index c7d1ba8c8..e69004254 100644
--- a/authentik/sources/oauth/types/google.py
+++ b/authentik/sources/oauth/types/google.py
@@ -2,12 +2,11 @@
from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
-@MANAGER.source(kind=RequestKind.REDIRECT, name="Google")
class GoogleOAuthRedirect(OAuthRedirect):
"""Google OAuth2 Redirect"""
@@ -17,7 +16,6 @@ class GoogleOAuthRedirect(OAuthRedirect):
}
-@MANAGER.source(kind=RequestKind.CALLBACK, name="Google")
class GoogleOAuth2Callback(OAuthCallback):
"""Google OAuth2 Callback"""
@@ -32,3 +30,17 @@ class GoogleOAuth2Callback(OAuthCallback):
"email": info.get("email"),
"name": info.get("name"),
}
+
+
+@MANAGER.type()
+class GoogleType(SourceType):
+ """Google Type definition"""
+
+ callback_view = GoogleOAuth2Callback
+ redirect_view = GoogleOAuthRedirect
+ name = "Google"
+ slug = "google"
+
+ authorization_url = "https://accounts.google.com/o/oauth2/auth"
+ access_token_url = "https://accounts.google.com/o/oauth2/token" # nosec
+ profile_url = "https://www.googleapis.com/oauth2/v1/userinfo"
diff --git a/authentik/sources/oauth/types/manager.py b/authentik/sources/oauth/types/manager.py
index ea51b7c5b..b1b921912 100644
--- a/authentik/sources/oauth/types/manager.py
+++ b/authentik/sources/oauth/types/manager.py
@@ -1,16 +1,17 @@
"""Source type manager"""
from enum import Enum
-from typing import Callable
+from typing import TYPE_CHECKING, Callable, Optional
-from django.utils.text import slugify
from structlog.stdlib import get_logger
-from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
+if TYPE_CHECKING:
+ from authentik.sources.oauth.models import OAuthSource
+
class RequestKind(Enum):
"""Enum of OAuth Request types"""
@@ -19,46 +20,67 @@ class RequestKind(Enum):
REDIRECT = "redirect"
+class SourceType:
+ """Source type, allows overriding of urls and views per type"""
+
+ callback_view = OAuthCallback
+ redirect_view = OAuthRedirect
+ name: str
+ slug: str
+
+ urls_customizable = False
+
+ request_token_url: Optional[str] = None
+ authorization_url: Optional[str] = None
+ access_token_url: Optional[str] = None
+ profile_url: Optional[str] = None
+
+
class SourceTypeManager:
"""Manager to hold all Source types."""
- __source_types: dict[RequestKind, dict[str, Callable]] = {}
- __names: list[str] = []
+ __sources: list[SourceType] = []
- def source(self, kind: RequestKind, name: str):
+ def type(self):
"""Class decorator to register classes inline."""
def inner_wrapper(cls):
- if kind.value not in self.__source_types:
- self.__source_types[kind.value] = {}
- self.__source_types[kind.value][slugify(name)] = cls
- self.__names.append(name)
+ self.__sources.append(cls)
return cls
return inner_wrapper
+ def get(self):
+ """Get a list of all source types"""
+ return self.__sources
+
def get_name_tuple(self):
"""Get list of tuples of all registered names"""
- return [(slugify(x), x) for x in set(self.__names)]
+ return [(x.slug, x.name) for x in self.__sources]
- def find(self, source: OAuthSource, kind: RequestKind) -> Callable:
- """Find fitting Source Type"""
- if kind.value in self.__source_types:
- if source.provider_type in self.__source_types[kind.value]:
- return self.__source_types[kind.value][source.provider_type]
+ def find_type(self, source: "OAuthSource") -> SourceType:
+ """Find type based on source"""
+ found_type = None
+ for src_type in self.__sources:
+ if src_type.slug == source.provider_type:
+ return src_type
+ if not found_type:
+ found_type = SourceType()
LOGGER.warning(
"no matching type found, using default",
wanted=source.provider_type,
- have=self.__source_types[kind.value].keys(),
+ have=[x.name for x in self.__sources],
)
- # Return defaults
- if kind == RequestKind.CALLBACK:
- return OAuthCallback
- if kind == RequestKind.REDIRECT:
- return OAuthRedirect
- raise KeyError(
- f"Provider Type {source.provider_type} (type {kind.value}) not found."
- )
+ return found_type
+
+ def find(self, source: "OAuthSource", kind: RequestKind) -> Callable:
+ """Find fitting Source Type"""
+ found_type = self.find_type(source)
+ if kind == RequestKind.CALLBACK:
+ return found_type.callback_view
+ if kind == RequestKind.REDIRECT:
+ return found_type.redirect_view
+ raise ValueError
MANAGER = SourceTypeManager()
diff --git a/authentik/sources/oauth/types/oidc.py b/authentik/sources/oauth/types/oidc.py
index 7fafaead2..e2acf4b63 100644
--- a/authentik/sources/oauth/types/oidc.py
+++ b/authentik/sources/oauth/types/oidc.py
@@ -2,12 +2,11 @@
from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
-@MANAGER.source(kind=RequestKind.REDIRECT, name="OpenID Connect")
class OpenIDConnectOAuthRedirect(OAuthRedirect):
"""OpenIDConnect OAuth2 Redirect"""
@@ -17,7 +16,6 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
}
-@MANAGER.source(kind=RequestKind.CALLBACK, name="OpenID Connect")
class OpenIDConnectOAuth2Callback(OAuthCallback):
"""OpenIDConnect OAuth2 Callback"""
@@ -35,3 +33,15 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
"email": info.get("email"),
"name": info.get("name"),
}
+
+
+@MANAGER.type()
+class OpenIDConnectType(SourceType):
+ """OpenIDConnect Type definition"""
+
+ callback_view = OpenIDConnectOAuth2Callback
+ redirect_view = OpenIDConnectOAuthRedirect
+ name = "OpenID Connect"
+ slug = "openid-connect"
+
+ urls_customizable = True
diff --git a/authentik/sources/oauth/types/reddit.py b/authentik/sources/oauth/types/reddit.py
index 868bb23dc..74c777e6d 100644
--- a/authentik/sources/oauth/types/reddit.py
+++ b/authentik/sources/oauth/types/reddit.py
@@ -5,12 +5,11 @@ from requests.auth import HTTPBasicAuth
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
-@MANAGER.source(kind=RequestKind.REDIRECT, name="reddit")
class RedditOAuthRedirect(OAuthRedirect):
"""Reddit OAuth2 Redirect"""
@@ -30,7 +29,6 @@ class RedditOAuth2Client(OAuth2Client):
return super().get_access_token(auth=auth)
-@MANAGER.source(kind=RequestKind.CALLBACK, name="reddit")
class RedditOAuth2Callback(OAuthCallback):
"""Reddit OAuth2 Callback"""
@@ -48,3 +46,17 @@ class RedditOAuth2Callback(OAuthCallback):
"name": info.get("name"),
"password": None,
}
+
+
+@MANAGER.type()
+class RedditType(SourceType):
+ """Reddit Type definition"""
+
+ callback_view = RedditOAuth2Callback
+ redirect_view = RedditOAuthRedirect
+ name = "reddit"
+ slug = "reddit"
+
+ authorization_url = "https://accounts.google.com/o/oauth2/auth"
+ access_token_url = "https://accounts.google.com/o/oauth2/token" # nosec
+ profile_url = "https://www.googleapis.com/oauth2/v1/userinfo"
diff --git a/authentik/sources/oauth/types/twitter.py b/authentik/sources/oauth/types/twitter.py
index ff4105ee6..df1ed1a9f 100644
--- a/authentik/sources/oauth/types/twitter.py
+++ b/authentik/sources/oauth/types/twitter.py
@@ -2,11 +2,10 @@
from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
-from authentik.sources.oauth.types.manager import MANAGER, RequestKind
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
-@MANAGER.source(kind=RequestKind.CALLBACK, name="Twitter")
class TwitterOAuthCallback(OAuthCallback):
"""Twitter OAuth2 Callback"""
@@ -21,3 +20,20 @@ class TwitterOAuthCallback(OAuthCallback):
"email": info.get("email", None),
"name": info.get("name"),
}
+
+
+@MANAGER.type()
+class TwitterType(SourceType):
+ """Twitter Type definition"""
+
+ callback_view = TwitterOAuthCallback
+ name = "Twitter"
+ slug = "twitter"
+
+ request_token_url = "https://api.twitter.com/oauth/request_token" # nosec
+ authorization_url = "https://api.twitter.com/oauth/authenticate"
+ access_token_url = "https://api.twitter.com/oauth/access_token" # nosec
+ profile_url = (
+ "https://api.twitter.com/1.1/account/"
+ "verify_credentials.json?include_email=true"
+ )
diff --git a/authentik/sources/saml/forms.py b/authentik/sources/saml/forms.py
deleted file mode 100644
index 0a4773b3a..000000000
--- a/authentik/sources/saml/forms.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""authentik SAML SP Forms"""
-
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.crypto.models import CertificateKeyPair
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.sources.saml.models import SAMLSource
-
-
-class SAMLSourceForm(forms.ModelForm):
- """SAML Provider form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.fields["pre_authentication_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.STAGE_CONFIGURATION
- )
- self.fields["authentication_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHENTICATION
- )
- self.fields["enrollment_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.ENROLLMENT
- )
- self.fields["signing_kp"].queryset = CertificateKeyPair.objects.filter(
- certificate_data__isnull=False,
- key_data__isnull=False,
- )
-
- class Meta:
-
- model = SAMLSource
- fields = [
- "name",
- "slug",
- "enabled",
- "policy_engine_mode",
- "pre_authentication_flow",
- "authentication_flow",
- "enrollment_flow",
- "issuer",
- "sso_url",
- "slo_url",
- "binding_type",
- "name_id_policy",
- "allow_idp_initiated",
- "signing_kp",
- "digest_algorithm",
- "signature_algorithm",
- "temporary_user_delete_after",
- ]
- widgets = {
- "name": forms.TextInput(),
- "issuer": forms.TextInput(),
- "sso_url": forms.TextInput(),
- "slo_url": forms.TextInput(),
- "temporary_user_delete_after": forms.TextInput(),
- }
- labels = {
- "name_id_policy": _("Name ID Policy"),
- "allow_idp_initiated": _("Allow IDP-initiated logins"),
- }
diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py
index 64f2b3f63..d35685aac 100644
--- a/authentik/sources/saml/models.py
+++ b/authentik/sources/saml/models.py
@@ -2,7 +2,6 @@
from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.http import HttpRequest
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -146,10 +145,8 @@ class SAMLSource(Source):
)
@property
- def form(self) -> Type[ModelForm]:
- from authentik.sources.saml.forms import SAMLSourceForm
-
- return SAMLSourceForm
+ def component(self) -> str:
+ return "ak-source-saml-form"
@property
def serializer(self) -> Type[Serializer]:
diff --git a/authentik/stages/authenticator_static/forms.py b/authentik/stages/authenticator_static/forms.py
deleted file mode 100644
index 95e6b3447..000000000
--- a/authentik/stages/authenticator_static/forms.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Static Authenticator forms"""
-from django import forms
-
-from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
-
-
-class AuthenticatorStaticStageForm(forms.ModelForm):
- """Static Authenticator Stage setup form"""
-
- class Meta:
-
- model = AuthenticatorStaticStage
- fields = ["name", "configure_flow", "token_count"]
-
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/authenticator_static/models.py b/authentik/stages/authenticator_static/models.py
index d0497b65a..125abb02f 100644
--- a/authentik/stages/authenticator_static/models.py
+++ b/authentik/stages/authenticator_static/models.py
@@ -2,7 +2,6 @@
from typing import Optional, Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -33,12 +32,8 @@ class AuthenticatorStaticStage(ConfigurableStage, Stage):
return AuthenticatorStaticStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.authenticator_static.forms import (
- AuthenticatorStaticStageForm,
- )
-
- return AuthenticatorStaticStageForm
+ def component(self) -> str:
+ return "ak-stage-authenticator-static-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
diff --git a/authentik/stages/authenticator_totp/forms.py b/authentik/stages/authenticator_totp/forms.py
deleted file mode 100644
index 98ebe481e..000000000
--- a/authentik/stages/authenticator_totp/forms.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""OTP Time forms"""
-from django import forms
-
-from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
-
-
-class AuthenticatorTOTPStageForm(forms.ModelForm):
- """OTP Time-based Stage setup form"""
-
- class Meta:
-
- model = AuthenticatorTOTPStage
- fields = ["name", "configure_flow", "digits"]
-
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/authenticator_totp/models.py b/authentik/stages/authenticator_totp/models.py
index 80f5044f4..daf69d015 100644
--- a/authentik/stages/authenticator_totp/models.py
+++ b/authentik/stages/authenticator_totp/models.py
@@ -2,7 +2,6 @@
from typing import Optional, Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -38,10 +37,8 @@ class AuthenticatorTOTPStage(ConfigurableStage, Stage):
return AuthenticatorTOTPStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.authenticator_totp.forms import AuthenticatorTOTPStageForm
-
- return AuthenticatorTOTPStageForm
+ def component(self) -> str:
+ return "ak-stage-authenticator-totp-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
diff --git a/authentik/stages/authenticator_validate/forms.py b/authentik/stages/authenticator_validate/forms.py
deleted file mode 100644
index 16223a326..000000000
--- a/authentik/stages/authenticator_validate/forms.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""OTP Validate stage forms"""
-from django import forms
-
-from authentik.flows.models import NotConfiguredAction
-from authentik.stages.authenticator_validate.models import (
- AuthenticatorValidateStage,
- DeviceClasses,
-)
-
-
-class AuthenticatorValidateStageForm(forms.ModelForm):
- """OTP Validate stage forms"""
-
- def clean_not_configured_action(self):
- """Ensure that a configuration stage is set when not_configured_action is configure"""
- not_configured_action = self.cleaned_data.get("not_configured_action")
- configuration_stage = self.cleaned_data.get("configuration_stage")
- if (
- not_configured_action == NotConfiguredAction.CONFIGURE
- and configuration_stage is None
- ):
- raise forms.ValidationError(
- (
- 'When "Not configured action" is set to "Configure", '
- "you must set a configuration stage."
- )
- )
- return not_configured_action
-
- class Meta:
-
- model = AuthenticatorValidateStage
- fields = [
- "name",
- "not_configured_action",
- "device_classes",
- "configuration_stage",
- ]
-
- widgets = {
- "name": forms.TextInput(),
- "device_classes": forms.SelectMultiple(choices=DeviceClasses.choices),
- }
diff --git a/authentik/stages/authenticator_validate/migrations/0007_auto_20210403_0927.py b/authentik/stages/authenticator_validate/migrations/0007_auto_20210403_0927.py
new file mode 100644
index 000000000..bebb772ae
--- /dev/null
+++ b/authentik/stages/authenticator_validate/migrations/0007_auto_20210403_0927.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1.7 on 2021-04-03 09:27
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+import authentik.stages.authenticator_validate.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_authenticator_validate", "0006_auto_20210301_1757"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="authenticatorvalidatestage",
+ name="device_classes",
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.TextField(
+ choices=[
+ ("static", "Static"),
+ ("totp", "TOTP"),
+ ("webauthn", "WebAuthn"),
+ ]
+ ),
+ default=authentik.stages.authenticator_validate.models.default_device_classes,
+ help_text="Device classes which can be used to authenticate",
+ size=None,
+ ),
+ ),
+ ]
diff --git a/authentik/stages/authenticator_validate/models.py b/authentik/stages/authenticator_validate/models.py
index dce7e0b59..321d51128 100644
--- a/authentik/stages/authenticator_validate/models.py
+++ b/authentik/stages/authenticator_validate/models.py
@@ -3,7 +3,6 @@ from typing import Type
from django.contrib.postgres.fields.array import ArrayField
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -52,7 +51,7 @@ class AuthenticatorValidateStage(Stage):
)
device_classes = ArrayField(
- models.TextField(),
+ models.TextField(choices=DeviceClasses.choices),
help_text=_("Device classes which can be used to authenticate"),
default=default_device_classes,
)
@@ -74,12 +73,8 @@ class AuthenticatorValidateStage(Stage):
return AuthenticatorValidateStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.authenticator_validate.forms import (
- AuthenticatorValidateStageForm,
- )
-
- return AuthenticatorValidateStageForm
+ def component(self) -> str:
+ return "ak-stage-authenticator-validate-form"
class Meta:
diff --git a/authentik/stages/authenticator_webauthn/forms.py b/authentik/stages/authenticator_webauthn/forms.py
deleted file mode 100644
index 881bf54d7..000000000
--- a/authentik/stages/authenticator_webauthn/forms.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""Webauthn stage forms"""
-from django import forms
-
-from authentik.stages.authenticator_webauthn.models import (
- AuthenticateWebAuthnStage,
- WebAuthnDevice,
-)
-
-
-class AuthenticateWebAuthnStageForm(forms.ModelForm):
- """OTP Time-based Stage setup form"""
-
- class Meta:
-
- model = AuthenticateWebAuthnStage
- fields = ["name"]
-
- widgets = {
- "name": forms.TextInput(),
- }
-
-
-class DeviceEditForm(forms.ModelForm):
- """Form to edit webauthn device"""
-
- class Meta:
-
- model = WebAuthnDevice
- fields = ["name"]
-
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py
index fdae8dd28..8358653fc 100644
--- a/authentik/stages/authenticator_webauthn/models.py
+++ b/authentik/stages/authenticator_webauthn/models.py
@@ -3,7 +3,6 @@ from typing import Optional, Type
from django.contrib.auth import get_user_model
from django.db import models
-from django.forms import ModelForm
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -34,12 +33,8 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
return AuthenticatorWebAuthnStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.authenticator_webauthn.forms import (
- AuthenticateWebAuthnStageForm,
- )
-
- return AuthenticateWebAuthnStageForm
+ def component(self) -> str:
+ return "ak-stage-authenticator-webauthn-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
diff --git a/authentik/stages/captcha/forms.py b/authentik/stages/captcha/forms.py
deleted file mode 100644
index 7902bcbd7..000000000
--- a/authentik/stages/captcha/forms.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""authentik captcha stage forms"""
-from django import forms
-
-from authentik.stages.captcha.models import CaptchaStage
-
-
-class CaptchaStageForm(forms.ModelForm):
- """Form to edit CaptchaStage Instance"""
-
- class Meta:
-
- model = CaptchaStage
- fields = ["name", "public_key", "private_key"]
- widgets = {
- "name": forms.TextInput(),
- "public_key": forms.TextInput(),
- "private_key": forms.TextInput(),
- }
diff --git a/authentik/stages/captcha/models.py b/authentik/stages/captcha/models.py
index d2a0fe501..17d4a721e 100644
--- a/authentik/stages/captcha/models.py
+++ b/authentik/stages/captcha/models.py
@@ -2,7 +2,6 @@
from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -37,10 +36,8 @@ class CaptchaStage(Stage):
return CaptchaStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.captcha.forms import CaptchaStageForm
-
- return CaptchaStageForm
+ def component(self) -> str:
+ return "ak-stage-captcha-form"
class Meta:
diff --git a/authentik/stages/consent/forms.py b/authentik/stages/consent/forms.py
deleted file mode 100644
index f61e483fd..000000000
--- a/authentik/stages/consent/forms.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""authentik consent stage forms"""
-from django import forms
-
-from authentik.stages.consent.models import ConsentStage
-
-
-class ConsentStageForm(forms.ModelForm):
- """Form to edit ConsentStage Instance"""
-
- class Meta:
-
- model = ConsentStage
- fields = ["name", "mode", "consent_expire_in"]
- widgets = {
- "name": forms.TextInput(),
- "consent_expire_in": forms.TextInput(),
- }
diff --git a/authentik/stages/consent/models.py b/authentik/stages/consent/models.py
index 4a96944e2..9c2fa5433 100644
--- a/authentik/stages/consent/models.py
+++ b/authentik/stages/consent/models.py
@@ -2,7 +2,6 @@
from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -51,10 +50,8 @@ class ConsentStage(Stage):
return ConsentStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.consent.forms import ConsentStageForm
-
- return ConsentStageForm
+ def component(self) -> str:
+ return "ak-stage-consent-form"
class Meta:
diff --git a/authentik/stages/deny/forms.py b/authentik/stages/deny/forms.py
deleted file mode 100644
index d1c66e646..000000000
--- a/authentik/stages/deny/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""authentik flows deny forms"""
-from django import forms
-
-from authentik.stages.deny.models import DenyStage
-
-
-class DenyStageForm(forms.ModelForm):
- """Form to create/edit DenyStage instances"""
-
- class Meta:
-
- model = DenyStage
- fields = ["name"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/deny/models.py b/authentik/stages/deny/models.py
index 5c201de7d..b403679a0 100644
--- a/authentik/stages/deny/models.py
+++ b/authentik/stages/deny/models.py
@@ -1,7 +1,6 @@
"""deny stage models"""
from typing import Type
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -25,10 +24,8 @@ class DenyStage(Stage):
return DenyStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.deny.forms import DenyStageForm
-
- return DenyStageForm
+ def component(self) -> str:
+ return "ak-stage-deny-form"
class Meta:
diff --git a/authentik/stages/deny/tests.py b/authentik/stages/deny/tests.py
index 22b4babc9..f87d4601a 100644
--- a/authentik/stages/deny/tests.py
+++ b/authentik/stages/deny/tests.py
@@ -9,7 +9,6 @@ from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
-from authentik.stages.deny.forms import DenyStageForm
from authentik.stages.deny.models import DenyStage
@@ -52,8 +51,3 @@ class TestUserDenyStage(TestCase):
"type": ChallengeTypes.NATIVE.value,
},
)
-
- def test_form(self):
- """Test Form"""
- data = {"name": "test"}
- self.assertEqual(DenyStageForm(data).is_valid(), True)
diff --git a/authentik/stages/dummy/forms.py b/authentik/stages/dummy/forms.py
deleted file mode 100644
index 92420cdd6..000000000
--- a/authentik/stages/dummy/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""authentik administration forms"""
-from django import forms
-
-from authentik.stages.dummy.models import DummyStage
-
-
-class DummyStageForm(forms.ModelForm):
- """Form to create/edit Dummy Stage"""
-
- class Meta:
-
- model = DummyStage
- fields = ["name"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/dummy/models.py b/authentik/stages/dummy/models.py
index 344868a82..2e19ec22a 100644
--- a/authentik/stages/dummy/models.py
+++ b/authentik/stages/dummy/models.py
@@ -1,7 +1,6 @@
"""dummy stage models"""
from typing import Type
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -27,10 +26,8 @@ class DummyStage(Stage):
return DummyStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.dummy.forms import DummyStageForm
-
- return DummyStageForm
+ def component(self) -> str:
+ return "ak-stage-dummy-form"
class Meta:
diff --git a/authentik/stages/dummy/tests.py b/authentik/stages/dummy/tests.py
index 02dd11876..b943d391e 100644
--- a/authentik/stages/dummy/tests.py
+++ b/authentik/stages/dummy/tests.py
@@ -5,7 +5,6 @@ from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
-from authentik.stages.dummy.forms import DummyStageForm
from authentik.stages.dummy.models import DummyStage
@@ -49,8 +48,3 @@ class TestDummyStage(TestCase):
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
-
- def test_form(self):
- """Test Form"""
- data = {"name": "test"}
- self.assertEqual(DummyStageForm(data).is_valid(), True)
diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py
index 2a41c0f82..116538b80 100644
--- a/authentik/stages/email/api.py
+++ b/authentik/stages/email/api.py
@@ -37,3 +37,5 @@ class EmailStageViewSet(ModelViewSet):
queryset = EmailStage.objects.all()
serializer_class = EmailStageSerializer
+
+ # TODO: Validate connection settings when use_global_settings is unchecked
diff --git a/authentik/stages/email/forms.py b/authentik/stages/email/forms.py
deleted file mode 100644
index 5e87b6544..000000000
--- a/authentik/stages/email/forms.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""authentik administration forms"""
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.stages.email.models import EmailStage, get_template_choices
-
-
-class EmailStageForm(forms.ModelForm):
- """Form to create/edit Email Stage"""
-
- template = forms.ChoiceField(choices=get_template_choices)
-
- class Meta:
-
- model = EmailStage
- fields = [
- "name",
- "use_global_settings",
- "token_expiry",
- "subject",
- "template",
- "host",
- "port",
- "username",
- "password",
- "use_tls",
- "use_ssl",
- "timeout",
- "from_address",
- ]
- widgets = {
- "name": forms.TextInput(),
- "host": forms.TextInput(),
- "subject": forms.TextInput(),
- "username": forms.TextInput(),
- "password": forms.TextInput(),
- }
- labels = {
- "use_tls": _("Use TLS"),
- "use_ssl": _("Use SSL"),
- }
diff --git a/authentik/stages/email/models.py b/authentik/stages/email/models.py
index 936952b15..65c155f4b 100644
--- a/authentik/stages/email/models.py
+++ b/authentik/stages/email/models.py
@@ -7,7 +7,6 @@ from django.conf import settings
from django.core.mail import get_connection
from django.core.mail.backends.base import BaseEmailBackend
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -31,6 +30,7 @@ class EmailTemplates(models.TextChoices):
)
+# TODO: Create api for choices
def get_template_choices():
"""Get all available Email templates, including dynamically mounted ones.
Directories are taken from TEMPLATES.DIR setting"""
@@ -95,10 +95,8 @@ class EmailStage(Stage):
return EmailStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.email.forms import EmailStageForm
-
- return EmailStageForm
+ def component(self) -> str:
+ return "ak-stage-email-form"
@property
def backend(self) -> BaseEmailBackend:
diff --git a/authentik/stages/email/templates/email/password_reset.html b/authentik/stages/email/templates/email/password_reset.html
index 3d6659ad4..d83a0cee1 100644
--- a/authentik/stages/email/templates/email/password_reset.html
+++ b/authentik/stages/email/templates/email/password_reset.html
@@ -1,6 +1,5 @@
{% extends "email/base.html" %}
-{% load authentik_utils %}
{% load i18n %}
{% load humanize %}
diff --git a/authentik/stages/identification/forms.py b/authentik/stages/identification/forms.py
deleted file mode 100644
index 29e9f37b0..000000000
--- a/authentik/stages/identification/forms.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""authentik flows identification forms"""
-from django import forms
-from structlog.stdlib import get_logger
-
-from authentik.admin.fields import ArrayFieldSelectMultiple
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.stages.identification.models import IdentificationStage, UserFields
-
-LOGGER = get_logger()
-
-
-class IdentificationStageForm(forms.ModelForm):
- """Form to create/edit IdentificationStage instances"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["enrollment_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.ENROLLMENT
- )
- self.fields["recovery_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.RECOVERY
- )
-
- class Meta:
-
- model = IdentificationStage
- fields = [
- "name",
- "user_fields",
- "case_insensitive_matching",
- "show_matched_user",
- "enrollment_flow",
- "recovery_flow",
- ]
- widgets = {
- "name": forms.TextInput(),
- "user_fields": ArrayFieldSelectMultiple(choices=UserFields.choices),
- }
diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py
index a7b2f4218..c00f3e2b8 100644
--- a/authentik/stages/identification/models.py
+++ b/authentik/stages/identification/models.py
@@ -3,7 +3,6 @@ from typing import Type
from django.contrib.postgres.fields import ArrayField
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -84,10 +83,8 @@ class IdentificationStage(Stage):
return IdentificationStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.identification.forms import IdentificationStageForm
-
- return IdentificationStageForm
+ def component(self) -> str:
+ return "ak-stage-identification-form"
class Meta:
diff --git a/authentik/stages/invitation/forms.py b/authentik/stages/invitation/forms.py
deleted file mode 100644
index a34bbe740..000000000
--- a/authentik/stages/invitation/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""authentik flows invitation forms"""
-from django import forms
-
-from authentik.stages.invitation.models import InvitationStage
-
-
-class InvitationStageForm(forms.ModelForm):
- """Form to create/edit InvitationStage instances"""
-
- class Meta:
-
- model = InvitationStage
- fields = ["name", "continue_flow_without_invitation"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/invitation/models.py b/authentik/stages/invitation/models.py
index 7d97a0ebb..9e013b1f6 100644
--- a/authentik/stages/invitation/models.py
+++ b/authentik/stages/invitation/models.py
@@ -3,7 +3,6 @@ from typing import Type
from uuid import uuid4
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -40,10 +39,8 @@ class InvitationStage(Stage):
return InvitationStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.invitation.forms import InvitationStageForm
-
- return InvitationStageForm
+ def component(self) -> str:
+ return "ak-stage-invitation-form"
class Meta:
diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py
index c0df4b6f7..121f76269 100644
--- a/authentik/stages/invitation/tests.py
+++ b/authentik/stages/invitation/tests.py
@@ -14,7 +14,6 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN
-from authentik.stages.invitation.forms import InvitationStageForm
from authentik.stages.invitation.models import Invitation, InvitationStage
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
@@ -36,11 +35,6 @@ class TestUserLoginStage(TestCase):
self.stage = InvitationStage.objects.create(name="invitation")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
- def test_form(self):
- """Test Form"""
- data = {"name": "test"}
- self.assertEqual(InvitationStageForm(data).is_valid(), True)
-
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
diff --git a/authentik/stages/password/forms.py b/authentik/stages/password/forms.py
deleted file mode 100644
index 5fba58db9..000000000
--- a/authentik/stages/password/forms.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""authentik administration forms"""
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from authentik.flows.models import Flow, FlowDesignation
-from authentik.stages.password.models import PasswordStage
-
-
-def get_authentication_backends():
- """Return all available authentication backends as tuple set"""
- return [
- (
- "django.contrib.auth.backends.ModelBackend",
- _("authentik-internal Userdatabase"),
- ),
- (
- "authentik.sources.ldap.auth.LDAPBackend",
- _("authentik LDAP"),
- ),
- ]
-
-
-class PasswordStageForm(forms.ModelForm):
- """Form to create/edit Password Stages"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["configure_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.STAGE_CONFIGURATION
- )
-
- class Meta:
-
- model = PasswordStage
- fields = ["name", "backends", "configure_flow", "failed_attempts_before_cancel"]
- widgets = {
- "name": forms.TextInput(),
- "backends": forms.SelectMultiple(choices=get_authentication_backends()),
- }
diff --git a/authentik/stages/password/migrations/0005_auto_20210402_2221.py b/authentik/stages/password/migrations/0005_auto_20210402_2221.py
new file mode 100644
index 000000000..d88061dd3
--- /dev/null
+++ b/authentik/stages/password/migrations/0005_auto_20210402_2221.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.1.7 on 2021-04-02 22:21
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_password", "0004_auto_20200925_1057"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="passwordstage",
+ name="backends",
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.TextField(
+ choices=[
+ (
+ "django.contrib.auth.backends.ModelBackend",
+ "authentik-internal Userdatabase",
+ ),
+ ("authentik.sources.ldap.auth.LDAPBackend", "authentik LDAP"),
+ ]
+ ),
+ help_text="Selection of backends to test the password against.",
+ size=None,
+ ),
+ ),
+ ]
diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py
index a4c8619fc..a4e14e51f 100644
--- a/authentik/stages/password/models.py
+++ b/authentik/stages/password/models.py
@@ -3,7 +3,6 @@ from typing import Optional, Type
from django.contrib.postgres.fields import ArrayField
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -12,11 +11,25 @@ from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
+def get_authentication_backends():
+ """Return all available authentication backends as tuple set"""
+ return [
+ (
+ "django.contrib.auth.backends.ModelBackend",
+ _("authentik-internal Userdatabase"),
+ ),
+ (
+ "authentik.sources.ldap.auth.LDAPBackend",
+ _("authentik LDAP"),
+ ),
+ ]
+
+
class PasswordStage(ConfigurableStage, Stage):
"""Prompts the user for their password, and validates it against the configured backends."""
backends = ArrayField(
- models.TextField(),
+ models.TextField(choices=get_authentication_backends()),
help_text=_("Selection of backends to test the password against."),
)
failed_attempts_before_cancel = models.IntegerField(
@@ -42,10 +55,8 @@ class PasswordStage(ConfigurableStage, Stage):
return PasswordStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.password.forms import PasswordStageForm
-
- return PasswordStageForm
+ def component(self) -> str:
+ return "ak-stage-password-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
diff --git a/authentik/stages/prompt/forms.py b/authentik/stages/prompt/forms.py
deleted file mode 100644
index 9f3b22bde..000000000
--- a/authentik/stages/prompt/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Prompt forms"""
-from django import forms
-
-from authentik.stages.prompt.models import PromptStage
-
-
-class PromptStageForm(forms.ModelForm):
- """Form to create/edit Prompt Stage instances"""
-
- class Meta:
-
- model = PromptStage
- fields = ["name", "fields", "validation_policies"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/prompt/models.py b/authentik/stages/prompt/models.py
index c5a00095f..686931103 100644
--- a/authentik/stages/prompt/models.py
+++ b/authentik/stages/prompt/models.py
@@ -3,7 +3,6 @@ from typing import Type
from uuid import uuid4
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.fields import (
@@ -144,10 +143,8 @@ class PromptStage(Stage):
return PromptStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.prompt.forms import PromptStageForm
-
- return PromptStageForm
+ def component(self) -> str:
+ return "ak-stage-prompt-form"
class Meta:
diff --git a/authentik/stages/user_delete/forms.py b/authentik/stages/user_delete/forms.py
deleted file mode 100644
index daf2e6fe0..000000000
--- a/authentik/stages/user_delete/forms.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""authentik flows delete forms"""
-from django import forms
-
-from authentik.stages.user_delete.models import UserDeleteStage
-
-
-class UserDeleteStageForm(forms.ModelForm):
- """Form to delete/edit UserDeleteStage instances"""
-
- class Meta:
-
- model = UserDeleteStage
- fields = ["name"]
- widgets = {
- "name": forms.TextInput(),
- }
-
-
-class UserDeleteForm(forms.Form):
- """Confirmation form to ensure user knows they are deleting their profile"""
diff --git a/authentik/stages/user_delete/models.py b/authentik/stages/user_delete/models.py
index 7e791e8c0..f8a6cf72f 100644
--- a/authentik/stages/user_delete/models.py
+++ b/authentik/stages/user_delete/models.py
@@ -1,7 +1,6 @@
"""delete stage models"""
from typing import Type
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -26,10 +25,8 @@ class UserDeleteStage(Stage):
return UserDeleteStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.user_delete.forms import UserDeleteStageForm
-
- return UserDeleteStageForm
+ def component(self) -> str:
+ return "ak-stage-user-delete-form"
class Meta:
diff --git a/authentik/stages/user_delete/stage.py b/authentik/stages/user_delete/stage.py
index 03c34afd6..15945dbe2 100644
--- a/authentik/stages/user_delete/stage.py
+++ b/authentik/stages/user_delete/stage.py
@@ -2,31 +2,25 @@
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
-from django.views.generic import FormView
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
-from authentik.stages.user_delete.forms import UserDeleteForm
LOGGER = get_logger()
-class UserDeleteStageView(FormView, StageView):
+class UserDeleteStageView(StageView):
"""Finalise unenrollment flow by deleting the user object."""
- form_class = UserDeleteForm
-
def get(self, request: HttpRequest) -> HttpResponse:
+ """Delete currently pending user"""
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
message = _("No Pending User.")
messages.error(request, message)
LOGGER.debug(message)
return self.executor.stage_invalid()
- return super().get(request)
-
- def form_valid(self, form: UserDeleteForm) -> HttpResponse:
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
user.delete()
LOGGER.debug("Deleted user", user=user)
diff --git a/authentik/stages/user_delete/tests.py b/authentik/stages/user_delete/tests.py
index 75d876b1c..48d0a86b7 100644
--- a/authentik/stages/user_delete/tests.py
+++ b/authentik/stages/user_delete/tests.py
@@ -73,24 +73,6 @@ class TestUserDeleteStage(TestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertEqual(response.status_code, 200)
-
- def test_user_delete_post(self):
- """Test User delete (actual)"""
- plan = FlowPlan(
- flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
- )
- plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
- session = self.client.session
- session[SESSION_KEY_PLAN] = plan
- session.save()
-
- response = self.client.post(
- reverse(
- "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
- ),
- {},
- )
- self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
diff --git a/authentik/stages/user_login/forms.py b/authentik/stages/user_login/forms.py
deleted file mode 100644
index 8693a7892..000000000
--- a/authentik/stages/user_login/forms.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""authentik flows login forms"""
-from django import forms
-
-from authentik.stages.user_login.models import UserLoginStage
-
-
-class UserLoginStageForm(forms.ModelForm):
- """Form to create/edit UserLoginStage instances"""
-
- class Meta:
-
- model = UserLoginStage
- fields = ["name", "session_duration"]
- widgets = {
- "name": forms.TextInput(),
- "session_duration": forms.TextInput(),
- }
diff --git a/authentik/stages/user_login/models.py b/authentik/stages/user_login/models.py
index db885dbd3..4d47a449f 100644
--- a/authentik/stages/user_login/models.py
+++ b/authentik/stages/user_login/models.py
@@ -2,7 +2,6 @@
from typing import Type
from django.db import models
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -37,10 +36,8 @@ class UserLoginStage(Stage):
return UserLoginStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.user_login.forms import UserLoginStageForm
-
- return UserLoginStageForm
+ def component(self) -> str:
+ return "ak-stage-user-login-form"
class Meta:
diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py
index d05e6dc07..b2e636802 100644
--- a/authentik/stages/user_login/tests.py
+++ b/authentik/stages/user_login/tests.py
@@ -13,7 +13,6 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
-from authentik.stages.user_login.forms import UserLoginStageForm
from authentik.stages.user_login.models import UserLoginStage
@@ -112,10 +111,3 @@ class TestUserLoginStage(TestCase):
"type": ChallengeTypes.NATIVE.value,
},
)
-
- def test_form(self):
- """Test Form"""
- data = {"name": "test", "session_duration": "seconds=0"}
- self.assertEqual(UserLoginStageForm(data).is_valid(), True)
- data = {"name": "test", "session_duration": "123"}
- self.assertEqual(UserLoginStageForm(data).is_valid(), False)
diff --git a/authentik/stages/user_logout/forms.py b/authentik/stages/user_logout/forms.py
deleted file mode 100644
index 44d3cba4c..000000000
--- a/authentik/stages/user_logout/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""authentik flows logout forms"""
-from django import forms
-
-from authentik.stages.user_logout.models import UserLogoutStage
-
-
-class UserLogoutStageForm(forms.ModelForm):
- """Form to create/edit UserLogoutStage instances"""
-
- class Meta:
-
- model = UserLogoutStage
- fields = ["name"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/user_logout/models.py b/authentik/stages/user_logout/models.py
index 8f07a5236..ff92f545b 100644
--- a/authentik/stages/user_logout/models.py
+++ b/authentik/stages/user_logout/models.py
@@ -1,7 +1,6 @@
"""logout stage models"""
from typing import Type
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -25,10 +24,8 @@ class UserLogoutStage(Stage):
return UserLogoutStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.user_logout.forms import UserLogoutStageForm
-
- return UserLogoutStageForm
+ def component(self) -> str:
+ return "ak-stage-user-logout-form"
class Meta:
diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py
index 4923dff93..4eb464f6a 100644
--- a/authentik/stages/user_logout/tests.py
+++ b/authentik/stages/user_logout/tests.py
@@ -9,7 +9,6 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
-from authentik.stages.user_logout.forms import UserLogoutStageForm
from authentik.stages.user_logout.models import UserLogoutStage
@@ -51,8 +50,3 @@ class TestUserLogoutStage(TestCase):
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
-
- def test_form(self):
- """Test Form"""
- data = {"name": "test"}
- self.assertEqual(UserLogoutStageForm(data).is_valid(), True)
diff --git a/authentik/stages/user_write/forms.py b/authentik/stages/user_write/forms.py
deleted file mode 100644
index 685afc57d..000000000
--- a/authentik/stages/user_write/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""authentik flows write forms"""
-from django import forms
-
-from authentik.stages.user_write.models import UserWriteStage
-
-
-class UserWriteStageForm(forms.ModelForm):
- """Form to write/edit UserWriteStage instances"""
-
- class Meta:
-
- model = UserWriteStage
- fields = ["name"]
- widgets = {
- "name": forms.TextInput(),
- }
diff --git a/authentik/stages/user_write/models.py b/authentik/stages/user_write/models.py
index f4d8b63c4..eb37a89f6 100644
--- a/authentik/stages/user_write/models.py
+++ b/authentik/stages/user_write/models.py
@@ -1,7 +1,6 @@
"""write stage models"""
from typing import Type
-from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@@ -26,10 +25,8 @@ class UserWriteStage(Stage):
return UserWriteStageView
@property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.user_write.forms import UserWriteStageForm
-
- return UserWriteStageForm
+ def component(self) -> str:
+ return "ak-stage-user-write-form"
class Meta:
diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py
index bd83b2e72..00cce0ac8 100644
--- a/authentik/stages/user_write/tests.py
+++ b/authentik/stages/user_write/tests.py
@@ -15,7 +15,6 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
-from authentik.stages.user_write.forms import UserWriteStageForm
from authentik.stages.user_write.models import UserWriteStage
@@ -135,8 +134,3 @@ class TestUserWriteStage(TestCase):
"type": ChallengeTypes.NATIVE.value,
},
)
-
- def test_form(self):
- """Test Form"""
- data = {"name": "test"}
- self.assertEqual(UserWriteStageForm(data).is_valid(), True)
diff --git a/swagger.yaml b/swagger.yaml
index 209392d4a..283d604f2 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -20,6 +20,25 @@ securityDefinitions:
security:
- token: []
paths:
+ /admin/apps/:
+ get:
+ operationId: admin_apps_list
+ description: List current messages and pass into Serializer
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/App'
+ '403':
+ description: Authentication credentials were invalid, absent or insufficient.
+ schema:
+ $ref: '#/definitions/GenericError'
+ tags:
+ - admin
+ parameters: []
/admin/metrics/:
get:
operationId: admin_metrics_list
@@ -2055,42 +2074,7 @@ paths:
get:
operationId: core_users_me
description: Get information about current user
- parameters:
- - name: username
- in: query
- description: ''
- required: false
- type: string
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: is_active
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -2107,42 +2091,7 @@ paths:
get:
operationId: core_users_metrics
description: User metrics per 1h
- parameters:
- - name: username
- in: query
- description: ''
- required: false
- type: string
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: is_active
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -2290,6 +2239,16 @@ paths:
operationId: crypto_certificatekeypairs_list
description: CertificateKeyPair Viewset
parameters:
+ - name: name
+ in: query
+ description: ''
+ required: false
+ type: string
+ - name: has_key
+ in: query
+ description: ''
+ required: false
+ type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@@ -2670,6 +2629,25 @@ paths:
tags:
- events
parameters: []
+ /events/events/actions/:
+ get:
+ operationId: events_events_actions
+ description: Get all actions
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/TypeCreate'
+ '403':
+ description: Authentication credentials were invalid, absent or insufficient.
+ schema:
+ $ref: '#/definitions/GenericError'
+ tags:
+ - events
+ parameters: []
/events/events/top_per_user/:
get:
operationId: events_events_top_per_user
@@ -3850,47 +3828,7 @@ paths:
get:
operationId: flows_instances_cache_info
description: Info about cached flows
- parameters:
- - name: flow_uuid
- in: query
- description: ''
- required: false
- type: string
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: slug
- in: query
- description: ''
- required: false
- type: string
- - name: designation
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -4953,32 +4891,7 @@ paths:
get:
operationId: outposts_service_connections_all_types
description: Get all creatable service connection types
- parameters:
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -5564,37 +5477,7 @@ paths:
get:
operationId: policies_all_cache_info
description: Info about cached policies
- parameters:
- - name: bindings__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: promptstage__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -5611,37 +5494,7 @@ paths:
get:
operationId: policies_all_types
description: Get all creatable policy types
- parameters:
- - name: bindings__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: promptstage__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -7821,32 +7674,7 @@ paths:
get:
operationId: propertymappings_all_types
description: Get all creatable property-mapping types
- parameters:
- - name: managed__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -8617,32 +8445,7 @@ paths:
get:
operationId: providers_all_types
description: Get all creatable provider types
- parameters:
- - name: application__isnull
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -9217,6 +9020,42 @@ paths:
tags:
- providers
parameters: []
+ /providers/saml/import_metadata/:
+ post:
+ operationId: providers_saml_import_metadata
+ description: Create provider from SAML Metadata
+ parameters:
+ - name: name
+ in: formData
+ required: true
+ type: string
+ minLength: 1
+ - name: authorization_flow
+ in: formData
+ required: true
+ type: string
+ format: slug
+ pattern: ^[-a-zA-Z0-9_]+$
+ - name: file
+ in: formData
+ required: true
+ type: file
+ responses:
+ '204':
+ description: Successfully imported provider
+ '400':
+ description: Invalid input.
+ schema:
+ $ref: '#/definitions/ValidationError'
+ '403':
+ description: Authentication credentials were invalid, absent or insufficient.
+ schema:
+ $ref: '#/definitions/GenericError'
+ consumes:
+ - multipart/form-data
+ tags:
+ - providers
+ parameters: []
/providers/saml/{id}/:
get:
operationId: providers_saml_read
@@ -9438,27 +9277,7 @@ paths:
get:
operationId: sources_all_types
description: Get all creatable source types
- parameters:
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -9477,27 +9296,7 @@ paths:
get:
operationId: sources_all_user_settings
description: Get all sources the user can configure
- parameters:
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -9881,6 +9680,25 @@ paths:
tags:
- sources
parameters: []
+ /sources/oauth/source_types/:
+ get:
+ operationId: sources_oauth_source_types
+ description: Get all creatable source types
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/SourceType'
+ '403':
+ description: Authentication credentials were invalid, absent or insufficient.
+ schema:
+ $ref: '#/definitions/GenericError'
+ tags:
+ - sources
+ parameters: []
/sources/oauth/{slug}/:
get:
operationId: sources_oauth_read
@@ -10495,32 +10313,7 @@ paths:
get:
operationId: stages_all_types
description: Get all creatable stage types
- parameters:
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -10539,32 +10332,7 @@ paths:
get:
operationId: stages_all_user_settings
description: Get all stages the user can configure
- parameters:
- - name: name
- in: query
- description: ''
- required: false
- type: string
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: Page Index
- required: false
- type: integer
- - name: page_size
- in: query
- description: Page Size
- required: false
- type: integer
+ parameters: []
responses:
'200':
description: ''
@@ -14457,6 +14225,20 @@ definitions:
code:
description: Error code
type: string
+ App:
+ required:
+ - name
+ - label
+ type: object
+ properties:
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ label:
+ title: Label
+ type: string
+ minLength: 1
Coordinate:
type: object
properties:
@@ -14642,7 +14424,6 @@ definitions:
Provider:
required:
- name
- - application
- authorization_flow
type: object
properties:
@@ -14654,9 +14435,6 @@ definitions:
title: Name
type: string
minLength: 1
- application:
- title: Application
- type: string
authorization_flow:
title: Authorization flow
description: Flow used when authorizing this provider.
@@ -14668,8 +14446,8 @@ definitions:
type: string
format: uuid
uniqueItems: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
assigned_application_slug:
@@ -14834,6 +14612,15 @@ definitions:
type: string
format: uuid
readOnly: true
+ managed:
+ title: Managed by authentik
+ description: Objects which are managed by authentik. These objects are created
+ and updated automatically. This is flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the API,
+ but expect changes to be overwritten in a later update.
+ type: string
+ minLength: 1
+ x-nullable: true
identifier:
title: Identifier
type: string
@@ -15030,6 +14817,25 @@ definitions:
title: Expires
type: string
format: date-time
+ TypeCreate:
+ required:
+ - name
+ - description
+ - component
+ type: object
+ properties:
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ description:
+ title: Description
+ type: string
+ minLength: 1
+ component:
+ title: Component
+ type: string
+ minLength: 1
EventTopPerUser:
required:
- application
@@ -15318,8 +15124,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -15446,7 +15252,6 @@ definitions:
OAuth2Provider:
required:
- name
- - application
- authorization_flow
type: object
properties:
@@ -15458,9 +15263,6 @@ definitions:
title: Name
type: string
minLength: 1
- application:
- title: Application
- type: string
authorization_flow:
title: Authorization flow
description: Flow used when authorizing this provider.
@@ -15472,8 +15274,8 @@ definitions:
type: string
format: uuid
uniqueItems: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
assigned_application_slug:
@@ -15804,8 +15606,8 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -15816,25 +15618,6 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
- TypeCreate:
- required:
- - name
- - description
- - link
- type: object
- properties:
- name:
- title: Name
- type: string
- minLength: 1
- description:
- title: Description
- type: string
- minLength: 1
- link:
- title: Link
- type: string
- minLength: 1
ServiceConnectionState:
type: object
properties:
@@ -15867,8 +15650,8 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -15919,8 +15702,8 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -15953,8 +15736,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16060,8 +15843,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16106,8 +15889,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16222,8 +16005,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16259,8 +16042,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16304,8 +16087,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16372,8 +16155,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16413,8 +16196,8 @@ definitions:
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16496,6 +16279,15 @@ definitions:
type: string
format: uuid
readOnly: true
+ managed:
+ title: Managed by authentik
+ description: Objects which are managed by authentik. These objects are created
+ and updated automatically. This is flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the API,
+ but expect changes to be overwritten in a later update.
+ type: string
+ minLength: 1
+ x-nullable: true
name:
title: Name
type: string
@@ -16504,8 +16296,8 @@ definitions:
title: Expression
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -16540,6 +16332,15 @@ definitions:
type: string
format: uuid
readOnly: true
+ managed:
+ title: Managed by authentik
+ description: Objects which are managed by authentik. These objects are created
+ and updated automatically. This is flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the API,
+ but expect changes to be overwritten in a later update.
+ type: string
+ minLength: 1
+ x-nullable: true
name:
title: Name
type: string
@@ -16548,10 +16349,10 @@ definitions:
title: Expression
type: string
minLength: 1
- object_field:
- title: Object field
+ component:
+ title: Component
type: string
- minLength: 1
+ readOnly: true
verbose_name:
title: Verbose name
type: string
@@ -16560,11 +16361,15 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
+ object_field:
+ title: Object field
+ type: string
+ minLength: 1
SAMLPropertyMapping:
required:
- name
- - saml_name
- expression
+ - saml_name
type: object
properties:
pk:
@@ -16572,10 +16377,35 @@ definitions:
type: string
format: uuid
readOnly: true
+ managed:
+ title: Managed by authentik
+ description: Objects which are managed by authentik. These objects are created
+ and updated automatically. This is flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the API,
+ but expect changes to be overwritten in a later update.
+ type: string
+ minLength: 1
+ x-nullable: true
name:
title: Name
type: string
minLength: 1
+ expression:
+ title: Expression
+ type: string
+ minLength: 1
+ component:
+ title: Component
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
saml_name:
title: SAML Name
type: string
@@ -16584,10 +16414,39 @@ definitions:
title: Friendly name
type: string
x-nullable: true
+ ScopeMapping:
+ required:
+ - name
+ - expression
+ - scope_name
+ type: object
+ properties:
+ pk:
+ title: Pm uuid
+ type: string
+ format: uuid
+ readOnly: true
+ managed:
+ title: Managed by authentik
+ description: Objects which are managed by authentik. These objects are created
+ and updated automatically. This is flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the API,
+ but expect changes to be overwritten in a later update.
+ type: string
+ minLength: 1
+ x-nullable: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
expression:
title: Expression
type: string
minLength: 1
+ component:
+ title: Component
+ type: string
+ readOnly: true
verbose_name:
title: Verbose name
type: string
@@ -16596,22 +16455,6 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
- ScopeMapping:
- required:
- - name
- - scope_name
- - expression
- type: object
- properties:
- pk:
- title: Pm uuid
- type: string
- format: uuid
- readOnly: true
- name:
- title: Name
- type: string
- minLength: 1
scope_name:
title: Scope name
description: Scope used by the client
@@ -16622,18 +16465,6 @@ definitions:
description: Description shown to the user when consenting. If left empty,
the user won't be informed.
type: string
- expression:
- title: Expression
- type: string
- minLength: 1
- verbose_name:
- title: Verbose name
- type: string
- readOnly: true
- verbose_name_plural:
- title: Verbose name plural
- type: string
- readOnly: true
OAuth2ProviderSetupURLs:
type: object
properties:
@@ -16664,7 +16495,6 @@ definitions:
ProxyProvider:
required:
- name
- - application
- authorization_flow
- internal_host
- external_host
@@ -16678,9 +16508,6 @@ definitions:
title: Name
type: string
minLength: 1
- application:
- title: Application
- type: string
authorization_flow:
title: Authorization flow
description: Flow used when authorizing this provider.
@@ -16692,8 +16519,8 @@ definitions:
type: string
format: uuid
uniqueItems: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
assigned_application_slug:
@@ -16752,7 +16579,6 @@ definitions:
SAMLProvider:
required:
- name
- - application
- authorization_flow
- acs_url
type: object
@@ -16765,9 +16591,6 @@ definitions:
title: Name
type: string
minLength: 1
- application:
- title: Application
- type: string
authorization_flow:
title: Authorization flow
description: Flow used when authorizing this provider.
@@ -16779,8 +16602,8 @@ definitions:
type: string
format: uuid
uniqueItems: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
assigned_application_slug:
@@ -16870,6 +16693,14 @@ definitions:
type: string
format: uuid
x-nullable: true
+ sp_binding:
+ title: Service Provider Binding
+ description: This determines how authentik sends the response back to the
+ Service Provider.
+ type: string
+ enum:
+ - redirect
+ - post
SAMLMetadata:
type: object
properties:
@@ -16960,8 +16791,8 @@ definitions:
type: string
format: uuid
x-nullable: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17040,8 +16871,8 @@ definitions:
type: string
format: uuid
x-nullable: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17143,6 +16974,49 @@ definitions:
type: string
format: date-time
readOnly: true
+ SourceType:
+ description: Get source's type configuration
+ required:
+ - name
+ - slug
+ - urls_customizable
+ type: object
+ properties:
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ slug:
+ title: Slug
+ type: string
+ minLength: 1
+ urls_customizable:
+ title: Urls customizable
+ type: boolean
+ request_token_url:
+ title: Request token url
+ type: string
+ readOnly: true
+ minLength: 1
+ x-nullable: true
+ authorization_url:
+ title: Authorization url
+ type: string
+ readOnly: true
+ minLength: 1
+ x-nullable: true
+ access_token_url:
+ title: Access token url
+ type: string
+ readOnly: true
+ minLength: 1
+ x-nullable: true
+ profile_url:
+ title: Profile url
+ type: string
+ readOnly: true
+ minLength: 1
+ x-nullable: true
OAuthSource:
required:
- name
@@ -17188,8 +17062,8 @@ definitions:
type: string
format: uuid
x-nullable: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17247,6 +17121,8 @@ definitions:
title: Callback url
type: string
readOnly: true
+ type:
+ $ref: '#/definitions/SourceType'
UserOAuthSourceConnection:
required:
- user
@@ -17310,8 +17186,8 @@ definitions:
type: string
format: uuid
x-nullable: true
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17419,8 +17295,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17462,8 +17338,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17505,8 +17381,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17534,7 +17410,10 @@ definitions:
items:
title: Device classes
type: string
- minLength: 1
+ enum:
+ - static
+ - totp
+ - webauthn
configuration_stage:
title: Configuration stage
description: Stage used to configure Authenticator when user doesn't have
@@ -17557,8 +17436,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17596,8 +17475,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17636,8 +17515,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17678,8 +17557,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17708,8 +17587,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17738,8 +17617,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -17822,8 +17701,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18114,8 +17993,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18151,8 +18030,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18173,7 +18052,9 @@ definitions:
items:
title: Backends
type: string
- minLength: 1
+ enum:
+ - django.contrib.auth.backends.ModelBackend
+ - authentik.sources.ldap.auth.LDAPBackend
configure_flow:
title: Configure flow
description: Flow used by an authenticated user to configure this Stage. If
@@ -18257,8 +18138,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18299,8 +18180,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18329,8 +18210,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18365,8 +18246,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
@@ -18395,8 +18276,8 @@ definitions:
title: Name
type: string
minLength: 1
- object_type:
- title: Object type
+ component:
+ title: Component
type: string
readOnly: true
verbose_name:
diff --git a/tests/e2e/test_provider_saml.py b/tests/e2e/test_provider_saml.py
index df6dac35e..b05c5439c 100644
--- a/tests/e2e/test_provider_saml.py
+++ b/tests/e2e/test_provider_saml.py
@@ -56,9 +56,10 @@ class TestProviderSAML(SeleniumTestCase):
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"SP_METADATA_URL": (
self.url(
- "authentik_providers_saml:metadata",
- application_slug=provider.application.slug,
+ "authentik_api:samlprovider-metadata",
+ pk=provider.pk,
)
+ + "?download"
),
},
)
diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py
index b332f6784..3d7c3bca4 100644
--- a/tests/e2e/utils.py
+++ b/tests/e2e/utils.py
@@ -49,7 +49,6 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def setUp(self):
super().setUp()
self.wait_timeout = 60
- makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(30)
@@ -85,6 +84,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def tearDown(self):
if "TF_BUILD" in environ:
+ makedirs("selenium_screenshots/", exist_ok=True)
screenshot_file = (
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
)
diff --git a/web/rollup.config.js b/web/rollup.config.js
index 98034e240..3517791e9 100644
--- a/web/rollup.config.js
+++ b/web/rollup.config.js
@@ -12,6 +12,7 @@ const resources = [
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
+ { src: "node_modules/@patternfly/patternfly/patternfly.min.css.map", dest: "dist/" },
{ src: "src/authentik.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts
index f134d8272..64c30b16b 100644
--- a/web/src/api/Users.ts
+++ b/web/src/api/Users.ts
@@ -4,7 +4,7 @@ import { DEFAULT_CONFIG } from "./Config";
let _globalMePromise: Promise;
export function me(): Promise {
if (!_globalMePromise) {
- _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({}).catch((ex) => {
+ _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe().catch((ex) => {
if (ex.status === 401 || ex.status === 403) {
window.location.assign("/");
}
diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts
index bd6a9dc33..871491be5 100644
--- a/web/src/api/legacy.ts
+++ b/web/src/api/legacy.ts
@@ -1,31 +1,3 @@
-export class AdminURLManager {
-
- static policies(rest: string): string {
- return `/administration/policies/${rest}`;
- }
-
- static providers(rest: string): string {
- return `/administration/providers/${rest}`;
- }
-
- static propertyMappings(rest: string): string {
- return `/administration/property-mappings/${rest}`;
- }
-
- static outpostServiceConnections(rest: string): string {
- return `/administration/outpost_service_connections/${rest}`;
- }
-
- static stages(rest: string): string {
- return `/administration/stages/${rest}`;
- }
-
- static sources(rest: string): string {
- return `/administration/sources/${rest}`;
- }
-
-}
-
export class AppURLManager {
static sourceSAML(slug: string, rest: string): string {
@@ -34,9 +6,6 @@ export class AppURLManager {
static sourceOAuth(slug: string, action: string): string {
return `/source/oauth/${action}/${slug}/`;
}
- static providerSAML(rest: string): string {
- return `/application/saml/${rest}`;
- }
}
diff --git a/web/src/elements/CodeMirror.ts b/web/src/elements/CodeMirror.ts
index 219776dfb..3f8d397b3 100644
--- a/web/src/elements/CodeMirror.ts
+++ b/web/src/elements/CodeMirror.ts
@@ -1,5 +1,4 @@
-import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
-import PFForm from "@patternfly/patternfly/components/Form/form.css";
+import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import CodeMirror from "codemirror";
import "codemirror/addon/display/autorefresh";
@@ -25,12 +24,16 @@ export class CodeMirrorTextarea extends LitElement {
editor?: CodeMirror.EditorFromTextArea;
- private _value?: string;
+ _value?: string;
@property()
set value(v: string) {
if (v === null) return;
- this._value = v.toString();
+ if (this.editor) {
+ this.editor.setValue(v);
+ } else {
+ this._value = v;
+ }
}
get value(): string {
@@ -45,18 +48,18 @@ export class CodeMirrorTextarea extends LitElement {
}
private getInnerValue(): string {
- if (!this.shadowRoot) {
+ if (!this.editor) {
return "";
}
- const ta = this.shadowRoot?.querySelector("textarea");
- if (!ta) {
- return "";
- }
- return ta.value;
+ return this.editor.getValue();
}
static get styles(): CSSResult[] {
- return [PFForm, CodeMirrorStyle, CodeMirrorTheme];
+ return [CodeMirrorStyle, CodeMirrorTheme, css`
+ .CodeMirror-wrap pre {
+ word-break: break-word !important;
+ }
+ `];
}
firstUpdated(): void {
@@ -70,6 +73,8 @@ export class CodeMirrorTextarea extends LitElement {
lineNumbers: false,
readOnly: this.readOnly,
autoRefresh: true,
+ lineWrapping: true,
+ value: this._value
});
this.editor.on("blur", () => {
this.editor?.save();
@@ -77,6 +82,6 @@ export class CodeMirrorTextarea extends LitElement {
}
render(): TemplateResult {
- return html``;
+ return html``;
}
}
diff --git a/web/src/elements/charts/UserChart.ts b/web/src/elements/charts/UserChart.ts
index 944e0ab1c..877b1da66 100644
--- a/web/src/elements/charts/UserChart.ts
+++ b/web/src/elements/charts/UserChart.ts
@@ -8,7 +8,7 @@ import { DEFAULT_CONFIG } from "../../api/Config";
export class UserChart extends AKChart {
apiRequest(): Promise {
- return new CoreApi(DEFAULT_CONFIG).coreUsersMetrics({});
+ return new CoreApi(DEFAULT_CONFIG).coreUsersMetrics();
}
getDatasets(data: UserMetrics): Chart.ChartDataSets[] {
diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts
index f7b77ea29..ee0ebf3c7 100644
--- a/web/src/elements/forms/Form.ts
+++ b/web/src/elements/forms/Form.ts
@@ -12,7 +12,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import { MessageLevel } from "../messages/Message";
import { IronFormElement } from "@polymer/iron-form/iron-form";
-import { camelToSnake } from "../../utils";
+import { camelToSnake, convertToSlug } from "../../utils";
import { ValidationError } from "authentik-api/src";
export class APIError extends Error {
@@ -47,6 +47,28 @@ export class Form extends LitElement {
return this.successMessage;
}
+ updated(): void {
+ this.shadowRoot?.querySelectorAll("input[name=name]").forEach(nameInput => {
+ const form = nameInput.closest("form");
+ if (form === null) {
+ return;
+ }
+ const slugField = form.querySelector("input[name=slug]");
+ if (!slugField) {
+ return;
+ }
+ // Only attach handler if the slug is already equal to the name
+ // if not, they are probably completely different and shouldn't update
+ // each other
+ if (convertToSlug(nameInput.value) !== slugField.value) {
+ return;
+ }
+ nameInput.addEventListener("input", () => {
+ slugField.value = convertToSlug(nameInput.value);
+ });
+ });
+ }
+
/**
* Reset the inner iron-form
*/
@@ -162,11 +184,7 @@ export class Form extends LitElement {
`;
}
- render(): TemplateResult {
- const rect = this.getBoundingClientRect();
- if (rect.x + rect.y + rect.width + rect.height === 0) {
- return html``;
- }
+ renderVisible(): TemplateResult {
return html`