From 28ddeb124f4791b156763de9526af48c88ed5d24 Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 6 Mar 2023 19:39:08 +0100 Subject: [PATCH] providers: SCIM (#4835) * basic user sync Signed-off-by: Jens Langhammer * add group sync and some refactor Signed-off-by: Jens Langhammer * start API Signed-off-by: Jens Langhammer * allow null authorization flow Signed-off-by: Jens Langhammer * add UI Signed-off-by: Jens Langhammer * make task monitored Signed-off-by: Jens Langhammer * add missing dependency Signed-off-by: Jens Langhammer * make authorization_flow required for most providers via API Signed-off-by: Jens Langhammer * more UI Signed-off-by: Jens Langhammer * make task result better readable, exclude anonymous user Signed-off-by: Jens Langhammer * add task UI Signed-off-by: Jens Langhammer * add scheduled task for all sync Signed-off-by: Jens Langhammer * make scim errors more readable Signed-off-by: Jens Langhammer * add mappings, migrate to mappings Signed-off-by: Jens Langhammer * add mapping UI and more Signed-off-by: Jens Langhammer * add scim docs to web Signed-off-by: Jens Langhammer * start implementing membership Signed-off-by: Jens Langhammer * migrate signals to tasks Signed-off-by: Jens Langhammer * migrate fully to tasks Signed-off-by: Jens Langhammer * strip none keys, fix lint errors Signed-off-by: Jens Langhammer * fix things Signed-off-by: Jens Langhammer * start adding tests Signed-off-by: Jens Langhammer * fix saml Signed-off-by: Jens Langhammer * add scim schemas and validate against it Signed-off-by: Jens Langhammer * improve error handling Signed-off-by: Jens Langhammer * add group put support, add group tests Signed-off-by: Jens Langhammer * send correct application/scim+json headers Signed-off-by: Jens Langhammer * stop sync if no mappings are confiugred Signed-off-by: Jens Langhammer * add test for task sync Signed-off-by: Jens Langhammer * add membership tests Signed-off-by: Jens Langhammer * use decorator for tests Signed-off-by: Jens Langhammer * make tests better Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .vscode/settings.json | 3 +- Dockerfile | 2 +- authentik/api/v3/urls.py | 4 + authentik/core/api/providers.py | 3 + .../0025_alter_provider_authorization_flow.py | 25 + authentik/core/models.py | 3 +- authentik/events/middleware.py | 5 +- authentik/outposts/controllers/k8s/base.py | 2 +- .../providers/saml/processors/assertion.py | 6 +- authentik/providers/saml/settings.py | 6 - .../providers/saml/tests/test_metadata.py | 2 +- authentik/providers/saml/tests/test_schema.py | 4 +- authentik/providers/scim/__init__.py | 0 authentik/providers/scim/api/__init__.py | 0 .../providers/scim/api/property_mapping.py | 38 + authentik/providers/scim/api/providers.py | 60 + authentik/providers/scim/apps.py | 15 + authentik/providers/scim/clients/__init__.py | 2 + authentik/providers/scim/clients/base.py | 105 + .../providers/scim/clients/exceptions.py | 43 + authentik/providers/scim/clients/group.py | 166 ++ authentik/providers/scim/clients/schema.py | 17 + authentik/providers/scim/clients/user.py | 91 + .../providers/scim/migrations/0001_initial.py | 62 + .../scim/migrations/0002_scimuser.py | 37 + .../scim/migrations/0003_scimgroup.py | 36 + ...04_scimprovider_property_mappings_group.py | 23 + .../providers/scim/migrations/__init__.py | 0 authentik/providers/scim/models.py | 80 + authentik/providers/scim/settings.py | 12 + authentik/providers/scim/signals.py | 41 + authentik/providers/scim/tasks.py | 176 ++ authentik/providers/scim/tests/__init__.py | 0 authentik/providers/scim/tests/test_client.py | 78 + authentik/providers/scim/tests/test_group.py | 133 + .../providers/scim/tests/test_membership.py | 228 ++ authentik/providers/scim/tests/test_user.py | 250 ++ authentik/root/settings.py | 1 + authentik/sources/saml/tests/test_metadata.py | 2 +- blueprints/schema.json | 2 + blueprints/system/providers-scim.yaml | 58 + poetry.lock | 60 +- pyproject.toml | 1 + schema.yml | 2508 +++++++++++++++-- .../saml-schema-assertion-2.0.xsd | 0 {xml => schemas}/saml-schema-metadata-2.0.xsd | 0 {xml => schemas}/saml-schema-protocol-2.0.xsd | 0 schemas/scim-enterpriseUser.schema.json | 49 + schemas/scim-group.schema.json | 45 + schemas/scim-resourceType.schema.json | 72 + .../scim-serviceProviderConfig.schema.json | 139 + schemas/scim-user.schema.json | 372 +++ {xml => schemas}/xenc-schema.xsd | 0 {xml => schemas}/xml.xsd | 0 {xml => schemas}/xmldsig-core-schema.xsd | 0 .../admin/applications/ApplicationListPage.ts | 1 - .../PropertyMappingListPage.ts | 1 + .../PropertyMappingSAMLForm.ts | 2 +- .../PropertyMappingSCIMForm.ts | 69 + web/src/admin/providers/ProviderListPage.ts | 24 +- web/src/admin/providers/ProviderViewPage.ts | 5 + .../providers/saml/SAMLProviderImportForm.ts | 2 +- .../admin/providers/scim/SCIMProviderForm.ts | 178 ++ .../providers/scim/SCIMProviderViewPage.ts | 211 ++ web/src/elements/Markdown.ts | 13 +- website/docs/providers/scim/index.md | 40 + website/sidebars.js | 1 + 67 files changed, 5422 insertions(+), 192 deletions(-) create mode 100644 authentik/core/migrations/0025_alter_provider_authorization_flow.py delete mode 100644 authentik/providers/saml/settings.py create mode 100644 authentik/providers/scim/__init__.py create mode 100644 authentik/providers/scim/api/__init__.py create mode 100644 authentik/providers/scim/api/property_mapping.py create mode 100644 authentik/providers/scim/api/providers.py create mode 100644 authentik/providers/scim/apps.py create mode 100644 authentik/providers/scim/clients/__init__.py create mode 100644 authentik/providers/scim/clients/base.py create mode 100644 authentik/providers/scim/clients/exceptions.py create mode 100644 authentik/providers/scim/clients/group.py create mode 100644 authentik/providers/scim/clients/schema.py create mode 100644 authentik/providers/scim/clients/user.py create mode 100644 authentik/providers/scim/migrations/0001_initial.py create mode 100644 authentik/providers/scim/migrations/0002_scimuser.py create mode 100644 authentik/providers/scim/migrations/0003_scimgroup.py create mode 100644 authentik/providers/scim/migrations/0004_scimprovider_property_mappings_group.py create mode 100644 authentik/providers/scim/migrations/__init__.py create mode 100644 authentik/providers/scim/models.py create mode 100644 authentik/providers/scim/settings.py create mode 100644 authentik/providers/scim/signals.py create mode 100644 authentik/providers/scim/tasks.py create mode 100644 authentik/providers/scim/tests/__init__.py create mode 100644 authentik/providers/scim/tests/test_client.py create mode 100644 authentik/providers/scim/tests/test_group.py create mode 100644 authentik/providers/scim/tests/test_membership.py create mode 100644 authentik/providers/scim/tests/test_user.py create mode 100644 blueprints/system/providers-scim.yaml rename {xml => schemas}/saml-schema-assertion-2.0.xsd (100%) rename {xml => schemas}/saml-schema-metadata-2.0.xsd (100%) rename {xml => schemas}/saml-schema-protocol-2.0.xsd (100%) create mode 100644 schemas/scim-enterpriseUser.schema.json create mode 100644 schemas/scim-group.schema.json create mode 100644 schemas/scim-resourceType.schema.json create mode 100644 schemas/scim-serviceProviderConfig.schema.json create mode 100644 schemas/scim-user.schema.json rename {xml => schemas}/xenc-schema.xsd (100%) rename {xml => schemas}/xml.xsd (100%) rename {xml => schemas}/xmldsig-core-schema.xsd (100%) create mode 100644 web/src/admin/property-mappings/PropertyMappingSCIMForm.ts create mode 100644 web/src/admin/providers/scim/SCIMProviderForm.ts create mode 100644 web/src/admin/providers/scim/SCIMProviderViewPage.ts create mode 100644 website/docs/providers/scim/index.md diff --git a/.vscode/settings.json b/.vscode/settings.json index ba5b7df63..c4b17eb8f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,8 @@ "passwordless", "kubernetes", "sso", - "slo" + "slo", + "scim", ], "python.linting.pylintEnabled": true, "todo-tree.tree.showCountsInTree": true, diff --git a/Dockerfile b/Dockerfile index ab9cd5414..4dc74a3a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -96,7 +96,7 @@ RUN apt-get update && \ COPY ./authentik/ /authentik COPY ./pyproject.toml / -COPY ./xml /xml +COPY ./schemas /schemas COPY ./locale /locale COPY ./tests /tests COPY ./manage.py / diff --git a/authentik/api/v3/urls.py b/authentik/api/v3/urls.py index 61d9eaf27..35d7ba34b 100644 --- a/authentik/api/v3/urls.py +++ b/authentik/api/v3/urls.py @@ -58,6 +58,8 @@ from authentik.providers.oauth2.api.tokens import ( from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet from authentik.providers.saml.api.providers import SAMLProviderViewSet +from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet +from authentik.providers.scim.api.providers import SCIMProviderViewSet from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet from authentik.sources.oauth.api.source import OAuthSourceViewSet from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet @@ -163,6 +165,7 @@ router.register("providers/ldap", LDAPProviderViewSet) router.register("providers/proxy", ProxyProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/saml", SAMLProviderViewSet) +router.register("providers/scim", SCIMProviderViewSet) router.register("oauth2/authorization_codes", AuthorizationCodeViewSet) router.register("oauth2/refresh_tokens", RefreshTokenViewSet) @@ -173,6 +176,7 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet) router.register("propertymappings/scope", ScopeMappingViewSet) router.register("propertymappings/notification", NotificationWebhookMappingViewSet) +router.register("propertymappings/scim", SCIMMappingViewSet) router.register("authenticators/all", DeviceViewSet, basename="device") router.register("authenticators/duo", DuoDeviceViewSet) diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index 88e12a698..4d37d7ffa 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -44,6 +44,9 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer): "verbose_name_plural", "meta_model_name", ] + extra_kwargs = { + "authorization_flow": {"required": True, "allow_null": False}, + } class ProviderViewSet( diff --git a/authentik/core/migrations/0025_alter_provider_authorization_flow.py b/authentik/core/migrations/0025_alter_provider_authorization_flow.py new file mode 100644 index 000000000..73f5fe675 --- /dev/null +++ b/authentik/core/migrations/0025_alter_provider_authorization_flow.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.7 on 2023-03-02 21:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"), + ("authentik_core", "0024_source_icon"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="authorization_flow", + field=models.ForeignKey( + help_text="Flow used when authorizing this provider.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="provider_authorization", + to="authentik_flows.flow", + ), + ), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 0eb611a9d..00b95148c 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -248,6 +248,7 @@ class Provider(SerializerModel): authorization_flow = models.ForeignKey( "authentik_flows.Flow", on_delete=models.CASCADE, + null=True, help_text=_("Flow used when authorizing this provider."), related_name="provider_authorization", ) @@ -630,7 +631,7 @@ class PropertyMapping(SerializerModel, ManagedModel): try: return evaluator.evaluate(self.expression) except Exception as exc: - raise PropertyMappingExpressionException(str(exc)) from exc + raise PropertyMappingExpressionException(exc) from exc def __str__(self): return f"Property Mapping {self.name}" diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py index 7c655b459..0215f8611 100644 --- a/authentik/events/middleware.py +++ b/authentik/events/middleware.py @@ -29,6 +29,7 @@ from authentik.lib.utils.errors import exception_to_string from authentik.outposts.models import OutpostServiceConnection from authentik.policies.models import Policy, PolicyBindingModel from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken +from authentik.providers.scim.models import SCIMGroup, SCIMUser IGNORED_MODELS = ( Event, @@ -49,6 +50,8 @@ IGNORED_MODELS = ( AuthorizationCode, AccessToken, RefreshToken, + SCIMUser, + SCIMGroup, ) @@ -188,7 +191,7 @@ class AuditMiddleware: user: User, request: HttpRequest, sender, instance: Model, action: str, **_ ): """Signal handler for all object's m2m_changed""" - if action not in ["pre_add", "pre_remove"]: + if action not in ["pre_add", "pre_remove", "post_clear"]: return if not should_log_m2m(instance): return diff --git a/authentik/outposts/controllers/k8s/base.py b/authentik/outposts/controllers/k8s/base.py index 607f0821b..58f91eb67 100644 --- a/authentik/outposts/controllers/k8s/base.py +++ b/authentik/outposts/controllers/k8s/base.py @@ -16,7 +16,6 @@ from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpda if TYPE_CHECKING: from authentik.outposts.controllers.kubernetes import KubernetesController -# pylint: disable=invalid-name T = TypeVar("T", V1Pod, V1Deployment) @@ -56,6 +55,7 @@ class KubernetesObjectReconciler(Generic[T]): } ).lower() + # pylint: disable=invalid-name def up(self): """Create object if it doesn't exist, update if needed or recreate if needed.""" current = None diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py index d89b7f7f7..773408910 100644 --- a/authentik/providers/saml/processors/assertion.py +++ b/authentik/providers/saml/processors/assertion.py @@ -73,9 +73,9 @@ class AssertionProcessor: # https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement") user = self.http_request.user - for mapping in self.provider.property_mappings.all().select_subclasses(): - if not isinstance(mapping, SAMLPropertyMapping): - continue + for mapping in SAMLPropertyMapping.objects.filter(provider=self.provider).order_by( + "saml_name" + ): try: mapping: SAMLPropertyMapping value = mapping.evaluate( diff --git a/authentik/providers/saml/settings.py b/authentik/providers/saml/settings.py deleted file mode 100644 index 5477bb568..000000000 --- a/authentik/providers/saml/settings.py +++ /dev/null @@ -1,6 +0,0 @@ -"""saml provider settings""" - -AUTHENTIK_PROVIDERS_SAML_PROCESSORS = [ - "authentik.providers.saml.processors.generic", - "authentik.providers.saml.processors.salesforce", -] diff --git a/authentik/providers/saml/tests/test_metadata.py b/authentik/providers/saml/tests/test_metadata.py index b891736cc..07fc6ad0e 100644 --- a/authentik/providers/saml/tests/test_metadata.py +++ b/authentik/providers/saml/tests/test_metadata.py @@ -59,7 +59,7 @@ class TestServiceProviderMetadataParser(TestCase): request = self.factory.get("/") metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor()) - schema = etree.XMLSchema(etree.parse("xml/saml-schema-metadata-2.0.xsd")) # nosec + schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec self.assertTrue(schema.validate(metadata)) def test_simple(self): diff --git a/authentik/providers/saml/tests/test_schema.py b/authentik/providers/saml/tests/test_schema.py index 63827b8af..a0f41c16e 100644 --- a/authentik/providers/saml/tests/test_schema.py +++ b/authentik/providers/saml/tests/test_schema.py @@ -46,7 +46,7 @@ class TestSchema(TestCase): metadata = lxml_from_string(request) - schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd")) # nosec + schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec self.assertTrue(schema.validate(metadata)) def test_response_schema(self): @@ -67,5 +67,5 @@ class TestSchema(TestCase): metadata = lxml_from_string(response) - schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd")) + schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) self.assertTrue(schema.validate(metadata)) diff --git a/authentik/providers/scim/__init__.py b/authentik/providers/scim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/scim/api/__init__.py b/authentik/providers/scim/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/scim/api/property_mapping.py b/authentik/providers/scim/api/property_mapping.py new file mode 100644 index 000000000..80c909329 --- /dev/null +++ b/authentik/providers/scim/api/property_mapping.py @@ -0,0 +1,38 @@ +"""scim Property mappings API Views""" +from django_filters.filters import AllValuesMultipleFilter +from django_filters.filterset import FilterSet +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.providers.scim.models import SCIMMapping + + +class SCIMMappingSerializer(PropertyMappingSerializer): + """SCIMMapping Serializer""" + + class Meta: + model = SCIMMapping + fields = PropertyMappingSerializer.Meta.fields + + +class SCIMMappingFilter(FilterSet): + """Filter for SCIMMapping""" + + managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed")) + + class Meta: + model = SCIMMapping + fields = "__all__" + + +class SCIMMappingViewSet(UsedByMixin, ModelViewSet): + """SCIMMapping Viewset""" + + queryset = SCIMMapping.objects.all() + serializer_class = SCIMMappingSerializer + filterset_class = SCIMMappingFilter + search_fields = ["name"] + ordering = ["name"] diff --git a/authentik/providers/scim/api/providers.py b/authentik/providers/scim/api/providers.py new file mode 100644 index 000000000..e9cf73d2f --- /dev/null +++ b/authentik/providers/scim/api/providers.py @@ -0,0 +1,60 @@ +"""SCIM Provider API Views""" +from django.utils.text import slugify +from drf_spectacular.utils import OpenApiResponse, extend_schema +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from authentik.admin.api.tasks import TaskSerializer +from authentik.core.api.providers import ProviderSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.events.monitored_tasks import TaskInfo +from authentik.providers.scim.models import SCIMProvider + + +class SCIMProviderSerializer(ProviderSerializer): + """SCIMProvider Serializer""" + + class Meta: + model = SCIMProvider + fields = [ + "pk", + "name", + "property_mappings", + "property_mappings_group", + "component", + "assigned_application_slug", + "assigned_application_name", + "verbose_name", + "verbose_name_plural", + "meta_model_name", + "url", + "token", + ] + extra_kwargs = {} + + +class SCIMProviderViewSet(UsedByMixin, ModelViewSet): + """SCIMProvider Viewset""" + + queryset = SCIMProvider.objects.all() + serializer_class = SCIMProviderSerializer + filterset_fields = ["name", "authorization_flow", "url", "token"] + search_fields = ["name", "url"] + ordering = ["name", "url"] + + @extend_schema( + responses={ + 200: TaskSerializer(), + 404: OpenApiResponse(description="Task not found"), + } + ) + @action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[]) + def sync_status(self, request: Request, pk: int) -> Response: + """Get provider's sync status""" + provider = self.get_object() + task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}") + if not task: + return Response(status=404) + return Response(TaskSerializer(task).data) diff --git a/authentik/providers/scim/apps.py b/authentik/providers/scim/apps.py new file mode 100644 index 000000000..37c6478c4 --- /dev/null +++ b/authentik/providers/scim/apps.py @@ -0,0 +1,15 @@ +"""authentik SCIM Provider app config""" +from authentik.blueprints.apps import ManagedAppConfig + + +class AuthentikProviderSCIMConfig(ManagedAppConfig): + """authentik SCIM Provider app config""" + + name = "authentik.providers.scim" + label = "authentik_providers_scim" + verbose_name = "authentik Providers.SCIM" + default = True + + def reconcile_load_signals(self): + """Load signals""" + self.import_module("authentik.providers.scim.signals") diff --git a/authentik/providers/scim/clients/__init__.py b/authentik/providers/scim/clients/__init__.py new file mode 100644 index 000000000..a57539f67 --- /dev/null +++ b/authentik/providers/scim/clients/__init__.py @@ -0,0 +1,2 @@ +"""SCIM constants""" +PAGE_SIZE = 100 diff --git a/authentik/providers/scim/clients/base.py b/authentik/providers/scim/clients/base.py new file mode 100644 index 000000000..f6960b947 --- /dev/null +++ b/authentik/providers/scim/clients/base.py @@ -0,0 +1,105 @@ +"""SCIM Client""" +from typing import Generic, TypeVar + +from pydantic import ValidationError +from pydanticscim.service_provider import ( + Bulk, + ChangePassword, + Filter, + Patch, + ServiceProviderConfiguration, + Sort, +) +from requests import RequestException, Session +from structlog.stdlib import get_logger + +from authentik.lib.utils.http import get_http_session +from authentik.providers.scim.clients.exceptions import SCIMRequestException +from authentik.providers.scim.models import SCIMProvider + +T = TypeVar("T") +# pylint: disable=invalid-name +SchemaType = TypeVar("SchemaType") + + +def default_service_provider_config() -> ServiceProviderConfiguration: + """Fallback service provider configuration""" + return ServiceProviderConfiguration( + patch=Patch(supported=False), + bulk=Bulk(supported=False), + filter=Filter(supported=False), + changePassword=ChangePassword(supported=False), + sort=Sort(supported=False), + authenticationSchemes=[], + ) + + +class SCIMClient(Generic[T, SchemaType]): + """SCIM Client""" + + base_url: str + token: str + provider: SCIMProvider + + _session: Session + _config: ServiceProviderConfiguration + + def __init__(self, provider: SCIMProvider): + self._session = get_http_session() + self.provider = provider + # Remove trailing slashes as we assume the URL doesn't have any + base_url = provider.url + if base_url.endswith("/"): + base_url = base_url[:-1] + self.base_url = base_url + self.token = provider.token + self.logger = get_logger().bind(provider=provider.name) + self._config = self.get_service_provider_config() + + def _request(self, method: str, path: str, **kwargs) -> dict: + """Wrapper to send a request to the full URL""" + try: + response = self._session.request( + method, + f"{self.base_url}{path}", + **kwargs, + headers={ + "Authorization": f"Bearer {self.token}", + "Accept": "application/scim+json", + "Content-Type": "application/scim+json", + }, + ) + except RequestException as exc: + raise SCIMRequestException(None) from exc + self.logger.debug("scim request", path=path, method=method, **kwargs) + if response.status_code >= 400: + self.logger.warning( + "Failed to send SCIM request", path=path, method=method, response=response.text + ) + raise SCIMRequestException(response) + if response.status_code == 204: + return {} + return response.json() + + def get_service_provider_config(self): + """Get Service provider config""" + default_config = default_service_provider_config() + try: + return ServiceProviderConfiguration.parse_obj( + self._request("GET", "/ServiceProviderConfig") + ) + except ValidationError as exc: + self.logger.warning("ServiceProviderConfig invalid", exc=exc) + return default_config + + def write(self, obj: T): + """Write object to SCIM""" + raise NotImplementedError() + + def delete(self, obj: T): + """Delete object from SCIM""" + raise NotImplementedError() + + def to_scim(self, obj: T) -> SchemaType: + """Convert object to scim""" + raise NotImplementedError() diff --git a/authentik/providers/scim/clients/exceptions.py b/authentik/providers/scim/clients/exceptions.py new file mode 100644 index 000000000..20c3a5f70 --- /dev/null +++ b/authentik/providers/scim/clients/exceptions.py @@ -0,0 +1,43 @@ +"""SCIM Client exceptions""" +from typing import Optional + +from pydantic import ValidationError +from pydanticscim.responses import SCIMError +from requests import Response + +from authentik.lib.sentry import SentryIgnoredException + + +class StopSync(SentryIgnoredException): + """Exception raised when a configuration error should stop the sync process""" + + def __init__(self, exc: Exception, obj: object, mapping: Optional[object] = None) -> None: + self.exc = exc + self.obj = obj + self.mapping = mapping + + def __str__(self) -> str: + msg = f"Error {str(self.exc)}, caused by {self.obj}" + + if self.mapping: + msg += f" (mapping {self.mapping})" + return msg + + +class SCIMRequestException(SentryIgnoredException): + """Exception raised when an SCIM request fails""" + + _response: Optional[Response] + + def __init__(self, response: Optional[Response] = None) -> None: + self._response = response + + def __str__(self) -> str: + if not self._response: + return super().__str__() + try: + error = SCIMError.parse_raw(self._response.text) + return error.detail + except ValidationError: + pass + return super().__str__() diff --git a/authentik/providers/scim/clients/group.py b/authentik/providers/scim/clients/group.py new file mode 100644 index 000000000..ec84614c8 --- /dev/null +++ b/authentik/providers/scim/clients/group.py @@ -0,0 +1,166 @@ +"""Group client""" +from deepmerge import always_merger +from pydantic import ValidationError +from pydanticscim.group import GroupMember +from pydanticscim.responses import PatchOp, PatchOperation, PatchRequest + +from authentik.core.exceptions import PropertyMappingExpressionException +from authentik.core.models import Group +from authentik.events.models import Event, EventAction +from authentik.lib.utils.errors import exception_to_string +from authentik.policies.utils import delete_none_keys +from authentik.providers.scim.clients.base import SCIMClient +from authentik.providers.scim.clients.exceptions import StopSync +from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema +from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMUser + + +class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]): + """SCIM client for groups""" + + def write(self, obj: Group): + """Write a group""" + scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first() + if not scim_group: + return self._create(obj) + scim_group = self.to_scim(obj) + scim_group.id = scim_group.id + return self._request( + "PUT", + f"/Groups/{scim_group.id}", + data=scim_group.json( + exclude_unset=True, + ), + ) + + def delete(self, obj: Group): + """Delete group""" + scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first() + if not scim_group: + self.logger.debug("Group does not exist in SCIM, skipping") + return None + response = self._request("DELETE", f"/Groups/{scim_group.id}") + scim_group.delete() + return response + + def to_scim(self, obj: Group) -> SCIMGroupSchema: + """Convert authentik user into SCIM""" + raw_scim_group = {} + for mapping in ( + self.provider.property_mappings_group.all().order_by("name").select_subclasses() + ): + if not isinstance(mapping, SCIMMapping): + continue + try: + mapping: SCIMMapping + value = mapping.evaluate( + user=None, + request=None, + group=obj, + provider=self.provider, + ) + if value is None: + continue + always_merger.merge(raw_scim_group, value) + except (PropertyMappingExpressionException, ValueError) as exc: + # Value error can be raised when assigning invalid data to an attribute + Event.new( + EventAction.CONFIGURATION_ERROR, + message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", + mapping=mapping, + ).save() + raise StopSync(exc, obj, mapping) from exc + if not raw_scim_group: + raise StopSync(ValueError("No group mappings configured"), obj) + try: + scim_group = SCIMGroupSchema.parse_obj(delete_none_keys(raw_scim_group)) + except ValidationError as exc: + raise StopSync(exc, obj) from exc + scim_group.externalId = str(obj.pk) + + users = list(obj.users.order_by("id").values_list("id", flat=True)) + connections = SCIMUser.objects.filter(provider=self.provider, user__pk__in=users) + for user in connections: + scim_group.members.append( + GroupMember( + value=user.id, + ) + ) + return scim_group + + def _create(self, group: Group): + """Create group from scratch and create a connection object""" + scim_group = self.to_scim(group) + response = self._request( + "POST", + "/Groups", + data=scim_group.json( + exclude_unset=True, + ), + ) + SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"]) + + def _patch( + self, + group_id: str, + *ops: PatchOperation, + ): + req = PatchRequest(Operations=ops) + self._request("PATCH", f"/Groups/{group_id}", data=req.json(exclude_unset=True)) + + def update_group(self, group: Group, action: PatchOp, users_set: set[int]): + """Update a group, either using PUT to replace it or PATCH if supported""" + if self._config.patch.supported: + if action == PatchOp.add: + return self._patch_add_users(group, users_set) + if action == PatchOp.remove: + return self._patch_remove_users(group, users_set) + return self.write(group) + + def _patch_add_users(self, group: Group, users_set: set[int]): + """Add users in users_set to group""" + if len(users_set) < 1: + return + scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first() + if not scim_group: + self.logger.warning( + "could not sync group membership, group does not exist", group=group + ) + return + user_ids = list( + SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list( + "id", flat=True + ) + ) + self._patch( + scim_group.id, + PatchOperation( + op=PatchOp.add, + path="members", + value=[{"value": x} for x in user_ids], + ), + ) + + def _patch_remove_users(self, group: Group, users_set: set[int]): + """Remove users in users_set from group""" + if len(users_set) < 1: + return + scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first() + if not scim_group: + self.logger.warning( + "could not sync group membership, group does not exist", group=group + ) + return + user_ids = list( + SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list( + "id", flat=True + ) + ) + self._patch( + scim_group.id, + PatchOperation( + op=PatchOp.remove, + path="members", + value=[{"value": x} for x in user_ids], + ), + ) diff --git a/authentik/providers/scim/clients/schema.py b/authentik/providers/scim/clients/schema.py new file mode 100644 index 000000000..f4c94157a --- /dev/null +++ b/authentik/providers/scim/clients/schema.py @@ -0,0 +1,17 @@ +"""Custom SCIM schemas""" +from typing import Optional + +from pydanticscim.group import Group as SCIMGroupSchema +from pydanticscim.user import User as SCIMUserSchema + + +class User(SCIMUserSchema): + """Modified User schema with added externalId field""" + + externalId: Optional[str] = None + + +class Group(SCIMGroupSchema): + """Modified Group schema with added externalId field""" + + externalId: Optional[str] = None diff --git a/authentik/providers/scim/clients/user.py b/authentik/providers/scim/clients/user.py new file mode 100644 index 000000000..6fe33dadd --- /dev/null +++ b/authentik/providers/scim/clients/user.py @@ -0,0 +1,91 @@ +"""User client""" +from deepmerge import always_merger +from pydantic import ValidationError + +from authentik.core.exceptions import PropertyMappingExpressionException +from authentik.core.models import User +from authentik.events.models import Event, EventAction +from authentik.lib.utils.errors import exception_to_string +from authentik.policies.utils import delete_none_keys +from authentik.providers.scim.clients.base import SCIMClient +from authentik.providers.scim.clients.exceptions import StopSync +from authentik.providers.scim.clients.schema import User as SCIMUserSchema +from authentik.providers.scim.models import SCIMMapping, SCIMUser + + +class SCIMUserClient(SCIMClient[User, SCIMUserSchema]): + """SCIM client for users""" + + def write(self, obj: User): + """Write a user""" + scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first() + if not scim_user: + return self._create(obj) + return self._update(obj, scim_user) + + def delete(self, obj: User): + """Delete user""" + scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first() + if not scim_user: + self.logger.debug("User does not exist in SCIM, skipping") + return None + response = self._request("DELETE", f"/Users/{scim_user.id}") + scim_user.delete() + return response + + def to_scim(self, obj: User) -> SCIMUserSchema: + """Convert authentik user into SCIM""" + raw_scim_user = {} + for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses(): + if not isinstance(mapping, SCIMMapping): + continue + try: + mapping: SCIMMapping + value = mapping.evaluate( + user=obj, + request=None, + provider=self.provider, + ) + if value is None: + continue + always_merger.merge(raw_scim_user, value) + except (PropertyMappingExpressionException, ValueError) as exc: + # Value error can be raised when assigning invalid data to an attribute + Event.new( + EventAction.CONFIGURATION_ERROR, + message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", + mapping=mapping, + ).save() + raise StopSync(exc, obj, mapping) from exc + if not raw_scim_user: + raise StopSync(ValueError("No user mappings configured"), obj) + try: + scim_user = SCIMUserSchema.parse_obj(delete_none_keys(raw_scim_user)) + except ValidationError as exc: + raise StopSync(exc, obj) from exc + scim_user.externalId = str(obj.uid) + return scim_user + + def _create(self, user: User): + """Create user from scratch and create a connection object""" + scim_user = self.to_scim(user) + response = self._request( + "POST", + "/Users", + data=scim_user.json( + exclude_unset=True, + ), + ) + SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"]) + + def _update(self, user: User, connection: SCIMUser): + """Update existing user""" + scim_user = self.to_scim(user) + scim_user.id = connection.id + self._request( + "PUT", + f"/Users/{connection.id}", + data=scim_user.json( + exclude_unset=True, + ), + ) diff --git a/authentik/providers/scim/migrations/0001_initial.py b/authentik/providers/scim/migrations/0001_initial.py new file mode 100644 index 000000000..4195d3787 --- /dev/null +++ b/authentik/providers/scim/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 4.1.7 on 2023-03-02 13:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("authentik_core", "0024_source_icon"), + ] + + operations = [ + migrations.CreateModel( + name="SCIMMapping", + fields=[ + ( + "propertymapping_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.propertymapping", + ), + ), + ], + options={ + "verbose_name": "SCIM Mapping", + "verbose_name_plural": "SCIM Mappings", + }, + bases=("authentik_core.propertymapping",), + ), + migrations.CreateModel( + name="SCIMProvider", + fields=[ + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.provider", + ), + ), + ( + "url", + models.TextField(help_text="Base URL to SCIM requests, usually ends in /v2"), + ), + ("token", models.TextField(help_text="Authentication token")), + ], + options={ + "verbose_name": "SCIM Provider", + "verbose_name_plural": "SCIM Providers", + }, + bases=("authentik_core.provider",), + ), + ] diff --git a/authentik/providers/scim/migrations/0002_scimuser.py b/authentik/providers/scim/migrations/0002_scimuser.py new file mode 100644 index 000000000..684df6b8e --- /dev/null +++ b/authentik/providers/scim/migrations/0002_scimuser.py @@ -0,0 +1,37 @@ +# Generated by Django 4.1.7 on 2023-03-02 15:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("authentik_providers_scim", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="SCIMUser", + fields=[ + ("id", models.TextField(primary_key=True, serialize=False)), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_scim.scimprovider", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "unique_together": {("id", "user", "provider")}, + }, + ), + ] diff --git a/authentik/providers/scim/migrations/0003_scimgroup.py b/authentik/providers/scim/migrations/0003_scimgroup.py new file mode 100644 index 000000000..1737cacbf --- /dev/null +++ b/authentik/providers/scim/migrations/0003_scimgroup.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.7 on 2023-03-02 15:55 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_core", "0024_source_icon"), + ("authentik_providers_scim", "0002_scimuser"), + ] + + operations = [ + migrations.CreateModel( + name="SCIMGroup", + fields=[ + ("id", models.TextField(primary_key=True, serialize=False)), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group" + ), + ), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_scim.scimprovider", + ), + ), + ], + options={ + "unique_together": {("id", "group", "provider")}, + }, + ), + ] diff --git a/authentik/providers/scim/migrations/0004_scimprovider_property_mappings_group.py b/authentik/providers/scim/migrations/0004_scimprovider_property_mappings_group.py new file mode 100644 index 000000000..0987642de --- /dev/null +++ b/authentik/providers/scim/migrations/0004_scimprovider_property_mappings_group.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-03-03 14:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_core", "0025_alter_provider_authorization_flow"), + ("authentik_providers_scim", "0003_scimgroup"), + ] + + operations = [ + migrations.AddField( + model_name="scimprovider", + name="property_mappings_group", + field=models.ManyToManyField( + blank=True, + default=None, + help_text="Property mappings used for group creation/updating.", + to="authentik_core.propertymapping", + ), + ), + ] diff --git a/authentik/providers/scim/migrations/__init__.py b/authentik/providers/scim/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/scim/models.py b/authentik/providers/scim/models.py new file mode 100644 index 000000000..774b08afd --- /dev/null +++ b/authentik/providers/scim/models.py @@ -0,0 +1,80 @@ +"""SCIM Provider models""" +from django.db import models +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer + +from authentik.core.models import Group, PropertyMapping, Provider, User + + +class SCIMProvider(Provider): + """SCIM 2.0 provider to create users and groups in external applications""" + + url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2")) + token = models.TextField(help_text=_("Authentication token")) + + property_mappings_group = models.ManyToManyField( + PropertyMapping, + default=None, + blank=True, + help_text=_("Property mappings used for group creation/updating."), + ) + + @property + def component(self) -> str: + return "ak-provider-scim-form" + + @property + def serializer(self) -> type[Serializer]: + from authentik.providers.scim.api.providers import SCIMProviderSerializer + + return SCIMProviderSerializer + + def __str__(self): + return f"SCIM Provider {self.name}" + + class Meta: + verbose_name = _("SCIM Provider") + verbose_name_plural = _("SCIM Providers") + + +class SCIMMapping(PropertyMapping): + """Map authentik data to outgoing SCIM requests""" + + @property + def component(self) -> str: + return "ak-property-mapping-scim-form" + + @property + def serializer(self) -> type[Serializer]: + from authentik.providers.scim.api.property_mapping import SCIMMappingSerializer + + return SCIMMappingSerializer + + def __str__(self): + return f"SCIM Mapping {self.name}" + + class Meta: + verbose_name = _("SCIM Mapping") + verbose_name_plural = _("SCIM Mappings") + + +class SCIMUser(models.Model): + """Mapping of a user and provider to a SCIM user ID""" + + id = models.TextField(primary_key=True) + user = models.ForeignKey(User, on_delete=models.CASCADE) + provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) + + class Meta: + unique_together = (("id", "user", "provider"),) + + +class SCIMGroup(models.Model): + """Mapping of a group and provider to a SCIM user ID""" + + id = models.TextField(primary_key=True) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) + + class Meta: + unique_together = (("id", "group", "provider"),) diff --git a/authentik/providers/scim/settings.py b/authentik/providers/scim/settings.py new file mode 100644 index 000000000..02b2e6e6e --- /dev/null +++ b/authentik/providers/scim/settings.py @@ -0,0 +1,12 @@ +"""SCIM task Settings""" +from celery.schedules import crontab + +from authentik.lib.utils.time import fqdn_rand + +CELERY_BEAT_SCHEDULE = { + "providers_scim_sync": { + "task": "authentik.providers.scim.tasks.scim_sync_all", + "schedule": crontab(minute=fqdn_rand("scim_sync_all"), hour="*"), + "options": {"queue": "authentik_scheduled"}, + }, +} diff --git a/authentik/providers/scim/signals.py b/authentik/providers/scim/signals.py new file mode 100644 index 000000000..4450a72a6 --- /dev/null +++ b/authentik/providers/scim/signals.py @@ -0,0 +1,41 @@ +"""SCIM provider signals""" +from django.db.models import Model +from django.db.models.signals import m2m_changed, post_save, pre_delete +from django.dispatch import receiver +from pydanticscim.responses import PatchOp +from structlog.stdlib import get_logger + +from authentik.core.models import Group, User +from authentik.lib.utils.reflection import class_to_path +from authentik.providers.scim.models import SCIMProvider +from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync + +LOGGER = get_logger() + + +@receiver(post_save, sender=SCIMProvider) +def post_save_provider(sender: type[Model], instance, created: bool, **_): + """Trigger sync when SCIM provider is saved""" + scim_sync.delay(instance.pk) + + +@receiver(post_save, sender=User) +@receiver(post_save, sender=Group) +def post_save_scim(sender: type[Model], instance: User | Group, created: bool, **_): + """Post save handler""" + scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.add.value) + + +@receiver(pre_delete, sender=User) +@receiver(pre_delete, sender=Group) +def pre_delete_scim(sender: type[Model], instance: User | Group, **_): + """Pre-delete handler""" + scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.remove.value) + + +@receiver(m2m_changed, sender=User.ak_groups.through) +def m2m_changed_scim(sender: type[Model], instance, action: str, pk_set: set, **kwargs): + """Sync group membership""" + if action not in ["post_add", "post_remove"]: + return + scim_signal_m2m.delay(str(instance.pk), action, list(pk_set)) diff --git a/authentik/providers/scim/tasks.py b/authentik/providers/scim/tasks.py new file mode 100644 index 000000000..03af6eebe --- /dev/null +++ b/authentik/providers/scim/tasks.py @@ -0,0 +1,176 @@ +"""SCIM Provider tasks""" +from typing import Any + +from celery.result import allow_join_result +from django.core.paginator import Paginator +from django.db.models import Model +from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ +from guardian.shortcuts import get_anonymous_user +from pydanticscim.responses import PatchOp +from structlog.stdlib import get_logger + +from authentik.core.models import Group, User +from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus +from authentik.lib.utils.reflection import path_to_class +from authentik.providers.scim.clients import PAGE_SIZE +from authentik.providers.scim.clients.base import SCIMClient +from authentik.providers.scim.clients.exceptions import SCIMRequestException, StopSync +from authentik.providers.scim.clients.group import SCIMGroupClient +from authentik.providers.scim.clients.user import SCIMUserClient +from authentik.providers.scim.models import SCIMProvider +from authentik.root.celery import CELERY_APP + +LOGGER = get_logger(__name__) + + +def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient: + """Get SCIM client for model""" + if isinstance(model, User): + return SCIMUserClient(provider) + if isinstance(model, Group): + return SCIMGroupClient(provider) + raise ValueError(f"Invalid model {model}") + + +@CELERY_APP.task() +def scim_sync_all(): + """Run sync for all providers""" + for provider in SCIMProvider.objects.all(): + scim_sync.delay(provider.pk) + + +@CELERY_APP.task(bind=True, base=MonitoredTask) +def scim_sync(self: MonitoredTask, provider_pk: int) -> None: + """Run SCIM full sync for provider""" + provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first() + if not provider: + return + self.set_uid(slugify(provider.name)) + result = TaskResult(TaskResultStatus.SUCCESSFUL, []) + result.messages.append(_("Starting full SCIM sync")) + # TODO: Filtering + LOGGER.debug("Starting SCIM sync") + users_paginator = Paginator( + User.objects.all().exclude(pk=get_anonymous_user().pk).order_by("pk"), PAGE_SIZE + ) + groups_paginator = Paginator(Group.objects.all().order_by("pk"), PAGE_SIZE) + with allow_join_result(): + try: + for page in users_paginator.page_range: + result.messages.append(_("Syncing page %(page)d of users" % {"page": page})) + for msg in scim_sync_users.delay(page, provider_pk).get(): + result.messages.append(msg) + for page in groups_paginator.page_range: + result.messages.append(_("Syncing page %(page)d of groups" % {"page": page})) + for msg in scim_sync_group.delay(page, provider_pk).get(): + result.messages.append(msg) + except StopSync as exc: + self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) + return + self.set_status(result) + + +@CELERY_APP.task() +def scim_sync_users(page: int, provider_pk: int, **kwargs): + """Sync single or multiple users to SCIM""" + messages = [] + provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first() + if not provider: + return messages + try: + client = SCIMUserClient(provider) + except SCIMRequestException: + return messages + paginator = Paginator( + User.objects.all().filter(**kwargs).exclude(pk=get_anonymous_user().pk).order_by("pk"), + PAGE_SIZE, + ) + LOGGER.debug("starting user sync for page", page=page) + for user in paginator.page(page).object_list: + try: + client.write(user) + except SCIMRequestException as exc: + LOGGER.warning("failed to sync user", exc=exc, user=user) + messages.append( + _( + "Failed to sync user due to remote error %(name)s: %(error)s" + % { + "name": user.username, + "error": str(exc), + } + ) + ) + except StopSync: + break + return messages + + +@CELERY_APP.task() +def scim_sync_group(page: int, provider_pk: int, **kwargs): + """Sync single or multiple groups to SCIM""" + messages = [] + provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first() + if not provider: + return messages + try: + client = SCIMGroupClient(provider) + except SCIMRequestException: + return messages + paginator = Paginator(Group.objects.all().filter(**kwargs).order_by("pk"), PAGE_SIZE) + LOGGER.debug("starting group sync for page", page=page) + for group in paginator.page(page).object_list: + try: + client.write(group) + except SCIMRequestException as exc: + LOGGER.warning("failed to sync group", exc=exc, group=group) + messages.append( + _( + "Failed to sync group due to remote error %(name)s: %(error)s" + % { + "name": group.name, + "error": str(exc), + } + ) + ) + except StopSync: + break + return messages + + +@CELERY_APP.task() +def scim_signal_direct(model: str, pk: Any, raw_op: str): + """Handler for post_save and pre_delete signal""" + model_class: type[Model] = path_to_class(model) + instance = model_class.objects.filter(pk=pk).first() + if not instance: + return + operation = PatchOp(raw_op) + for provider in SCIMProvider.objects.all(): + client = client_for_model(provider, instance) + try: + if operation == PatchOp.add: + client.write(instance) + if operation == PatchOp.remove: + client.delete(instance) + except (StopSync, SCIMRequestException) as exc: + LOGGER.warning(exc) + + +@CELERY_APP.task() +def scim_signal_m2m(group_pk: str, action: str, pk_set: set[int]): + """Update m2m (group membership)""" + group = Group.objects.filter(pk=group_pk).first() + if not group: + return + for provider in SCIMProvider.objects.all(): + client = SCIMGroupClient(provider) + try: + operation = None + if action == "post_add": + operation = PatchOp.add + if action == "post_remove": + operation = PatchOp.remove + client.update_group(group, operation, pk_set) + except (StopSync, SCIMRequestException) as exc: + LOGGER.warning(exc) diff --git a/authentik/providers/scim/tests/__init__.py b/authentik/providers/scim/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/scim/tests/test_client.py b/authentik/providers/scim/tests/test_client.py new file mode 100644 index 000000000..c7d862824 --- /dev/null +++ b/authentik/providers/scim/tests/test_client.py @@ -0,0 +1,78 @@ +"""SCIM Client tests""" +from django.test import TestCase +from requests_mock import Mocker + +from authentik.blueprints.tests import apply_blueprint +from authentik.lib.generators import generate_id +from authentik.providers.scim.clients.base import SCIMClient +from authentik.providers.scim.models import SCIMMapping, SCIMProvider + + +class SCIMClientTests(TestCase): + """SCIM Client tests""" + + @apply_blueprint("system/providers-scim.yaml") + def setUp(self) -> None: + self.provider: SCIMProvider = SCIMProvider.objects.create( + name=generate_id(), + url="https://localhost", + token=generate_id(), + ) + self.provider.property_mappings.add( + SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user") + ) + self.provider.property_mappings_group.add( + SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group") + ) + + def test_config(self): + """Test valid config: + https://docs.aws.amazon.com/singlesignon/latest/developerguide/serviceproviderconfig.html""" + with Mocker() as mock: + mock: Mocker + mock.get( + "https://localhost/ServiceProviderConfig", + json={ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], + "documentationUri": ( + "https://docs.aws.amazon.com/singlesignon/latest/" + "userguide/manage-your-identity-source-idp.html" + ), + "authenticationSchemes": [ + { + "type": "oauthbearertoken", + "name": "OAuth Bearer Token", + "description": ( + "Authentication scheme using the OAuth Bearer Token Standard" + ), + "specUri": "https://www.rfc-editor.org/info/rfc6750", + "documentationUri": ( + "https://docs.aws.amazon.com/singlesignon/latest/" + "userguide/provision-automatically.html" + ), + "primary": True, + } + ], + "patch": {"supported": True}, + "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576}, + "filter": {"supported": True, "maxResults": 50}, + "changePassword": {"supported": False}, + "sort": {"supported": False}, + "etag": {"supported": False}, + }, + ) + SCIMClient(self.provider) + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.request_history[0].method, "GET") + + def test_config_invalid(self): + """Test invalid config""" + with Mocker() as mock: + mock: Mocker + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + SCIMClient(self.provider) + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.request_history[0].method, "GET") diff --git a/authentik/providers/scim/tests/test_group.py b/authentik/providers/scim/tests/test_group.py new file mode 100644 index 000000000..503117bcd --- /dev/null +++ b/authentik/providers/scim/tests/test_group.py @@ -0,0 +1,133 @@ +"""SCIM Group tests""" +from json import loads + +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user +from jsonschema import validate +from requests_mock import Mocker + +from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Group, User +from authentik.lib.generators import generate_id +from authentik.providers.scim.models import SCIMMapping, SCIMProvider + + +class SCIMGroupTests(TestCase): + """SCIM Group tests""" + + @apply_blueprint("system/providers-scim.yaml") + def setUp(self) -> None: + # Delete all users and groups as the mocked HTTP responses only return one ID + # which will cause errors with multiple users + User.objects.all().exclude(pk=get_anonymous_user().pk).delete() + Group.objects.all().delete() + self.provider: SCIMProvider = SCIMProvider.objects.create( + name=generate_id(), + url="https://localhost", + token=generate_id(), + ) + self.provider.property_mappings.set( + [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")] + ) + self.provider.property_mappings_group.set( + [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")] + ) + + @Mocker() + def test_group_create(self, mock: Mocker): + """Test group creation""" + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Groups", + json={ + "id": scim_id, + }, + ) + uid = generate_id() + group = Group.objects.create( + name=uid, + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertJSONEqual( + mock.request_history[1].body, + {"externalId": str(group.pk), "displayName": group.name}, + ) + + @Mocker() + def test_group_create_update(self, mock: Mocker): + """Test group creation and update""" + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Groups", + json={ + "id": scim_id, + }, + ) + mock.put( + "https://localhost/Groups", + json={ + "id": scim_id, + }, + ) + uid = generate_id() + group = Group.objects.create( + name=uid, + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + body = loads(mock.request_history[1].body) + with open("schemas/scim-group.schema.json", encoding="utf-8") as schema: + validate(body, loads(schema.read())) + self.assertEqual( + body, + {"externalId": str(group.pk), "displayName": group.name}, + ) + group.save() + self.assertEqual(mock.call_count, 4) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertEqual(mock.request_history[2].method, "GET") + self.assertEqual(mock.request_history[3].method, "PUT") + + @Mocker() + def test_group_create_delete(self, mock: Mocker): + """Test group creation""" + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Groups", + json={ + "id": scim_id, + }, + ) + mock.delete("https://localhost/Groups", status_code=204) + uid = generate_id() + group = Group.objects.create( + name=uid, + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertJSONEqual( + mock.request_history[1].body, + {"externalId": str(group.pk), "displayName": group.name}, + ) + group.delete() + self.assertEqual(mock.call_count, 4) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[3].method, "DELETE") + self.assertEqual(mock.request_history[3].url, f"https://localhost/Groups/{scim_id}") diff --git a/authentik/providers/scim/tests/test_membership.py b/authentik/providers/scim/tests/test_membership.py new file mode 100644 index 000000000..76046e622 --- /dev/null +++ b/authentik/providers/scim/tests/test_membership.py @@ -0,0 +1,228 @@ +"""SCIM Membership tests""" +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user +from requests_mock import Mocker + +from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Group, User +from authentik.lib.generators import generate_id +from authentik.providers.scim.clients.base import default_service_provider_config +from authentik.providers.scim.models import SCIMMapping, SCIMProvider +from authentik.providers.scim.tasks import scim_sync + + +class SCIMMembershipTests(TestCase): + """SCIM Membership tests""" + + provider: SCIMProvider + + def setUp(self) -> None: + # Delete all users and groups as the mocked HTTP responses only return one ID + # which will cause errors with multiple users + User.objects.all().exclude(pk=get_anonymous_user().pk).delete() + Group.objects.all().delete() + + @apply_blueprint("system/providers-scim.yaml") + def configure(self) -> None: + """Configure provider""" + self.provider: SCIMProvider = SCIMProvider.objects.create( + name=generate_id(), + url="https://localhost", + token=generate_id(), + ) + self.provider.property_mappings.set( + [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")] + ) + self.provider.property_mappings_group.set( + [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")] + ) + + def test_member_add(self): + """Test member add""" + config = default_service_provider_config() + config.patch.supported = True + user_scim_id = generate_id() + group_scim_id = generate_id() + uid = generate_id() + group = Group.objects.create( + name=uid, + ) + + user = User.objects.create(username=generate_id()) + + with Mocker() as mocker: + mocker.get( + "https://localhost/ServiceProviderConfig", + json=config.dict(), + ) + mocker.post( + "https://localhost/Users", + json={ + "id": user_scim_id, + }, + ) + mocker.post( + "https://localhost/Groups", + json={ + "id": group_scim_id, + }, + ) + + self.configure() + scim_sync.delay(self.provider.pk).get() + + self.assertEqual(mocker.call_count, 6) + self.assertEqual(mocker.request_history[0].method, "GET") + self.assertEqual(mocker.request_history[1].method, "GET") + self.assertEqual(mocker.request_history[2].method, "GET") + self.assertEqual(mocker.request_history[3].method, "POST") + self.assertEqual(mocker.request_history[4].method, "GET") + self.assertEqual(mocker.request_history[5].method, "POST") + self.assertJSONEqual( + mocker.request_history[3].body, + { + "emails": [], + "externalId": user.uid, + "name": {"familyName": "", "formatted": "", "givenName": ""}, + "photos": [], + "userName": user.username, + }, + ) + self.assertJSONEqual( + mocker.request_history[5].body, + {"externalId": str(group.pk), "displayName": group.name}, + ) + + with Mocker() as mocker: + mocker.get( + "https://localhost/ServiceProviderConfig", + json=config.dict(), + ) + mocker.patch( + f"https://localhost/Groups/{group_scim_id}", + json={}, + ) + group.users.add(user) + self.assertEqual(mocker.call_count, 2) + self.assertEqual(mocker.request_history[0].method, "GET") + self.assertEqual(mocker.request_history[1].method, "PATCH") + self.assertJSONEqual( + mocker.request_history[1].body, + { + "Operations": [ + { + "op": "add", + "path": "members", + "value": [{"value": user_scim_id}], + } + ] + }, + ) + + def test_member_remove(self): + """Test member remove""" + config = default_service_provider_config() + config.patch.supported = True + user_scim_id = generate_id() + group_scim_id = generate_id() + uid = generate_id() + group = Group.objects.create( + name=uid, + ) + + user = User.objects.create(username=generate_id()) + + with Mocker() as mocker: + mocker.get( + "https://localhost/ServiceProviderConfig", + json=config.dict(), + ) + mocker.post( + "https://localhost/Users", + json={ + "id": user_scim_id, + }, + ) + mocker.post( + "https://localhost/Groups", + json={ + "id": group_scim_id, + }, + ) + + self.configure() + scim_sync.delay(self.provider.pk).get() + + self.assertEqual(mocker.call_count, 6) + self.assertEqual(mocker.request_history[0].method, "GET") + self.assertEqual(mocker.request_history[1].method, "GET") + self.assertEqual(mocker.request_history[2].method, "GET") + self.assertEqual(mocker.request_history[3].method, "POST") + self.assertEqual(mocker.request_history[4].method, "GET") + self.assertEqual(mocker.request_history[5].method, "POST") + self.assertJSONEqual( + mocker.request_history[3].body, + { + "emails": [], + "externalId": user.uid, + "name": {"familyName": "", "formatted": "", "givenName": ""}, + "photos": [], + "userName": user.username, + }, + ) + self.assertJSONEqual( + mocker.request_history[5].body, + {"externalId": str(group.pk), "displayName": group.name}, + ) + + with Mocker() as mocker: + mocker.get( + "https://localhost/ServiceProviderConfig", + json=config.dict(), + ) + mocker.patch( + f"https://localhost/Groups/{group_scim_id}", + json={}, + ) + group.users.add(user) + self.assertEqual(mocker.call_count, 2) + self.assertEqual(mocker.request_history[0].method, "GET") + self.assertEqual(mocker.request_history[1].method, "PATCH") + self.assertJSONEqual( + mocker.request_history[1].body, + { + "Operations": [ + { + "op": "add", + "path": "members", + "value": [{"value": user_scim_id}], + } + ] + }, + ) + + with Mocker() as mocker: + mocker.get( + "https://localhost/ServiceProviderConfig", + json=config.dict(), + ) + mocker.patch( + f"https://localhost/Groups/{group_scim_id}", + json={}, + ) + group.users.remove(user) + self.assertEqual(mocker.call_count, 2) + self.assertEqual(mocker.request_history[0].method, "GET") + self.assertEqual(mocker.request_history[1].method, "PATCH") + self.assertJSONEqual( + mocker.request_history[1].body, + { + "Operations": [ + { + "op": "remove", + "path": "members", + "value": [{"value": user_scim_id}], + } + ] + }, + ) diff --git a/authentik/providers/scim/tests/test_user.py b/authentik/providers/scim/tests/test_user.py new file mode 100644 index 000000000..2d510e118 --- /dev/null +++ b/authentik/providers/scim/tests/test_user.py @@ -0,0 +1,250 @@ +"""SCIM User tests""" +from json import loads + +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user +from jsonschema import validate +from requests_mock import Mocker + +from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Group, User +from authentik.lib.generators import generate_id +from authentik.providers.scim.models import SCIMMapping, SCIMProvider +from authentik.providers.scim.tasks import scim_sync + + +class SCIMUserTests(TestCase): + """SCIM User tests""" + + @apply_blueprint("system/providers-scim.yaml") + def setUp(self) -> None: + # Delete all users and groups as the mocked HTTP responses only return one ID + # which will cause errors with multiple users + User.objects.all().exclude(pk=get_anonymous_user().pk).delete() + Group.objects.all().delete() + self.provider: SCIMProvider = SCIMProvider.objects.create( + name=generate_id(), + url="https://localhost", + token=generate_id(), + ) + self.provider.property_mappings.add( + SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user") + ) + self.provider.property_mappings_group.add( + SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group") + ) + + @Mocker() + def test_user_create(self, mock: Mocker): + """Test user creation""" + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Users", + json={ + "id": scim_id, + }, + ) + uid = generate_id() + user = User.objects.create( + username=uid, + name=uid, + email=f"{uid}@goauthentik.io", + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertJSONEqual( + mock.request_history[1].body, + { + "emails": [ + { + "primary": True, + "type": "other", + "value": f"{uid}@goauthentik.io", + } + ], + "externalId": user.uid, + "name": { + "familyName": "", + "formatted": uid, + "givenName": uid, + }, + "photos": [], + "userName": uid, + }, + ) + + @Mocker() + def test_user_create_update(self, mock: Mocker): + """Test user creation and update""" + scim_id = generate_id() + mock: Mocker + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Users", + json={ + "id": scim_id, + }, + ) + mock.put( + "https://localhost/Users", + json={ + "id": scim_id, + }, + ) + uid = generate_id() + user = User.objects.create( + username=uid, + name=uid, + email=f"{uid}@goauthentik.io", + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + body = loads(mock.request_history[1].body) + with open("schemas/scim-user.schema.json", encoding="utf-8") as schema: + validate(body, loads(schema.read())) + self.assertEqual( + body, + { + "emails": [ + { + "primary": True, + "type": "other", + "value": f"{uid}@goauthentik.io", + } + ], + "externalId": user.uid, + "name": { + "familyName": "", + "formatted": uid, + "givenName": uid, + }, + "photos": [], + "userName": uid, + }, + ) + user.save() + self.assertEqual(mock.call_count, 4) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertEqual(mock.request_history[2].method, "GET") + self.assertEqual(mock.request_history[3].method, "PUT") + + @Mocker() + def test_user_create_delete(self, mock: Mocker): + """Test user creation""" + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Users", + json={ + "id": scim_id, + }, + ) + mock.delete("https://localhost/Users", status_code=204) + uid = generate_id() + user = User.objects.create( + username=uid, + name=uid, + email=f"{uid}@goauthentik.io", + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertJSONEqual( + mock.request_history[1].body, + { + "emails": [ + { + "primary": True, + "type": "other", + "value": f"{uid}@goauthentik.io", + } + ], + "externalId": user.uid, + "name": { + "familyName": "", + "formatted": uid, + "givenName": uid, + }, + "photos": [], + "userName": uid, + }, + ) + user.delete() + self.assertEqual(mock.call_count, 4) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[3].method, "DELETE") + self.assertEqual(mock.request_history[3].url, f"https://localhost/Users/{scim_id}") + + @Mocker() + def test_sync_task(self, mock: Mocker): + """Test sync tasks""" + user_scim_id = generate_id() + group_scim_id = generate_id() + uid = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Users", + json={ + "id": user_scim_id, + }, + ) + mock.put( + f"https://localhost/Users/{user_scim_id}", + json={ + "id": user_scim_id, + }, + ) + mock.post( + "https://localhost/Groups", + json={ + "id": group_scim_id, + }, + ) + user = User.objects.create( + username=uid, + name=uid, + email=f"{uid}@goauthentik.io", + ) + + scim_sync.delay(self.provider.pk).get() + + self.assertEqual(mock.call_count, 5) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertEqual(mock.request_history[-2].method, "PUT") + self.assertJSONEqual( + mock.request_history[1].body, + { + "emails": [ + { + "primary": True, + "type": "other", + "value": f"{uid}@goauthentik.io", + } + ], + "externalId": user.uid, + "name": { + "familyName": "", + "formatted": uid, + "givenName": uid, + }, + "photos": [], + "userName": uid, + }, + ) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 22ccbdb9c..7aa71f00b 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -80,6 +80,7 @@ INSTALLED_APPS = [ "authentik.providers.oauth2", "authentik.providers.proxy", "authentik.providers.saml", + "authentik.providers.scim", "authentik.recovery", "authentik.sources.ldap", "authentik.sources.oauth", diff --git a/authentik/sources/saml/tests/test_metadata.py b/authentik/sources/saml/tests/test_metadata.py index 590af7d96..d81033e12 100644 --- a/authentik/sources/saml/tests/test_metadata.py +++ b/authentik/sources/saml/tests/test_metadata.py @@ -28,7 +28,7 @@ class TestMetadataProcessor(TestCase): xml = MetadataProcessor(self.source, request).build_entity_descriptor() metadata = lxml_from_string(xml) - schema = etree.XMLSchema(etree.parse("xml/saml-schema-metadata-2.0.xsd")) # nosec + schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec self.assertTrue(schema.validate(metadata)) def test_metadata_consistent(self): diff --git a/blueprints/schema.json b/blueprints/schema.json index 98b4f5435..e9232acd9 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -81,6 +81,8 @@ "authentik_providers_proxy.proxyprovider", "authentik_providers_saml.samlpropertymapping", "authentik_providers_saml.samlprovider", + "authentik_providers_scim.scimmapping", + "authentik_providers_scim.scimprovider", "authentik_sources_ldap.ldappropertymapping", "authentik_sources_ldap.ldapsource", "authentik_sources_oauth.oauthsource", diff --git a/blueprints/system/providers-scim.yaml b/blueprints/system/providers-scim.yaml new file mode 100644 index 000000000..5f7fc4b17 --- /dev/null +++ b/blueprints/system/providers-scim.yaml @@ -0,0 +1,58 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/system: "true" + name: System - SCIM Provider - Mappings +entries: + - identifiers: + managed: goauthentik.io/providers/scim/user + model: authentik_providers_scim.scimmapping + attrs: + name: "authentik default SCIM Mapping: User" + expression: | + # Some implementations require givenName and familyName to be set + givenName, familyName = request.user.name, "" + # This default sets givenName to the name before the first space + # and the remainder as family name + # if the user's name has no space the givenName is the entire name + # (this might cause issues with some SCIM implementations) + if " " in request.user.name: + givenName, _, familyName = request.user.name.partition(" ") + + # photos supports URLs to images, however authentik might return data URIs + avatar = request.user.avatar + photos = [] + if "://" in avatar: + photos = [{"value": avatar, "type": "photo"}] + + locale = request.user.locale() + if locale == "": + locale = None + + emails = [] + if request.user.email != "": + emails.append({ + "value": request.user.email, + "type": "other", + "primary": True, + }) + return { + "userName": request.user.username, + "name": { + "formatted": request.user.name, + "givenName": givenName, + "familyName": familyName, + }, + "photos": photos, + "locale": locale, + "emails": emails, + } + - identifiers: + managed: goauthentik.io/providers/scim/group + model: authentik_providers_scim.scimmapping + attrs: + name: "authentik default SCIM Mapping: Group" + expression: | + return { + "displayName": group.name, + } diff --git a/poetry.lock b/poetry.lock index 1c91771c2..b017b1567 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1172,6 +1172,27 @@ django = "*" django-guardian = "*" djangorestframework = "*" +[[package]] +name = "dnspython" +version = "2.3.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, +] + +[package.extras] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<40.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + [[package]] name = "docker" version = "6.0.1" @@ -1249,6 +1270,22 @@ files = [ setuptools = "*" six = "*" +[[package]] +name = "email-validator" +version = "1.3.1" +description = "A robust email address syntax and deliverability validation library." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"}, + {file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"}, +] + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + [[package]] name = "facebook-sdk" version = "3.1.0" @@ -2425,12 +2462,31 @@ files = [ ] [package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} typing-extensions = ">=4.1.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pydantic-scim" +version = "0.0.7" +description = "Pydantic types for SCIM" +category = "main" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pydantic-scim-0.0.7.tar.gz", hash = "sha256:bc043da51c346051dfd372f12d1837c0846b815236340156d663a8514cba5761"}, + {file = "pydantic_scim-0.0.7-py3-none-any.whl", hash = "sha256:058eb195f75ef32d04eaf6369c125d5fb7052891694686f8e55e04d184ab1360"}, +] + +[package.dependencies] +pydantic = [ + {version = ">=1.8.0"}, + {version = ">=1.8.0", extras = ["email"]}, +] + [[package]] name = "pyjwt" version = "2.6.0" @@ -2533,7 +2589,7 @@ files = [ cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] @@ -3903,4 +3959,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "47eeb02200cb4980368d3a11c6bee111a19a86d7e4d8ad90ef3bd590493f28b3" +content-hash = "2ebb5d81a0b4c0883704dd8b74dc9bf7e8893cd7caadccaa0c47237e0394d54a" diff --git a/pyproject.toml b/pyproject.toml index 105948126..eeec897fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,6 +161,7 @@ wsproto = "*" xmlsec = "*" zxcvbn = "*" watchdog = "*" +pydantic-scim = "^0.0.7" [tool.poetry.dev-dependencies] bandit = "*" diff --git a/schema.yml b/schema.yml index a5e7e0e0d..c5acc3056 100644 --- a/schema.yml +++ b/schema.yml @@ -4007,6 +4007,16 @@ paths: - app_password - recovery - verification + description: |- + * `verification` - Intent Verification + * `api` - Intent Api + * `recovery` - Intent Recovery + * `app_password` - Intent App Password + + * `verification` - Intent Verification + * `api` - Intent Api + * `recovery` - Intent Recovery + * `app_password` - Intent App Password - in: query name: managed schema: @@ -5864,6 +5874,14 @@ paths: - alert - notice - warning + description: |- + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert + + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert - in: query name: user schema: @@ -6139,8 +6157,12 @@ paths: - alert - notice - warning - description: Controls which severity level the created notifications will - have. + description: |- + Controls which severity level the created notifications will have. + + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert tags: - events security: @@ -6391,6 +6413,16 @@ paths: - local - webhook - webhook_slack + description: |- + * `local` - authentik inbuilt notifications + * `webhook` - Generic Webhook + * `webhook_slack` - Slack Webhook (Slack/Discord) + * `email` - Email + + * `local` - authentik inbuilt notifications + * `webhook` - Generic Webhook + * `webhook_slack` - Slack Webhook (Slack/Discord) + * `email` - Email - in: query name: name schema: @@ -6724,10 +6756,12 @@ paths: - restart - restart_with_context - retry - description: Configure how the flow executor should handle an invalid response - to a challenge. RETRY returns the error message and a similar challenge - to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT - restarts the flow while keeping the current context. + description: |- + Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context. + + * `retry` - Retry + * `restart` - Restart + * `restart_with_context` - Restart With Context - in: query name: order schema: @@ -6771,6 +6805,12 @@ paths: enum: - all - any + description: |- + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass + + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass - in: query name: re_evaluate_policies schema: @@ -7154,7 +7194,12 @@ paths: - continue - message - message_continue - description: Configure what should happen when a flow denies access to a user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue - in: query name: designation schema: @@ -7167,8 +7212,16 @@ paths: - recovery - stage_configuration - unenrollment - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration - in: query name: flow_uuid schema: @@ -10778,8 +10831,36 @@ paths: - system_task_execution - update_available - user_write - description: Match created events with this action type. When left empty, - all action types will be matched. + description: |- + Match created events with this action type. When left empty, all action types will be matched. + + * `login` - Login + * `login_failed` - Login Failed + * `logout` - Logout + * `user_write` - User Write + * `suspicious_request` - Suspicious Request + * `password_set` - Password Set + * `secret_view` - Secret View + * `secret_rotate` - Secret Rotate + * `invitation_used` - Invite Used + * `authorize_application` - Authorize Application + * `source_linked` - Source Linked + * `impersonation_started` - Impersonation Started + * `impersonation_ended` - Impersonation Ended + * `flow_execution` - Flow Execution + * `policy_execution` - Policy Execution + * `policy_exception` - Policy Exception + * `property_mapping_exception` - Property Mapping Exception + * `system_task_execution` - System Task Execution + * `system_task_exception` - System Task Exception + * `system_exception` - System Exception + * `configuration_error` - Configuration Error + * `model_created` - Model Created + * `model_updated` - Model Updated + * `model_deleted` - Model Deleted + * `email_sent` - Email Sent + * `update_available` - Update Available + * `custom_` - Custom Prefix - in: query name: app schema: @@ -13546,6 +13627,292 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /propertymappings/scim/: + get: + operationId: propertymappings_scim_list + description: SCIMMapping Viewset + parameters: + - in: query + name: expression + schema: + type: string + - in: query + name: managed + schema: + type: array + items: + type: string + explode: true + style: form + - in: query + name: name + schema: + type: string + - name: ordering + required: false + in: query + description: Which field to use when ordering the results. + schema: + type: string + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page_size + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - in: query + name: pm_uuid + schema: + type: string + format: uuid + - name: search + required: false + in: query + description: A search term. + schema: + type: string + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSCIMMappingList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: propertymappings_scim_create + description: SCIMMapping Viewset + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMappingRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /propertymappings/scim/{pm_uuid}/: + get: + operationId: propertymappings_scim_retrieve + description: SCIMMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this SCIM Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: propertymappings_scim_update + description: SCIMMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this SCIM Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMappingRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: propertymappings_scim_partial_update + description: SCIMMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this SCIM Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSCIMMappingRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: propertymappings_scim_destroy + description: SCIMMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this SCIM Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /propertymappings/scim/{pm_uuid}/used_by/: + get: + operationId: propertymappings_scim_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this SCIM Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /propertymappings/scope/: get: operationId: propertymappings_scope_list @@ -14351,8 +14718,11 @@ paths: enum: - confidential - public - description: Confidential clients are capable of maintaining the confidentiality - of their credentials. Public clients are incapable + description: |- + Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + + * `confidential` - Confidential + * `public` - Public - in: query name: include_claims_in_id_token schema: @@ -14364,7 +14734,11 @@ paths: enum: - global - per_provider - description: Configure how the issuer field of the ID Token should be filled. + description: |- + Configure how the issuer field of the ID Token should be filled. + + * `global` - Same identifier is used for all providers + * `per_provider` - Each provider has a different issuer, based on the application slug. - in: query name: name schema: @@ -14425,8 +14799,14 @@ paths: - user_id - user_upn - user_username - description: Configure what data should be used as unique User Identifier. - For most cases, the default should be fine. + description: |- + Configure what data should be used as unique User Identifier. For most cases, the default should be fine. + + * `hashed_user_id` - Based on the Hashed User ID + * `user_id` - Based on user ID + * `user_username` - Based on the username + * `user_email` - Based on the User's Email. This is recommended over the UPN method. + * `user_upn` - Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains. tags: - providers security: @@ -15089,6 +15469,16 @@ paths: - http://www.w3.org/2001/04/xmldsig-more#sha384 - http://www.w3.org/2001/04/xmlenc#sha256 - http://www.w3.org/2001/04/xmlenc#sha512 + description: |- + * `http://www.w3.org/2000/09/xmldsig#sha1` - SHA1 + * `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384 + * `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512 + + * `http://www.w3.org/2000/09/xmldsig#sha1` - SHA1 + * `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384 + * `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512 - in: query name: issuer schema: @@ -15149,6 +15539,18 @@ paths: - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 + description: |- + * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - RSA-SHA1 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` - RSA-SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` - RSA-SHA384 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` - RSA-SHA512 + * `http://www.w3.org/2000/09/xmldsig#dsa-sha1` - DSA-SHA1 + + * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - RSA-SHA1 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` - RSA-SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` - RSA-SHA384 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` - RSA-SHA512 + * `http://www.w3.org/2000/09/xmldsig#dsa-sha1` - DSA-SHA1 - in: query name: signing_kp schema: @@ -15162,8 +15564,11 @@ paths: enum: - post - redirect - description: This determines how authentik sends the response back to the - Service Provider. + description: |- + This determines how authentik sends the response back to the Service Provider. + + * `redirect` - Redirect + * `post` - Post - in: query name: verification_kp schema: @@ -15504,6 +15909,319 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /providers/scim/: + get: + operationId: providers_scim_list + description: SCIMProvider Viewset + parameters: + - in: query + name: authorization_flow + schema: + type: string + format: uuid + - in: query + name: name + schema: + type: string + - name: ordering + required: false + in: query + description: Which field to use when ordering the results. + schema: + type: string + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page_size + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: search + required: false + in: query + description: A search term. + schema: + type: string + - in: query + name: token + schema: + type: string + - in: query + name: url + schema: + type: string + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSCIMProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_scim_create + description: SCIMProvider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/scim/{id}/: + get: + operationId: providers_scim_retrieve + description: SCIMProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_scim_update + description: SCIMProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_scim_partial_update + description: SCIMProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSCIMProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SCIMProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_scim_destroy + description: SCIMProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/scim/{id}/sync_status/: + get: + operationId: providers_scim_sync_status_retrieve + description: Get provider's sync status + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + description: '' + '404': + description: Task not found + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/scim/{id}/used_by/: + get: + operationId: providers_scim_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SCIM Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /root/config/: get: operationId: root_config_retrieve @@ -16462,6 +17180,12 @@ paths: enum: - all - any + description: |- + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass + + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass - in: query name: profile_url schema: @@ -16494,8 +17218,14 @@ paths: - identifier - username_deny - username_link - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. tags: - sources security: @@ -16819,6 +17549,12 @@ paths: enum: - all - any + description: |- + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass + + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass - name: search required: false in: query @@ -16839,8 +17575,14 @@ paths: - identifier - username_deny - username_link - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. tags: - sources security: @@ -17153,6 +17895,14 @@ paths: - POST - POST_AUTO - REDIRECT + description: |- + * `REDIRECT` - Redirect Binding + * `POST` - POST Binding + * `POST_AUTO` - POST Binding with auto-confirmation + + * `REDIRECT` - Redirect Binding + * `POST` - POST Binding + * `POST_AUTO` - POST Binding with auto-confirmation - in: query name: digest_algorithm schema: @@ -17162,6 +17912,16 @@ paths: - http://www.w3.org/2001/04/xmldsig-more#sha384 - http://www.w3.org/2001/04/xmlenc#sha256 - http://www.w3.org/2001/04/xmlenc#sha512 + description: |- + * `http://www.w3.org/2000/09/xmldsig#sha1` - SHA1 + * `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384 + * `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512 + + * `http://www.w3.org/2000/09/xmldsig#sha1` - SHA1 + * `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384 + * `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512 - in: query name: enabled schema: @@ -17193,8 +17953,14 @@ paths: - urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - description: NameID Policy sent to the IdP. Can be unset, in which case no - Policy is sent. + description: |- + NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent. + + * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` - Email + * `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - Persistent + * `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509 + * `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows + * `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient - name: ordering required: false in: query @@ -17220,6 +17986,12 @@ paths: enum: - all - any + description: |- + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass + + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass - in: query name: pre_authentication_flow schema: @@ -17241,6 +18013,18 @@ paths: - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 + description: |- + * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - RSA-SHA1 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` - RSA-SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` - RSA-SHA384 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` - RSA-SHA512 + * `http://www.w3.org/2000/09/xmldsig#dsa-sha1` - DSA-SHA1 + + * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - RSA-SHA1 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` - RSA-SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` - RSA-SHA384 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` - RSA-SHA512 + * `http://www.w3.org/2000/09/xmldsig#dsa-sha1` - DSA-SHA1 - in: query name: signing_kp schema: @@ -17272,8 +18056,14 @@ paths: - identifier - username_deny - username_link - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. tags: - sources security: @@ -19173,6 +19963,12 @@ paths: enum: - basic - bearer + description: |- + * `basic` - Basic + * `bearer` - Bearer + + * `basic` - Basic + * `bearer` - Bearer - in: query name: configure_flow schema: @@ -19216,6 +20012,12 @@ paths: enum: - generic - twilio + description: |- + * `twilio` - Twilio + * `generic` - Generic + + * `twilio` - Twilio + * `generic` - Generic - name: search required: false in: query @@ -19767,6 +20569,12 @@ paths: enum: - 6 - 8 + description: |- + * `6` - 6 digits, widely compatible + * `8` - 8 digits, not compatible with apps like Google Authenticator + + * `6` - 6 digits, widely compatible + * `8` - 8 digits, not compatible with apps like Google Authenticator - in: query name: name schema: @@ -20062,6 +20870,14 @@ paths: - configure - deny - skip + description: |- + * `skip` - Skip + * `deny` - Deny + * `configure` - Configure + + * `skip` - Skip + * `deny` - Deny + * `configure` - Configure - name: ordering required: false in: query @@ -20335,6 +21151,12 @@ paths: enum: - cross-platform - platform + description: |- + * `platform` - Platform + * `cross-platform` - Cross Platform + + * `platform` - Platform + * `cross-platform` - Cross Platform - in: query name: configure_flow schema: @@ -20370,6 +21192,14 @@ paths: - discouraged - preferred - required + description: |- + * `discouraged` - Discouraged + * `preferred` - Preferred + * `required` - Required + + * `discouraged` - Discouraged + * `preferred` - Preferred + * `required` - Required - name: search required: false in: query @@ -20389,6 +21219,14 @@ paths: - discouraged - preferred - required + description: |- + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged + + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged tags: - stages security: @@ -20915,6 +21753,14 @@ paths: - always_require - expiring - permanent + description: |- + * `always_require` - Always Require + * `permanent` - Permanent + * `expiring` - Expiring + + * `always_require` - Always Require + * `permanent` - Permanent + * `expiring` - Expiring - in: query name: name schema: @@ -23289,6 +24135,36 @@ paths: - text - text_read_only - username + description: |- + * `text` - Text: Simple Text input + * `text_read_only` - Text (read-only): Simple Text input, but cannot be edited. + * `username` - Username: Same as Text input, but checks for and prevents duplicate usernames. + * `email` - Email: Text field with Email type. + * `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical. + * `number` - Number + * `checkbox` - Checkbox + * `date` - Date + * `date-time` - Date Time + * `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI + * `separator` - Separator: Static Separator Line + * `hidden` - Hidden: Hidden field, can be used to insert data into form. + * `static` - Static: Static value, displayed as-is. + * `ak-locale` - authentik: Selection of locales authentik supports + + * `text` - Text: Simple Text input + * `text_read_only` - Text (read-only): Simple Text input, but cannot be edited. + * `username` - Username: Same as Text input, but checks for and prevents duplicate usernames. + * `email` - Email: Text field with Email type. + * `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical. + * `number` - Number + * `checkbox` - Checkbox + * `date` - Date + * `date-time` - Date Time + * `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI + * `separator` - Separator: Static Separator Line + * `hidden` - Hidden: Hidden field, can be used to insert data into form. + * `static` - Static: Static value, displayed as-is. + * `ak-locale` - authentik: Selection of locales authentik supports tags: - stages security: @@ -24702,6 +25578,14 @@ paths: - always_create - create_when_required - never_create + description: |- + * `never_create` - Never Create + * `create_when_required` - Create When Required + * `always_create` - Always Create + + * `never_create` - Never Create + * `create_when_required` - Create When Required + * `always_create` - Always Create - in: query name: user_path_template schema: @@ -25002,6 +25886,7 @@ components: - authentik.providers.oauth2 - authentik.providers.proxy - authentik.providers.saml + - authentik.providers.scim - authentik.recovery - authentik.sources.ldap - authentik.sources.oauth @@ -25030,6 +25915,53 @@ components: - authentik.blueprints - authentik.core type: string + description: |- + * `authentik.admin` - authentik Admin + * `authentik.api` - authentik API + * `authentik.crypto` - authentik Crypto + * `authentik.events` - authentik Events + * `authentik.flows` - authentik Flows + * `authentik.lib` - authentik lib + * `authentik.outposts` - authentik Outpost + * `authentik.policies.dummy` - authentik Policies.Dummy + * `authentik.policies.event_matcher` - authentik Policies.Event Matcher + * `authentik.policies.expiry` - authentik Policies.Expiry + * `authentik.policies.expression` - authentik Policies.Expression + * `authentik.policies.password` - authentik Policies.Password + * `authentik.policies.reputation` - authentik Policies.Reputation + * `authentik.policies` - authentik Policies + * `authentik.providers.ldap` - authentik Providers.LDAP + * `authentik.providers.oauth2` - authentik Providers.OAuth2 + * `authentik.providers.proxy` - authentik Providers.Proxy + * `authentik.providers.saml` - authentik Providers.SAML + * `authentik.providers.scim` - authentik Providers.SCIM + * `authentik.recovery` - authentik Recovery + * `authentik.sources.ldap` - authentik Sources.LDAP + * `authentik.sources.oauth` - authentik Sources.OAuth + * `authentik.sources.plex` - authentik Sources.Plex + * `authentik.sources.saml` - authentik Sources.SAML + * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS + * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static + * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP + * `authentik.stages.authenticator_validate` - authentik Stages.Authenticator.Validate + * `authentik.stages.authenticator_webauthn` - authentik Stages.Authenticator.WebAuthn + * `authentik.stages.captcha` - authentik Stages.Captcha + * `authentik.stages.consent` - authentik Stages.Consent + * `authentik.stages.deny` - authentik Stages.Deny + * `authentik.stages.dummy` - authentik Stages.Dummy + * `authentik.stages.email` - authentik Stages.Email + * `authentik.stages.identification` - authentik Stages.Identification + * `authentik.stages.invitation` - authentik Stages.User Invitation + * `authentik.stages.password` - authentik Stages.Password + * `authentik.stages.prompt` - authentik Stages.Prompt + * `authentik.stages.user_delete` - authentik Stages.User Delete + * `authentik.stages.user_login` - authentik Stages.User Login + * `authentik.stages.user_logout` - authentik Stages.User Logout + * `authentik.stages.user_write` - authentik Stages.User Write + * `authentik.tenants` - authentik Tenants + * `authentik.blueprints` - authentik Blueprints + * `authentik.core` - authentik Core AppleChallengeResponseRequest: type: object description: Pseudo class for plex response @@ -25097,6 +26029,7 @@ components: launch_url: type: string nullable: true + description: Allow formatting of launch URL readOnly: true open_in_new_tab: type: boolean @@ -25107,6 +26040,9 @@ components: meta_icon: type: string nullable: true + description: |- + Get the URL to the App Icon image. If the name is /static or starts with http + it is returned as-is readOnly: true meta_description: type: string @@ -25162,6 +26098,9 @@ components: - basic - bearer type: string + description: |- + * `basic` - Basic + * `bearer` - Bearer AuthenticateWebAuthnStage: type: object description: AuthenticateWebAuthnStage Serializer @@ -25175,15 +26114,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25246,10 +26189,11 @@ components: format: uuid current: type: boolean + description: Check if session is currently active session readOnly: true user_agent: type: object - description: User agent details + description: Get parsed user agent properties: device: type: object @@ -25312,7 +26256,7 @@ components: readOnly: true geo_ip: type: object - description: GeoIP Details + description: Get parsed user agent properties: continent: type: string @@ -25361,11 +26305,19 @@ components: - require_unauthenticated - require_superuser type: string + description: |- + * `none` - None + * `require_authenticated` - Require Authenticated + * `require_unauthenticated` - Require Unauthenticated + * `require_superuser` - Require Superuser AuthenticatorAttachmentEnum: enum: - platform - cross-platform type: string + description: |- + * `platform` - Platform + * `cross-platform` - Cross Platform AuthenticatorDuoChallenge: type: object description: Duo Challenge @@ -25421,15 +26373,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25571,15 +26527,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25723,15 +26683,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25830,15 +26794,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25901,15 +26869,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -25937,7 +26909,12 @@ components: webauthn_user_verification: allOf: - $ref: '#/components/schemas/UserVerificationEnum' - description: Enforce user verification for WebAuthn devices. + description: |- + Enforce user verification for WebAuthn devices. + + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged required: - component - meta_model_name @@ -25979,7 +26956,12 @@ components: webauthn_user_verification: allOf: - $ref: '#/components/schemas/UserVerificationEnum' - description: Enforce user verification for WebAuthn devices. + description: |- + Enforce user verification for WebAuthn devices. + + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged required: - name AuthenticatorValidationChallenge: @@ -26123,12 +27105,20 @@ components: - authentik.core.auth.TokenBackend - authentik.sources.ldap.auth.LDAPBackend type: string + description: |- + * `authentik.core.auth.InbuiltBackend` - User database + standard password + * `authentik.core.auth.TokenBackend` - User database + app passwords + * `authentik.sources.ldap.auth.LDAPBackend` - User database + LDAP password BindingTypeEnum: enum: - REDIRECT - POST - POST_AUTO type: string + description: |- + * `REDIRECT` - Redirect Binding + * `POST` - POST Binding + * `POST_AUTO` - POST Binding with auto-confirmation BlueprintFile: type: object properties: @@ -26224,6 +27214,12 @@ components: - orphaned - unknown type: string + description: |- + * `successful` - Successful + * `warning` - Warning + * `error` - Error + * `orphaned` - Orphaned + * `unknown` - Unknown Cache: type: object description: Generic cache stats for an object @@ -26240,6 +27236,11 @@ components: - can_impersonate - can_debug type: string + description: |- + * `can_save_media` - Can Save Media + * `can_geo_ip` - Can Geo Ip + * `can_impersonate` - Can Impersonate + * `can_debug` - Can Debug CaptchaChallenge: type: object description: Site public key @@ -26297,15 +27298,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -26393,32 +27398,40 @@ components: fingerprint_sha256: type: string nullable: true + description: Get certificate Hash (SHA256) readOnly: true fingerprint_sha1: type: string nullable: true + description: Get certificate Hash (SHA1) readOnly: true cert_expiry: type: string format: date-time nullable: true + description: Get certificate expiry readOnly: true cert_subject: type: string nullable: true + description: Get certificate subject as full rfc4514 readOnly: true private_key_available: type: boolean + description: Show if this keypair has a private key configured or not readOnly: true private_key_type: type: string nullable: true + description: Get the private key's type, if set readOnly: true certificate_download_url: type: string + description: Get URL to download certificate readOnly: true private_key_download_url: type: string + description: Get URL to download private key readOnly: true managed: type: string @@ -26474,6 +27487,10 @@ components: - shell - redirect type: string + description: |- + * `native` - NATIVE + * `shell` - SHELL + * `redirect` - REDIRECT ChallengeTypes: oneOf: - $ref: '#/components/schemas/AccessDeniedChallenge' @@ -26528,6 +27545,9 @@ components: - confidential - public type: string + description: |- + * `confidential` - Confidential + * `public` - Public Config: type: object description: Serialize authentik Config into DRF Object @@ -26619,15 +27639,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -26652,6 +27676,10 @@ components: - permanent - expiring type: string + description: |- + * `always_require` - Always Require + * `permanent` - Permanent + * `expiring` - Expiring ConsentStageRequest: type: object description: ConsentStage Serializer @@ -26746,6 +27774,10 @@ components: - message - continue type: string + description: |- + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue DenyStage: type: object description: DenyStage Serializer @@ -26759,15 +27791,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -26799,12 +27835,15 @@ components: properties: verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true pk: type: integer @@ -26812,6 +27851,7 @@ components: type: string type: type: string + description: Get type of device readOnly: true confirmed: type: boolean @@ -26863,6 +27903,12 @@ components: - duo - sms type: string + description: |- + * `static` - Static + * `totp` - TOTP + * `webauthn` - WebAuthn + * `duo` - Duo + * `sms` - SMS DigestAlgorithmEnum: enum: - http://www.w3.org/2000/09/xmldsig#sha1 @@ -26870,11 +27916,19 @@ components: - http://www.w3.org/2001/04/xmldsig-more#sha384 - http://www.w3.org/2001/04/xmlenc#sha512 type: string + description: |- + * `http://www.w3.org/2000/09/xmldsig#sha1` - SHA1 + * `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384 + * `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512 DigitsEnum: enum: - 6 - 8 type: integer + description: |- + * `6` - 6 digits, widely compatible + * `8` - 8 digits, not compatible with apps like Google Authenticator DockerServiceConnection: type: object description: DockerServiceConnection Serializer @@ -26895,12 +27949,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true url: type: string @@ -27003,18 +28060,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true result: type: boolean @@ -27070,15 +28132,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -27147,6 +28213,10 @@ components: - waiting - invalid type: string + description: |- + * `success` - Success + * `waiting` - Waiting + * `invalid` - Invalid EmailChallenge: type: object description: Email challenge @@ -27189,15 +28259,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -27406,6 +28480,34 @@ components: - update_available - custom_ type: string + description: |- + * `login` - Login + * `login_failed` - Login Failed + * `logout` - Logout + * `user_write` - User Write + * `suspicious_request` - Suspicious Request + * `password_set` - Password Set + * `secret_view` - Secret View + * `secret_rotate` - Secret Rotate + * `invitation_used` - Invite Used + * `authorize_application` - Authorize Application + * `source_linked` - Source Linked + * `impersonation_started` - Impersonation Started + * `impersonation_ended` - Impersonation Ended + * `flow_execution` - Flow Execution + * `policy_execution` - Policy Execution + * `policy_exception` - Policy Exception + * `property_mapping_exception` - Property Mapping Exception + * `system_task_execution` - System Task Execution + * `system_task_exception` - System Task Exception + * `system_exception` - System Exception + * `configuration_error` - Configuration Error + * `model_created` - Model Created + * `model_updated` - Model Updated + * `model_deleted` - Model Deleted + * `email_sent` - Email Sent + * `update_available` - Update Available + * `custom_` - Custom Prefix EventMatcherPolicy: type: object description: Event Matcher Policy Serializer @@ -27423,24 +28525,57 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true action: allOf: - $ref: '#/components/schemas/EventActions' - description: Match created events with this action type. When left empty, - all action types will be matched. + description: |- + Match created events with this action type. When left empty, all action types will be matched. + + * `login` - Login + * `login_failed` - Login Failed + * `logout` - Logout + * `user_write` - User Write + * `suspicious_request` - Suspicious Request + * `password_set` - Password Set + * `secret_view` - Secret View + * `secret_rotate` - Secret Rotate + * `invitation_used` - Invite Used + * `authorize_application` - Authorize Application + * `source_linked` - Source Linked + * `impersonation_started` - Impersonation Started + * `impersonation_ended` - Impersonation Ended + * `flow_execution` - Flow Execution + * `policy_execution` - Policy Execution + * `policy_exception` - Policy Exception + * `property_mapping_exception` - Property Mapping Exception + * `system_task_execution` - System Task Execution + * `system_task_exception` - System Task Exception + * `system_exception` - System Exception + * `configuration_error` - Configuration Error + * `model_created` - Model Created + * `model_updated` - Model Updated + * `model_deleted` - Model Deleted + * `email_sent` - Email Sent + * `update_available` - Update Available + * `custom_` - Custom Prefix client_ip: type: string description: Matches Event's Client IP (strict matching, for network matching @@ -27448,8 +28583,55 @@ components: app: allOf: - $ref: '#/components/schemas/AppEnum' - description: Match events created by selected application. When left empty, - all applications are matched. + description: |- + Match events created by selected application. When left empty, all applications are matched. + + * `authentik.admin` - authentik Admin + * `authentik.api` - authentik API + * `authentik.crypto` - authentik Crypto + * `authentik.events` - authentik Events + * `authentik.flows` - authentik Flows + * `authentik.lib` - authentik lib + * `authentik.outposts` - authentik Outpost + * `authentik.policies.dummy` - authentik Policies.Dummy + * `authentik.policies.event_matcher` - authentik Policies.Event Matcher + * `authentik.policies.expiry` - authentik Policies.Expiry + * `authentik.policies.expression` - authentik Policies.Expression + * `authentik.policies.password` - authentik Policies.Password + * `authentik.policies.reputation` - authentik Policies.Reputation + * `authentik.policies` - authentik Policies + * `authentik.providers.ldap` - authentik Providers.LDAP + * `authentik.providers.oauth2` - authentik Providers.OAuth2 + * `authentik.providers.proxy` - authentik Providers.Proxy + * `authentik.providers.saml` - authentik Providers.SAML + * `authentik.providers.scim` - authentik Providers.SCIM + * `authentik.recovery` - authentik Recovery + * `authentik.sources.ldap` - authentik Sources.LDAP + * `authentik.sources.oauth` - authentik Sources.OAuth + * `authentik.sources.plex` - authentik Sources.Plex + * `authentik.sources.saml` - authentik Sources.SAML + * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS + * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static + * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP + * `authentik.stages.authenticator_validate` - authentik Stages.Authenticator.Validate + * `authentik.stages.authenticator_webauthn` - authentik Stages.Authenticator.WebAuthn + * `authentik.stages.captcha` - authentik Stages.Captcha + * `authentik.stages.consent` - authentik Stages.Consent + * `authentik.stages.deny` - authentik Stages.Deny + * `authentik.stages.dummy` - authentik Stages.Dummy + * `authentik.stages.email` - authentik Stages.Email + * `authentik.stages.identification` - authentik Stages.Identification + * `authentik.stages.invitation` - authentik Stages.User Invitation + * `authentik.stages.password` - authentik Stages.Password + * `authentik.stages.prompt` - authentik Stages.Prompt + * `authentik.stages.user_delete` - authentik Stages.User Delete + * `authentik.stages.user_login` - authentik Stages.User Login + * `authentik.stages.user_logout` - authentik Stages.User Logout + * `authentik.stages.user_write` - authentik Stages.User Write + * `authentik.tenants` - authentik Tenants + * `authentik.blueprints` - authentik Blueprints + * `authentik.core` - authentik Core required: - bound_to - component @@ -27472,8 +28654,36 @@ components: action: allOf: - $ref: '#/components/schemas/EventActions' - description: Match created events with this action type. When left empty, - all action types will be matched. + description: |- + Match created events with this action type. When left empty, all action types will be matched. + + * `login` - Login + * `login_failed` - Login Failed + * `logout` - Logout + * `user_write` - User Write + * `suspicious_request` - Suspicious Request + * `password_set` - Password Set + * `secret_view` - Secret View + * `secret_rotate` - Secret Rotate + * `invitation_used` - Invite Used + * `authorize_application` - Authorize Application + * `source_linked` - Source Linked + * `impersonation_started` - Impersonation Started + * `impersonation_ended` - Impersonation Ended + * `flow_execution` - Flow Execution + * `policy_execution` - Policy Execution + * `policy_exception` - Policy Exception + * `property_mapping_exception` - Property Mapping Exception + * `system_task_execution` - System Task Execution + * `system_task_exception` - System Task Exception + * `system_exception` - System Exception + * `configuration_error` - Configuration Error + * `model_created` - Model Created + * `model_updated` - Model Updated + * `model_deleted` - Model Deleted + * `email_sent` - Email Sent + * `update_available` - Update Available + * `custom_` - Custom Prefix client_ip: type: string description: Matches Event's Client IP (strict matching, for network matching @@ -27481,8 +28691,55 @@ components: app: allOf: - $ref: '#/components/schemas/AppEnum' - description: Match events created by selected application. When left empty, - all applications are matched. + description: |- + Match events created by selected application. When left empty, all applications are matched. + + * `authentik.admin` - authentik Admin + * `authentik.api` - authentik API + * `authentik.crypto` - authentik Crypto + * `authentik.events` - authentik Events + * `authentik.flows` - authentik Flows + * `authentik.lib` - authentik lib + * `authentik.outposts` - authentik Outpost + * `authentik.policies.dummy` - authentik Policies.Dummy + * `authentik.policies.event_matcher` - authentik Policies.Event Matcher + * `authentik.policies.expiry` - authentik Policies.Expiry + * `authentik.policies.expression` - authentik Policies.Expression + * `authentik.policies.password` - authentik Policies.Password + * `authentik.policies.reputation` - authentik Policies.Reputation + * `authentik.policies` - authentik Policies + * `authentik.providers.ldap` - authentik Providers.LDAP + * `authentik.providers.oauth2` - authentik Providers.OAuth2 + * `authentik.providers.proxy` - authentik Providers.Proxy + * `authentik.providers.saml` - authentik Providers.SAML + * `authentik.providers.scim` - authentik Providers.SCIM + * `authentik.recovery` - authentik Recovery + * `authentik.sources.ldap` - authentik Sources.LDAP + * `authentik.sources.oauth` - authentik Sources.OAuth + * `authentik.sources.plex` - authentik Sources.Plex + * `authentik.sources.saml` - authentik Sources.SAML + * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS + * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static + * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP + * `authentik.stages.authenticator_validate` - authentik Stages.Authenticator.Validate + * `authentik.stages.authenticator_webauthn` - authentik Stages.Authenticator.WebAuthn + * `authentik.stages.captcha` - authentik Stages.Captcha + * `authentik.stages.consent` - authentik Stages.Consent + * `authentik.stages.deny` - authentik Stages.Deny + * `authentik.stages.dummy` - authentik Stages.Dummy + * `authentik.stages.email` - authentik Stages.Email + * `authentik.stages.identification` - authentik Stages.Identification + * `authentik.stages.invitation` - authentik Stages.User Invitation + * `authentik.stages.password` - authentik Stages.Password + * `authentik.stages.prompt` - authentik Stages.Prompt + * `authentik.stages.user_delete` - authentik Stages.User Delete + * `authentik.stages.user_login` - authentik Stages.User Login + * `authentik.stages.user_logout` - authentik Stages.User Logout + * `authentik.stages.user_write` - authentik Stages.User Write + * `authentik.tenants` - authentik Tenants + * `authentik.blueprints` - authentik Blueprints + * `authentik.core` - authentik Core required: - name EventRequest: @@ -27542,6 +28799,7 @@ components: $ref: '#/components/schemas/User' is_expired: type: boolean + description: Check if token is expired yet. readOnly: true expires: type: string @@ -27573,18 +28831,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true expression: type: string @@ -27659,10 +28922,21 @@ components: designation: allOf: - $ref: '#/components/schemas/FlowDesignationEnum' - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration background: type: string + description: |- + Get the URL to the background image. If the name is /static or starts with http + it is returned as-is readOnly: true stages: type: array @@ -27678,6 +28952,7 @@ components: readOnly: true cache_count: type: integer + description: Get count of cached flows readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' @@ -27687,19 +28962,29 @@ components: managers on mobile devices. export_url: type: string + description: Get export URL for flow readOnly: true layout: $ref: '#/components/schemas/LayoutEnum' denied_action: allOf: - $ref: '#/components/schemas/DeniedActionEnum' - description: Configure what should happen when a flow denies access to a - user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue authentication: allOf: - $ref: '#/components/schemas/AuthenticationEnum' - description: Required level of authentication and authorization to access - a flow. + description: |- + Required level of authentication and authorization to access a flow. + + * `none` - None + * `require_authenticated` - Require Authenticated + * `require_unauthenticated` - Require Unauthenticated + * `require_superuser` - Require Superuser required: - background - cache_count @@ -27763,6 +29048,14 @@ components: - recovery - stage_configuration type: string + description: |- + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration FlowDiagram: type: object description: response of the flow's diagram action @@ -27846,9 +29139,11 @@ components: plan_context: type: object additionalProperties: {} + description: Get the plan's context, sanitized readOnly: true session_id: type: string + description: Get a unique session ID readOnly: true required: - current_stage @@ -27875,8 +29170,16 @@ components: designation: allOf: - $ref: '#/components/schemas/FlowDesignationEnum' - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' compatibility_mode: @@ -27888,13 +29191,22 @@ components: denied_action: allOf: - $ref: '#/components/schemas/DeniedActionEnum' - description: Configure what should happen when a flow denies access to a - user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue authentication: allOf: - $ref: '#/components/schemas/AuthenticationEnum' - description: Required level of authentication and authorization to access - a flow. + description: |- + Required level of authentication and authorization to access a flow. + + * `none` - None + * `require_authenticated` - Require Authenticated + * `require_unauthenticated` - Require Unauthenticated + * `require_superuser` - Require Superuser required: - designation - name @@ -27926,10 +29238,21 @@ components: designation: allOf: - $ref: '#/components/schemas/FlowDesignationEnum' - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration background: type: string + description: |- + Get the URL to the background image. If the name is /static or starts with http + it is returned as-is readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' @@ -27939,14 +29262,19 @@ components: managers on mobile devices. export_url: type: string + description: Get export URL for flow readOnly: true layout: $ref: '#/components/schemas/LayoutEnum' denied_action: allOf: - $ref: '#/components/schemas/DeniedActionEnum' - description: Configure what should happen when a flow denies access to a - user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue required: - background - designation @@ -27976,8 +29304,16 @@ components: designation: allOf: - $ref: '#/components/schemas/FlowDesignationEnum' - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' compatibility_mode: @@ -27989,8 +29325,12 @@ components: denied_action: allOf: - $ref: '#/components/schemas/DeniedActionEnum' - description: Configure what should happen when a flow denies access to a - user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue required: - designation - name @@ -28034,10 +29374,12 @@ components: invalid_response_action: allOf: - $ref: '#/components/schemas/InvalidResponseActionEnum' - description: Configure how the flow executor should handle an invalid response - to a challenge. RETRY returns the error message and a similar challenge - to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT - restarts the flow while keeping the current context. + description: |- + Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context. + + * `retry` - Retry + * `restart` - Restart + * `restart_with_context` - Restart With Context required: - order - pk @@ -28070,10 +29412,12 @@ components: invalid_response_action: allOf: - $ref: '#/components/schemas/InvalidResponseActionEnum' - description: Configure how the flow executor should handle an invalid response - to a challenge. RETRY returns the error message and a similar challenge - to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT - restarts the flow while keeping the current context. + description: |- + Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context. + + * `retry` - Retry + * `restart` - Restart + * `restart_with_context` - Restart With Context required: - order - stage @@ -28320,15 +29664,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -28453,12 +29801,21 @@ components: - recovery - app_password type: string + description: |- + * `verification` - Intent Verification + * `api` - Intent Api + * `recovery` - Intent Recovery + * `app_password` - Intent App Password InvalidResponseActionEnum: enum: - retry - restart - restart_with_context type: string + description: |- + * `retry` - Retry + * `restart` - Restart + * `restart_with_context` - Restart With Context Invitation: type: object description: Invitation Serializer @@ -28537,15 +29894,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -28586,6 +29947,9 @@ components: - global - per_provider type: string + description: |- + * `global` - Same identifier is used for all providers + * `per_provider` - Each provider has a different issuer, based on the application slug. KubernetesServiceConnection: type: object description: KubernetesServiceConnection Serializer @@ -28606,12 +29970,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true kubeconfig: type: object @@ -28654,6 +30021,9 @@ components: - direct - cached type: string + description: |- + * `direct` - Direct + * `cached` - Cached LDAPDebug: type: object properties: @@ -28755,15 +30125,19 @@ components: type: string component: type: string + description: Get object's component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true object_field: type: string @@ -28815,6 +30189,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -28823,6 +30198,7 @@ components: format: uuid component: type: string + description: Get object component so that we know how to edit the object readOnly: true assigned_application_slug: type: string @@ -28834,12 +30210,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true base_dn: type: string @@ -28883,7 +30262,6 @@ components: required: - assigned_application_name - assigned_application_slug - - authorization_flow - component - meta_model_name - name @@ -28901,6 +30279,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -28943,7 +30322,6 @@ components: bind_mode: $ref: '#/components/schemas/LDAPAPIAccessMode' required: - - authorization_flow - name LDAPSource: type: object @@ -28976,23 +30354,33 @@ components: description: Flow to use when enrolling new users. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. managed: type: string nullable: true @@ -29007,6 +30395,9 @@ components: icon: type: string nullable: true + description: |- + Get the URL to the Icon. If the name is /static or + starts with http it is returned as-is readOnly: true server_uri: type: string @@ -29110,8 +30501,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -29196,6 +30593,12 @@ components: - sidebar_left - sidebar_right type: string + description: |- + * `stacked` - STACKED + * `content_left` - CONTENT_LEFT + * `content_right` - CONTENT_RIGHT + * `sidebar_left` - SIDEBAR_LEFT + * `sidebar_right` - SIDEBAR_RIGHT Link: type: object description: Returns a single link @@ -29272,12 +30675,22 @@ components: - urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName - urn:oasis:names:tc:SAML:2.0:nameid-format:transient type: string + description: |- + * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` - Email + * `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - Persistent + * `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509 + * `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows + * `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient NotConfiguredActionEnum: enum: - skip - deny - configure type: string + description: |- + * `skip` - Skip + * `deny` - Deny + * `configure` - Configure Notification: type: object description: Notification Serializer @@ -29337,8 +30750,12 @@ components: severity: allOf: - $ref: '#/components/schemas/SeverityEnum' - description: Controls which severity level the created notifications will - have. + description: |- + Controls which severity level the created notifications will have. + + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert group: type: string format: uuid @@ -29371,8 +30788,12 @@ components: severity: allOf: - $ref: '#/components/schemas/SeverityEnum' - description: Controls which severity level the created notifications will - have. + description: |- + Controls which severity level the created notifications will have. + + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert group: type: string format: uuid @@ -29396,6 +30817,7 @@ components: $ref: '#/components/schemas/NotificationTransportModeEnum' mode_verbose: type: string + description: Return selected mode with a UI Label readOnly: true webhook_url: type: string @@ -29419,6 +30841,11 @@ components: - webhook_slack - email type: string + description: |- + * `local` - authentik inbuilt notifications + * `webhook` - Generic Webhook + * `webhook_slack` - Slack Webhook (Slack/Discord) + * `email` - Email NotificationTransportRequest: type: object description: NotificationTransport Serializer @@ -29494,6 +30921,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -29502,6 +30930,7 @@ components: format: uuid component: type: string + description: Get object component so that we know how to edit the object readOnly: true assigned_application_slug: type: string @@ -29513,18 +30942,24 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true client_type: allOf: - $ref: '#/components/schemas/ClientTypeEnum' - description: Confidential clients are capable of maintaining the confidentiality - of their credentials. Public clients are incapable + description: |- + Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + + * `confidential` - Confidential + * `public` - Public client_id: type: string maxLength: 255 @@ -29559,12 +30994,22 @@ components: sub_mode: allOf: - $ref: '#/components/schemas/SubModeEnum' - description: Configure what data should be used as unique User Identifier. - For most cases, the default should be fine. + description: |- + Configure what data should be used as unique User Identifier. For most cases, the default should be fine. + + * `hashed_user_id` - Based on the Hashed User ID + * `user_id` - Based on user ID + * `user_username` - Based on the username + * `user_email` - Based on the User's Email. This is recommended over the UPN method. + * `user_upn` - Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains. issuer_mode: allOf: - $ref: '#/components/schemas/IssuerModeEnum' - description: Configure how the issuer field of the ID Token should be filled. + description: |- + Configure how the issuer field of the ID Token should be filled. + + * `global` - Same identifier is used for all providers + * `per_provider` - Each provider has a different issuer, based on the application slug. jwks_sources: type: array items: @@ -29576,7 +31021,6 @@ components: required: - assigned_application_name - assigned_application_slug - - authorization_flow - component - meta_model_name - name @@ -29593,6 +31037,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -29602,8 +31047,11 @@ components: client_type: allOf: - $ref: '#/components/schemas/ClientTypeEnum' - description: Confidential clients are capable of maintaining the confidentiality - of their credentials. Public clients are incapable + description: |- + Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + + * `confidential` - Confidential + * `public` - Public client_id: type: string minLength: 1 @@ -29642,12 +31090,22 @@ components: sub_mode: allOf: - $ref: '#/components/schemas/SubModeEnum' - description: Configure what data should be used as unique User Identifier. - For most cases, the default should be fine. + description: |- + Configure what data should be used as unique User Identifier. For most cases, the default should be fine. + + * `hashed_user_id` - Based on the Hashed User ID + * `user_id` - Based on user ID + * `user_username` - Based on the username + * `user_email` - Based on the User's Email. This is recommended over the UPN method. + * `user_upn` - Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains. issuer_mode: allOf: - $ref: '#/components/schemas/IssuerModeEnum' - description: Configure how the issuer field of the ID Token should be filled. + description: |- + Configure how the issuer field of the ID Token should be filled. + + * `global` - Same identifier is used for all providers + * `per_provider` - Each provider has a different issuer, based on the application slug. jwks_sources: type: array items: @@ -29657,7 +31115,6 @@ components: authenticate. title: Any JWT signed by the JWK of the selected source can be used to authenticate. required: - - authorization_flow - name OAuth2ProviderSetupURLs: type: object @@ -29781,23 +31238,33 @@ components: description: Flow to use when enrolling new users. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. managed: type: string nullable: true @@ -29812,6 +31279,9 @@ components: icon: type: string nullable: true + description: |- + Get the URL to the Icon. If the name is /static or + starts with http it is returned as-is readOnly: true provider_type: $ref: '#/components/schemas/ProviderTypeEnum' @@ -29840,6 +31310,7 @@ components: type: string callback_url: type: string + description: Get OAuth Callback URL readOnly: true additional_scopes: type: string @@ -29899,8 +31370,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -30033,6 +31510,7 @@ components: readOnly: true token_identifier: type: string + description: Get Token identifier readOnly: true config: type: object @@ -30143,6 +31621,9 @@ components: - proxy - ldap type: string + description: |- + * `proxy` - Proxy + * `ldap` - Ldap PaginatedApplicationList: type: object properties: @@ -32173,6 +33654,76 @@ components: required: - pagination - results + PaginatedSCIMMappingList: + type: object + properties: + pagination: + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + results: + type: array + items: + $ref: '#/components/schemas/SCIMMapping' + required: + - pagination + - results + PaginatedSCIMProviderList: + type: object + properties: + pagination: + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + results: + type: array + items: + $ref: '#/components/schemas/SCIMProvider' + required: + - pagination + - results PaginatedSMSDeviceList: type: object properties: @@ -32930,18 +34481,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true days: type: integer @@ -32995,18 +34551,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true password_field: type: string @@ -33134,15 +34695,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -33411,7 +34976,12 @@ components: webauthn_user_verification: allOf: - $ref: '#/components/schemas/UserVerificationEnum' - description: Enforce user verification for WebAuthn devices. + description: |- + Enforce user verification for WebAuthn devices. + + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged PatchedBlueprintInstanceRequest: type: object description: Info about a single blueprint instance file @@ -33650,8 +35220,36 @@ components: action: allOf: - $ref: '#/components/schemas/EventActions' - description: Match created events with this action type. When left empty, - all action types will be matched. + description: |- + Match created events with this action type. When left empty, all action types will be matched. + + * `login` - Login + * `login_failed` - Login Failed + * `logout` - Logout + * `user_write` - User Write + * `suspicious_request` - Suspicious Request + * `password_set` - Password Set + * `secret_view` - Secret View + * `secret_rotate` - Secret Rotate + * `invitation_used` - Invite Used + * `authorize_application` - Authorize Application + * `source_linked` - Source Linked + * `impersonation_started` - Impersonation Started + * `impersonation_ended` - Impersonation Ended + * `flow_execution` - Flow Execution + * `policy_execution` - Policy Execution + * `policy_exception` - Policy Exception + * `property_mapping_exception` - Property Mapping Exception + * `system_task_execution` - System Task Execution + * `system_task_exception` - System Task Exception + * `system_exception` - System Exception + * `configuration_error` - Configuration Error + * `model_created` - Model Created + * `model_updated` - Model Updated + * `model_deleted` - Model Deleted + * `email_sent` - Email Sent + * `update_available` - Update Available + * `custom_` - Custom Prefix client_ip: type: string description: Matches Event's Client IP (strict matching, for network matching @@ -33659,8 +35257,55 @@ components: app: allOf: - $ref: '#/components/schemas/AppEnum' - description: Match events created by selected application. When left empty, - all applications are matched. + description: |- + Match events created by selected application. When left empty, all applications are matched. + + * `authentik.admin` - authentik Admin + * `authentik.api` - authentik API + * `authentik.crypto` - authentik Crypto + * `authentik.events` - authentik Events + * `authentik.flows` - authentik Flows + * `authentik.lib` - authentik lib + * `authentik.outposts` - authentik Outpost + * `authentik.policies.dummy` - authentik Policies.Dummy + * `authentik.policies.event_matcher` - authentik Policies.Event Matcher + * `authentik.policies.expiry` - authentik Policies.Expiry + * `authentik.policies.expression` - authentik Policies.Expression + * `authentik.policies.password` - authentik Policies.Password + * `authentik.policies.reputation` - authentik Policies.Reputation + * `authentik.policies` - authentik Policies + * `authentik.providers.ldap` - authentik Providers.LDAP + * `authentik.providers.oauth2` - authentik Providers.OAuth2 + * `authentik.providers.proxy` - authentik Providers.Proxy + * `authentik.providers.saml` - authentik Providers.SAML + * `authentik.providers.scim` - authentik Providers.SCIM + * `authentik.recovery` - authentik Recovery + * `authentik.sources.ldap` - authentik Sources.LDAP + * `authentik.sources.oauth` - authentik Sources.OAuth + * `authentik.sources.plex` - authentik Sources.Plex + * `authentik.sources.saml` - authentik Sources.SAML + * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS + * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static + * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP + * `authentik.stages.authenticator_validate` - authentik Stages.Authenticator.Validate + * `authentik.stages.authenticator_webauthn` - authentik Stages.Authenticator.WebAuthn + * `authentik.stages.captcha` - authentik Stages.Captcha + * `authentik.stages.consent` - authentik Stages.Consent + * `authentik.stages.deny` - authentik Stages.Deny + * `authentik.stages.dummy` - authentik Stages.Dummy + * `authentik.stages.email` - authentik Stages.Email + * `authentik.stages.identification` - authentik Stages.Identification + * `authentik.stages.invitation` - authentik Stages.User Invitation + * `authentik.stages.password` - authentik Stages.Password + * `authentik.stages.prompt` - authentik Stages.Prompt + * `authentik.stages.user_delete` - authentik Stages.User Delete + * `authentik.stages.user_login` - authentik Stages.User Login + * `authentik.stages.user_logout` - authentik Stages.User Logout + * `authentik.stages.user_write` - authentik Stages.User Write + * `authentik.tenants` - authentik Tenants + * `authentik.blueprints` - authentik Blueprints + * `authentik.core` - authentik Core PatchedEventRequest: type: object description: Event Serializer @@ -33720,8 +35365,16 @@ components: designation: allOf: - $ref: '#/components/schemas/FlowDesignationEnum' - description: Decides what this Flow is used for. For example, the Authentication - flow is redirect to when an un-authenticated user visits authentik. + description: |- + Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik. + + * `authentication` - Authentication + * `authorization` - Authorization + * `invalidation` - Invalidation + * `enrollment` - Enrollment + * `unenrollment` - Unrenollment + * `recovery` - Recovery + * `stage_configuration` - Stage Configuration policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' compatibility_mode: @@ -33733,13 +35386,22 @@ components: denied_action: allOf: - $ref: '#/components/schemas/DeniedActionEnum' - description: Configure what should happen when a flow denies access to a - user. + description: |- + Configure what should happen when a flow denies access to a user. + + * `message_continue` - Message Continue + * `message` - Message + * `continue` - Continue authentication: allOf: - $ref: '#/components/schemas/AuthenticationEnum' - description: Required level of authentication and authorization to access - a flow. + description: |- + Required level of authentication and authorization to access a flow. + + * `none` - None + * `require_authenticated` - Require Authenticated + * `require_unauthenticated` - Require Unauthenticated + * `require_superuser` - Require Superuser PatchedFlowStageBindingRequest: type: object description: FlowStageBinding Serializer @@ -33765,10 +35427,12 @@ components: invalid_response_action: allOf: - $ref: '#/components/schemas/InvalidResponseActionEnum' - description: Configure how the flow executor should handle an invalid response - to a challenge. RETRY returns the error message and a similar challenge - to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT - restarts the flow while keeping the current context. + description: |- + Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context. + + * `retry` - Retry + * `restart` - Restart + * `restart_with_context` - Restart With Context PatchedGroupRequest: type: object description: Group Serializer @@ -33938,6 +35602,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -34010,8 +35675,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -34109,8 +35780,12 @@ components: severity: allOf: - $ref: '#/components/schemas/SeverityEnum' - description: Controls which severity level the created notifications will - have. + description: |- + Controls which severity level the created notifications will have. + + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert group: type: string format: uuid @@ -34157,6 +35832,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -34166,8 +35842,11 @@ components: client_type: allOf: - $ref: '#/components/schemas/ClientTypeEnum' - description: Confidential clients are capable of maintaining the confidentiality - of their credentials. Public clients are incapable + description: |- + Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + + * `confidential` - Confidential + * `public` - Public client_id: type: string minLength: 1 @@ -34206,12 +35885,22 @@ components: sub_mode: allOf: - $ref: '#/components/schemas/SubModeEnum' - description: Configure what data should be used as unique User Identifier. - For most cases, the default should be fine. + description: |- + Configure what data should be used as unique User Identifier. For most cases, the default should be fine. + + * `hashed_user_id` - Based on the Hashed User ID + * `user_id` - Based on user ID + * `user_username` - Based on the username + * `user_email` - Based on the User's Email. This is recommended over the UPN method. + * `user_upn` - Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains. issuer_mode: allOf: - $ref: '#/components/schemas/IssuerModeEnum' - description: Configure how the issuer field of the ID Token should be filled. + description: |- + Configure how the issuer field of the ID Token should be filled. + + * `global` - Same identifier is used for all providers + * `per_provider` - Each provider has a different issuer, based on the application slug. jwks_sources: type: array items: @@ -34251,8 +35940,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -34474,8 +36169,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -34592,6 +36293,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -34634,8 +36336,12 @@ components: mode: allOf: - $ref: '#/components/schemas/ProxyMode' - description: Enable support for forwardAuth in traefik and nginx auth_request. - Exclusive with internal_host. + description: |- + Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host. + + * `proxy` - Proxy + * `forward_single` - Forward Single + * `forward_domain` - Forward Domain intercept_header_auth: type: boolean description: When enabled, this provider will intercept the authorization @@ -34714,6 +36420,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -34777,8 +36484,11 @@ components: allOf: - $ref: '#/components/schemas/SpBindingEnum' title: Service Provider Binding - description: This determines how authentik sends the response back to the - Service Provider. + description: |- + This determines how authentik sends the response back to the Service Provider. + + * `redirect` - Redirect + * `post` - Post PatchedSAMLSourceRequest: type: object description: SAMLSource Serializer @@ -34810,8 +36520,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -34841,8 +36557,14 @@ components: name_id_policy: allOf: - $ref: '#/components/schemas/NameIdPolicyEnum' - description: NameID Policy sent to the IdP. Can be unset, in which case - no Policy is sent. + description: |- + NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent. + + * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` - Email + * `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - Persistent + * `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509 + * `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows + * `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient binding_type: $ref: '#/components/schemas/BindingTypeEnum' signing_kp: @@ -34863,6 +36585,51 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' + PatchedSCIMMappingRequest: + type: object + description: SCIMMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + 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. + name: + type: string + minLength: 1 + expression: + type: string + minLength: 1 + PatchedSCIMProviderRequest: + type: object + description: SCIMProvider Serializer + properties: + name: + type: string + minLength: 1 + property_mappings: + type: array + items: + type: string + format: uuid + property_mappings_group: + type: array + items: + type: string + format: uuid + description: Property mappings used for group creation/updating. + url: + type: string + minLength: 1 + description: Base URL to SCIM requests, usually ends in /v2 + token: + type: string + minLength: 1 + description: Authentication token PatchedSMSDeviceRequest: type: object description: Serializer for sms authenticator devices @@ -35208,23 +36975,33 @@ components: description: Flow to use when enrolling new users. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. managed: type: string nullable: true @@ -35239,6 +37016,9 @@ components: icon: type: string nullable: true + description: |- + Get the URL to the Icon. If the name is /static or + starts with http it is returned as-is readOnly: true client_id: type: string @@ -35335,8 +37115,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -35388,18 +37174,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true required: - bound_to @@ -35505,6 +37296,9 @@ components: - all - any type: string + description: |- + * `all` - ALL, all policies must pass + * `any` - ANY, any policy must pass PolicyRequest: type: object description: Policy Serializer @@ -35675,15 +37469,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -35748,6 +37546,21 @@ components: - static - ak-locale type: string + description: |- + * `text` - Text: Simple Text input + * `text_read_only` - Text (read-only): Simple Text input, but cannot be edited. + * `username` - Username: Same as Text input, but checks for and prevents duplicate usernames. + * `email` - Email: Text field with Email type. + * `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical. + * `number` - Number + * `checkbox` - Checkbox + * `date` - Date + * `date-time` - Date Time + * `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI + * `separator` - Separator: Static Separator Line + * `hidden` - Hidden: Hidden field, can be used to insert data into form. + * `static` - Static: Static value, displayed as-is. + * `ak-locale` - authentik: Selection of locales authentik supports PropertyMapping: type: object description: PropertyMapping Serializer @@ -35771,15 +37584,19 @@ components: type: string component: type: string + description: Get object's component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true required: - component @@ -35834,6 +37651,7 @@ components: format: uuid component: type: string + description: Get object component so that we know how to edit the object readOnly: true assigned_application_slug: type: string @@ -35845,12 +37663,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true required: - assigned_application_name @@ -35867,6 +37688,9 @@ components: - twilio - generic type: string + description: |- + * `twilio` - Twilio + * `generic` - Generic ProviderRequest: type: object description: Provider Serializer @@ -35901,12 +37725,29 @@ components: - mailcow - twitch type: string + description: |- + * `apple` - Apple + * `azuread` - Azure AD + * `discord` - Discord + * `facebook` - Facebook + * `github` - GitHub + * `google` - Google + * `openidconnect` - OpenID Connect + * `okta` - Okta + * `reddit` - reddit + * `twitter` - Twitter + * `mailcow` - Mailcow + * `twitch` - Twitch ProxyMode: enum: - proxy - forward_single - forward_domain type: string + description: |- + * `proxy` - Proxy + * `forward_single` - Forward Single + * `forward_domain` - Forward Domain ProxyOutpostConfig: type: object description: Proxy provider serializer for outposts @@ -35964,14 +37805,19 @@ components: mode: allOf: - $ref: '#/components/schemas/ProxyMode' - description: Enable support for forwardAuth in traefik and nginx auth_request. - Exclusive with internal_host. + description: |- + Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host. + + * `proxy` - Proxy + * `forward_single` - Forward Single + * `forward_domain` - Forward Domain cookie_domain: type: string access_token_validity: type: number format: double nullable: true + description: Get token validity as second count readOnly: true intercept_header_auth: type: boolean @@ -35981,6 +37827,9 @@ components: type: array items: type: string + description: |- + Get all the scope names the outpost should request, + including custom-defined ones readOnly: true assigned_application_slug: type: string @@ -36012,6 +37861,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -36020,6 +37870,7 @@ components: format: uuid component: type: string + description: Get object component so that we know how to edit the object readOnly: true assigned_application_slug: type: string @@ -36031,12 +37882,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true client_id: type: string @@ -36076,8 +37930,12 @@ components: mode: allOf: - $ref: '#/components/schemas/ProxyMode' - description: Enable support for forwardAuth in traefik and nginx auth_request. - Exclusive with internal_host. + description: |- + Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host. + + * `proxy` - Proxy + * `forward_single` - Forward Single + * `forward_domain` - Forward Domain intercept_header_auth: type: boolean description: When enabled, this provider will intercept the authorization @@ -36111,7 +37969,6 @@ components: required: - assigned_application_name - assigned_application_slug - - authorization_flow - client_id - component - external_host @@ -36132,6 +37989,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -36174,8 +38032,12 @@ components: mode: allOf: - $ref: '#/components/schemas/ProxyMode' - description: Enable support for forwardAuth in traefik and nginx auth_request. - Exclusive with internal_host. + description: |- + Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host. + + * `proxy` - Proxy + * `forward_single` - Forward Single + * `forward_domain` - Forward Domain intercept_header_auth: type: boolean description: When enabled, this provider will intercept the authorization @@ -36201,7 +38063,6 @@ components: description: 'Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3).' required: - - authorization_flow - external_host - name RedirectChallenge: @@ -36271,18 +38132,23 @@ components: will be logged. By default, only execution errors are logged. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true bound_to: type: integer + description: Return objects policy is bound to readOnly: true check_ip: type: boolean @@ -36327,6 +38193,10 @@ components: - preferred - required type: string + description: |- + * `discouraged` - Discouraged + * `preferred` - Preferred + * `required` - Required SAMLMetadata: type: object description: SAML Provider Metadata serializer @@ -36363,15 +38233,19 @@ components: type: string component: type: string + description: Get object's component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true saml_name: type: string @@ -36429,6 +38303,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -36437,6 +38312,7 @@ components: format: uuid component: type: string + description: Get object component so that we know how to edit the object readOnly: true assigned_application_slug: type: string @@ -36448,12 +38324,15 @@ components: readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true acs_url: type: string @@ -36507,31 +38386,39 @@ components: allOf: - $ref: '#/components/schemas/SpBindingEnum' title: Service Provider Binding - description: This determines how authentik sends the response back to the - Service Provider. + description: |- + This determines how authentik sends the response back to the Service Provider. + + * `redirect` - Redirect + * `post` - Post url_download_metadata: type: string + description: Get metadata download URL readOnly: true url_sso_post: type: string + description: Get SSO Post URL readOnly: true url_sso_redirect: type: string + description: Get SSO Redirect URL readOnly: true url_sso_init: type: string + description: Get SSO IDP-Initiated URL readOnly: true url_slo_post: type: string + description: Get SLO POST URL readOnly: true url_slo_redirect: type: string + description: Get SLO redirect URL readOnly: true required: - acs_url - assigned_application_name - assigned_application_slug - - authorization_flow - component - meta_model_name - name @@ -36572,6 +38459,7 @@ components: authorization_flow: type: string format: uuid + nullable: true description: Flow used when authorizing this provider. property_mappings: type: array @@ -36635,11 +38523,13 @@ components: allOf: - $ref: '#/components/schemas/SpBindingEnum' title: Service Provider Binding - description: This determines how authentik sends the response back to the - Service Provider. + description: |- + This determines how authentik sends the response back to the Service Provider. + + * `redirect` - Redirect + * `post` - Post required: - acs_url - - authorization_flow - name SAMLSource: type: object @@ -36672,23 +38562,33 @@ components: description: Flow to use when enrolling new users. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. managed: type: string nullable: true @@ -36703,6 +38603,9 @@ components: icon: type: string nullable: true + description: |- + Get the URL to the Icon. If the name is /static or + starts with http it is returned as-is readOnly: true pre_authentication_flow: type: string @@ -36729,8 +38632,14 @@ components: name_id_policy: allOf: - $ref: '#/components/schemas/NameIdPolicyEnum' - description: NameID Policy sent to the IdP. Can be unset, in which case - no Policy is sent. + description: |- + NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent. + + * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` - Email + * `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - Persistent + * `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509 + * `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows + * `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient binding_type: $ref: '#/components/schemas/BindingTypeEnum' signing_kp: @@ -36793,8 +38702,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -36824,8 +38739,14 @@ components: name_id_policy: allOf: - $ref: '#/components/schemas/NameIdPolicyEnum' - description: NameID Policy sent to the IdP. Can be unset, in which case - no Policy is sent. + description: |- + NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent. + + * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` - Email + * `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - Persistent + * `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509 + * `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows + * `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient binding_type: $ref: '#/components/schemas/BindingTypeEnum' signing_kp: @@ -36851,6 +38772,165 @@ components: - pre_authentication_flow - slug - sso_url + SCIMMapping: + type: object + description: SCIMMapping Serializer + properties: + pk: + type: string + format: uuid + readOnly: true + title: Pm uuid + managed: + type: string + nullable: true + 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. + name: + type: string + expression: + type: string + component: + type: string + description: Get object's component so that we know how to edit the object + readOnly: true + verbose_name: + type: string + description: Return object's verbose_name + readOnly: true + verbose_name_plural: + type: string + description: Return object's plural verbose_name + readOnly: true + meta_model_name: + type: string + description: Return internal model name + readOnly: true + required: + - component + - expression + - meta_model_name + - name + - pk + - verbose_name + - verbose_name_plural + SCIMMappingRequest: + type: object + description: SCIMMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + 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. + name: + type: string + minLength: 1 + expression: + type: string + minLength: 1 + required: + - expression + - name + SCIMProvider: + type: object + description: SCIMProvider Serializer + properties: + pk: + type: integer + readOnly: true + title: ID + name: + type: string + property_mappings: + type: array + items: + type: string + format: uuid + property_mappings_group: + type: array + items: + type: string + format: uuid + description: Property mappings used for group creation/updating. + component: + type: string + description: Get object component so that we know how to edit the object + readOnly: true + assigned_application_slug: + type: string + description: Internal application name, used in URLs. + readOnly: true + assigned_application_name: + type: string + description: Application's display Name. + readOnly: true + verbose_name: + type: string + description: Return object's verbose_name + readOnly: true + verbose_name_plural: + type: string + description: Return object's plural verbose_name + readOnly: true + meta_model_name: + type: string + description: Return internal model name + readOnly: true + url: + type: string + description: Base URL to SCIM requests, usually ends in /v2 + token: + type: string + description: Authentication token + required: + - assigned_application_name + - assigned_application_slug + - component + - meta_model_name + - name + - pk + - token + - url + - verbose_name + - verbose_name_plural + SCIMProviderRequest: + type: object + description: SCIMProvider Serializer + properties: + name: + type: string + minLength: 1 + property_mappings: + type: array + items: + type: string + format: uuid + property_mappings_group: + type: array + items: + type: string + format: uuid + description: Property mappings used for group creation/updating. + url: + type: string + minLength: 1 + description: Base URL to SCIM requests, usually ends in /v2 + token: + type: string + minLength: 1 + description: Authentication token + required: + - name + - token + - url SMSDevice: type: object description: Serializer for sms authenticator devices @@ -36904,15 +38984,19 @@ components: type: string component: type: string + description: Get object's component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true scope_name: type: string @@ -36996,15 +39080,19 @@ components: Integration component: type: string + description: Return component used to edit this object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true required: - component @@ -37057,6 +39145,10 @@ components: - warning - alert type: string + description: |- + * `notice` - Notice + * `warning` - Warning + * `alert` - Alert ShellChallenge: type: object description: challenge type to render HTML as-is @@ -37087,6 +39179,12 @@ components: - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 - http://www.w3.org/2000/09/xmldsig#dsa-sha1 type: string + description: |- + * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - RSA-SHA1 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` - RSA-SHA256 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` - RSA-SHA384 + * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` - RSA-SHA512 + * `http://www.w3.org/2000/09/xmldsig#dsa-sha1` - DSA-SHA1 Source: type: object description: Source Serializer @@ -37118,23 +39216,33 @@ components: description: Flow to use when enrolling new users. component: type: string + description: Get object component so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true policy_engine_mode: $ref: '#/components/schemas/PolicyEngineMode' user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. managed: type: string nullable: true @@ -37149,6 +39257,9 @@ components: icon: type: string nullable: true + description: |- + Get the URL to the Icon. If the name is /static or + starts with http it is returned as-is readOnly: true required: - component @@ -37191,8 +39302,14 @@ components: user_matching_mode: allOf: - $ref: '#/components/schemas/UserMatchingModeEnum' - description: How the source determines if an existing user should be authenticated - or a new user enrolled. + description: |- + How the source determines if an existing user should be authenticated or a new user enrolled. + + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. user_path_template: type: string minLength: 1 @@ -37238,6 +39355,9 @@ components: - redirect - post type: string + description: |- + * `redirect` - Redirect + * `post` - Post Stage: type: object description: Stage Serializer @@ -37251,15 +39371,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -37370,6 +39494,12 @@ components: - user_email - user_upn type: string + description: |- + * `hashed_user_id` - Based on the Hashed User ID + * `user_id` - Based on user ID + * `user_username` - Based on the username + * `user_email` - Based on the User's Email. This is recommended over the UPN method. + * `user_upn` - Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains. System: type: object description: Get system information. @@ -37378,21 +39508,25 @@ components: type: object additionalProperties: type: string + description: Get Environment readOnly: true http_headers: type: object additionalProperties: type: string + description: Get HTTP Request headers readOnly: true http_host: type: string + description: Get HTTP host readOnly: true http_is_secure: type: boolean + description: Get HTTP Secure flag readOnly: true runtime: type: object - description: Runtime information + description: Get versions properties: python_version: type: string @@ -37416,13 +39550,16 @@ components: readOnly: true tenant: type: string + description: Currently active tenant readOnly: true server_time: type: string format: date-time + description: Current server time readOnly: true embedded_outpost_host: type: string + description: Get the FQDN configured on the embedded outpost readOnly: true required: - embedded_outpost_host @@ -37472,6 +39609,7 @@ components: format: date-time task_duration: type: integer + description: Get the duration a task took to run readOnly: true status: $ref: '#/components/schemas/TaskStatusEnum' @@ -37492,6 +39630,11 @@ components: - ERROR - UNKNOWN type: string + description: |- + * `SUCCESSFUL` - SUCCESSFUL + * `WARNING` - WARNING + * `ERROR` - ERROR + * `UNKNOWN` - UNKNOWN Tenant: type: object description: Tenant Serializer @@ -37662,6 +39805,7 @@ components: $ref: '#/components/schemas/User' is_expired: type: boolean + description: Check if token is expired yet. readOnly: true expires: type: string @@ -37672,6 +39816,7 @@ components: type: string id_token: type: string + description: Get the token's id_token as JSON String readOnly: true revoked: type: boolean @@ -37774,6 +39919,11 @@ components: - SET_NULL - SET_DEFAULT type: string + description: |- + * `CASCADE` - CASCADE + * `CASCADE_MANY` - CASCADE_MANY + * `SET_NULL` - SET_NULL + * `SET_DEFAULT` - SET_DEFAULT User: type: object description: User Serializer @@ -37871,6 +40021,10 @@ components: - create_when_required - always_create type: string + description: |- + * `never_create` - Never Create + * `create_when_required` - Create When Required + * `always_create` - Always Create UserDeleteStage: type: object description: UserDeleteStage Serializer @@ -37884,15 +40038,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -37924,6 +40082,10 @@ components: - username - upn type: string + description: |- + * `email` - E Mail + * `username` - Username + * `upn` - Upn UserGroup: type: object description: Simplified Group Serializer for user's groups @@ -37935,6 +40097,7 @@ components: title: Group uuid num_pk: type: integer + description: Get a numerical, int32 ID for the group readOnly: true name: type: string @@ -37990,15 +40153,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -38052,15 +40219,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -38094,6 +40265,12 @@ components: - username_link - username_deny type: string + description: |- + * `identifier` - Use the source-specific identifier + * `email_link` - Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses. + * `email_deny` - Use the user's email address, but deny enrollment when the email address already exists. + * `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source. + * `username_deny` - Use the user's username, but deny enrollment when the username already exists. UserMetrics: type: object description: User Metrics @@ -38292,6 +40469,7 @@ components: settings: type: object additionalProperties: {} + description: Get user settings with tenant and group settings applied readOnly: true required: - avatar @@ -38399,6 +40577,10 @@ components: - preferred - discouraged type: string + description: |- + * `required` - Required + * `preferred` - Preferred + * `discouraged` - Discouraged UserWriteStage: type: object description: UserWriteStage Serializer @@ -38412,15 +40594,19 @@ components: type: string component: type: string + description: Get object type so that we know how to edit the object readOnly: true verbose_name: type: string + description: Return object's verbose_name readOnly: true verbose_name_plural: type: string + description: Return object's plural verbose_name readOnly: true meta_model_name: type: string + description: Return internal model name readOnly: true flow_set: type: array @@ -38487,15 +40673,19 @@ components: properties: version_current: type: string + description: Get current version readOnly: true version_latest: type: string + description: Get latest version from cache readOnly: true build_hash: type: string + description: Get build hash, if version is not latest or released readOnly: true outdated: type: boolean + description: Check if we're running the latest version readOnly: true required: - build_hash diff --git a/xml/saml-schema-assertion-2.0.xsd b/schemas/saml-schema-assertion-2.0.xsd similarity index 100% rename from xml/saml-schema-assertion-2.0.xsd rename to schemas/saml-schema-assertion-2.0.xsd diff --git a/xml/saml-schema-metadata-2.0.xsd b/schemas/saml-schema-metadata-2.0.xsd similarity index 100% rename from xml/saml-schema-metadata-2.0.xsd rename to schemas/saml-schema-metadata-2.0.xsd diff --git a/xml/saml-schema-protocol-2.0.xsd b/schemas/saml-schema-protocol-2.0.xsd similarity index 100% rename from xml/saml-schema-protocol-2.0.xsd rename to schemas/saml-schema-protocol-2.0.xsd diff --git a/schemas/scim-enterpriseUser.schema.json b/schemas/scim-enterpriseUser.schema.json new file mode 100644 index 000000000..626d1907b --- /dev/null +++ b/schemas/scim-enterpriseUser.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + "title": "EnterpriseUser", + "description": "Enterprise User", + "properties": { + "employeeNumber": { + "description": "Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with anorganization.", + "type": "string" + }, + "costCenter": { + "description": "Identifies the name of a cost center.", + "type": "string" + }, + "organization": { + "description": "Identifies the name of an organization.", + "type": "string" + }, + "division": { + "description": "Identifies the name of a division.", + "type": "string" + }, + "department": { + "description": "Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with anorganization.", + "type": "string" + }, + "manager": { + "description": "The User's manager. A complex type that optionally allows service providers to represent organizational hierarchy by referencing the 'id' attribute of another User.", + "type": "object", + "properties": { + "value": { + "description": "The id of the SCIM resource representingthe User's manager. REQUIRED.", + "type": "string" + }, + "$ref": { + "description": "The URI of the SCIM resource representing the User's manager. REQUIRED.", + "type": "string", + "format": "uri" + }, + "displayName": { + "description": "The displayName of the User's manager. OPTIONAL and READ-ONLY.", + "type": "string", + "readOnly": true + } + }, + "required": [] + } + } +} diff --git a/schemas/scim-group.schema.json b/schemas/scim-group.schema.json new file mode 100644 index 000000000..a1fe12bfa --- /dev/null +++ b/schemas/scim-group.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:ietf:params:scim:schemas:core:2.0:Group", + "title": "Group", + "description": "Group", + "properties": { + "displayName": { + "description": "A human-readable name for the Group. REQUIRED.", + "type": "string" + }, + "members": { + "description": "A list of members of the Group.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "Identifier of the member of this Group.", + "type": "string", + "readOnly": true + }, + "$ref": { + "description": "The URI corresponding to a SCIM resource that is a member of this Group.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "type": { + "description": "A label indicating the type of resource, e.g., 'User' or 'Group'.", + "type": "string", + "enum": [ + "User", + "Group" + ], + "readOnly": true + } + }, + "readOnly": true + } + } + }, + "required": [ + "displayName" + ] +} diff --git a/schemas/scim-resourceType.schema.json b/schemas/scim-resourceType.schema.json new file mode 100644 index 000000000..b04aaf7e0 --- /dev/null +++ b/schemas/scim-resourceType.schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", + "title": "ResourceType", + "description": "Specifies the schema that describes a SCIM resource type", + "type": "object", + "properties": { + "id": { + "description": "The resource type's server unique id. May be the same as the 'name' attribute.", + "type": "string", + "readOnly": true + }, + "name": { + "description": "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'.", + "type": "string", + "readOnly": true + }, + "description": { + "description": "The resource type's human-readable description. When applicable, service providers MUST specify the description.", + "type": "string", + "readOnly": true + }, + "endpoint": { + "description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.", + "type": "string", + "format": "uri-reference", + "readOnly": true + }, + "schema": { + "description": "The resource type's primary/base schema URI.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "schemaExtensions": { + "description": "A list of URIs of the resource type's schema extensions.", + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "schema": { + "description": "The URI of a schema extension.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "required": { + "description": "A Boolean value that specifies whether or not the schema extension is required for the resource type. If true, a resource of this type MUST include this schema extension and also include any attributes declared as required in this schema extension. If false, a resource of this type MAY omit this schema extension.", + "type": "boolean", + "readOnly": true + } + }, + "required": [ + "schema", + "required" + ], + "additionalProperties": true + } + ], + "additionalItems": false, + "readOnly": true + } + }, + "required": [ + "name", + "endpoint", + "schema", + "schemaExtensions" + ], + "additionalProperties": true +} diff --git a/schemas/scim-serviceProviderConfig.schema.json b/schemas/scim-serviceProviderConfig.schema.json new file mode 100644 index 000000000..634dfb90f --- /dev/null +++ b/schemas/scim-serviceProviderConfig.schema.json @@ -0,0 +1,139 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", + "title": "Service Provider Configuration", + "description": "Schema for representing the service provider's configuration", + "type": "object", + "properties": { + "documentationUri": { + "description": "An HTTP-addressable URL pointing to the service provider's human-consumable help documentation.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "patch": { + "description": "A complex type that specifies PATCH configuration options.", + "type": "object", + "properties": { + "supported": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "boolean", + "readOnly": true + } + }, + "required": [ + "supported" + ], + "readOnly": true + }, + "bulk": { + "description": "A complex type that specifies bulk configuration options.", + "type": "object", + "properties": { + "supported": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "boolean", + "readOnly": true + } + }, + "required": [ + "supported" + ], + "readOnly": true + }, + "filter": { + "description": "A complex type that specifies FILTER options.", + "type": "object", + "properties": { + "supported": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "boolean", + "readOnly": true + }, + "maxResults": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "integer", + "readOnly": true + } + }, + "required": [ + "supported" + ], + "readOnly": true + }, + "changePassword": { + "description": "A complex type that specifies configuration options related to changing a password.", + "type": "object", + "properties": { + "supported": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "boolean", + "readOnly": true + } + }, + "required": [ + "supported" + ], + "readOnly": true + }, + "sort": { + "description": "A complex type that specifies sort result options.", + "type": "object", + "properties": { + "supported": { + "description": "A Boolean value specifying whether or not the operation is supported.", + "type": "boolean", + "readOnly": true + } + }, + "required": [ + "supported" + ], + "readOnly": true + }, + "authenticationSchemes": { + "description": "A complex type that specifies supported authentication scheme properties.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "The common authentication scheme name, e.g., HTTP Basic.", + "type": "string", + "readOnly": true + }, + "description": { + "description": "A description of the authentication scheme.", + "type": "string", + "readOnly": true + }, + "specUri": { + "description": "An HTTP-addressable URL pointing to the authentication scheme's specification.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "documentationUri": { + "description": "An HTTP-addressable URL pointing to the authentication scheme's usage documentation.", + "type": "string", + "readOnly": true + } + }, + "required": [ + "name", + "description" + ], + "readOnly": true + }, + "readOnly": true + } + }, + "required": [ + "patch", + "bulk", + "filter", + "changePassword", + "sort", + "authenticationSchemes" + ] +} diff --git a/schemas/scim-user.schema.json b/schemas/scim-user.schema.json new file mode 100644 index 000000000..b5bf69d34 --- /dev/null +++ b/schemas/scim-user.schema.json @@ -0,0 +1,372 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:ietf:params:scim:schemas:core:2.0:User", + "title": "User", + "description": "User Account", + "type": "object", + "properties": { + "userName": { + "description": "Unique identifier for the User, typically used by the user to directly authenticate to the service provider. Each User MUST include a non-empty userName value. This identifier MUST be unique across the service provider's entire set of Users. REQUIRED.", + "type": "string" + }, + "name": { + "description": "The components of the user's real name. Providers MAY return just the full name as a single string in the formatted sub-attribute, or they MAY return just the individual component attributes using the other sub-attributes, or they MAY return both. If both variants are returned, they SHOULD be describing the same name, with the formatted name indicating how the component attributes should be combined.", + "type": "object", + "properties": { + "formatted": { + "description": "The full name, including all middle names, titles, and suffixes as appropriate, formatted for display (e.g., 'Ms. Barbara J Jensen, III').", + "type": "string" + }, + "familyName": { + "description": "The family name of the User, or last name in most Western languages (e.g., 'Jensen' given the full name 'Ms. Barbara J Jensen, III').", + "type": "string" + }, + "givenName": { + "description": "The given name of the User, or first name in most Western languages (e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III').", + "type": "string" + }, + "middleName": { + "description": "The middle name(s) of the User (e.g., 'Jane' given the full name 'Ms. Barbara J Jensen, III').", + "type": "string" + }, + "honorificPrefix": { + "description": "The honorific prefix(es) of the User, or title in most Western languages (e.g., 'Ms.' given the full name 'Ms. Barbara J Jensen, III').", + "type": "string" + }, + "honorificSuffix": { + "description": "The honorific suffix(es) of the User, or suffix in most Western languages (e.g., 'III' given the full name 'Ms. Barbara J Jensen, III').", + "type": "string" + } + } + }, + "displayName": { + "description": "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.", + "type": "string" + }, + "nickName": { + "description": "The casual way to address the user in real life, e.g., 'Bob' or 'Bobby' instead of 'Robert'. This attribute SHOULD NOT be used to represent a User's username (e.g., 'bjensen' or 'mpepperidge').", + "type": "string" + }, + "profileUrl": { + "description": "A fully qualified URL pointing to a page representing the User's online profile.", + "type": "string", + "format": "uri" + }, + "title": { + "description": "The user's title, such as \"Vice President.\"", + "type": "string" + }, + "userType": { + "description": "Used to identify the relationship between the organization and the user. Typical values used might be 'Contractor', 'Employee', 'Intern', 'Temp', 'External', and 'Unknown', but any value may be used.", + "type": "string" + }, + "preferredLanguage": { + "description": "Indicates the User's preferred written or spoken language. Generally used for selecting a localized user interface; e.g., 'en_US' specifies the language English and country US.", + "type": "string" + }, + "locale": { + "description": "Used to indicate the User's default location for purposes of localizing items such as currency, date time format, or numerical representations.", + "type": "string" + }, + "timezone": { + "description": "The User's time zone in the 'Olson' time zone database format, e.g., 'America/Los_Angeles'.", + "type": "string" + }, + "active": { + "description": "A Boolean value indicating the User's administrative status.", + "type": "boolean" + }, + "password": { + "description": "The User's cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User's password.", + "type": "string", + "writeOnly": true + }, + "emails": { + "description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", + "type": "string", + "format": "email" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", + "type": "string", + "enum": [ + "work", + "home", + "other" + ] + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "phoneNumbers": { + "description": "Phone numbers for the User. The value SHOULD be canonicalized by the service provider according to the format specified in RFC 3966, e.g., 'tel:+1-201-555-0123'. Canonical type values of 'work', 'home', 'mobile', 'fax', 'pager', and 'other'.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "Phone number of the User.", + "type": "string" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.", + "type": "string", + "enum": [ + "work", + "home", + "mobile", + "fax", + "pager", + "other" + ] + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "ims": { + "description": "Instant messaging addresses for the User.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "Instant messaging address for the User.", + "type": "string" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.", + "type": "string", + "enum": [ + "aim", + "gtalk", + "icq", + "xmpp", + "msn", + "skype", + "qq", + "yahoo" + ] + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "photos": { + "description": "URLs of photos of the User.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "URL of a photo of the User.", + "type": "string", + "format": "uri" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.", + "type": "string", + "enum": [ + "photo", + "thumbnail" + ] + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred photo or thumbnail. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "addresses": { + "description": "A physical mailing address for this User. Canonical type values of 'work', 'home', and 'other'. This attribute is a complex type with the following sub-attributes.", + "type": "array", + "items": { + "type": "object", + "properties": { + "formatted": { + "description": "The full mailing address, formatted for display or use with a mailing label. This attribute MAY contain newlines.", + "type": "string" + }, + "streetAddress": { + "description": "The full street address component, which may include house number, street name, P.O. box, and multi-line extended street address information. This attribute MAY contain newlines.", + "type": "string" + }, + "locality": { + "description": "The city or locality component.", + "type": "string" + }, + "region": { + "description": "The state or region component.", + "type": "string" + }, + "postalCode": { + "description": "The zip code or postal code component.", + "type": "string" + }, + "country": { + "description": "The country name component.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", + "type": "string", + "enum": [ + "work", + "home", + "other" + ] + } + } + } + }, + "groups": { + "description": "A list of groups to which the user belongs, either through direct membership, through nested groups, or dynamically calculated.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "The identifier of the User's group.", + "type": "string", + "readOnly": true + }, + "$ref": { + "description": "The URI of the corresponding 'Group' resource to which the user belongs.", + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string", + "readOnly": true + }, + "type": { + "description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.", + "type": "string", + "enum": [ + "direct", + "indirect" + ], + "readOnly": true + } + }, + "readOnly": true + }, + "readOnly": true + }, + "entitlements": { + "description": "A list of entitlements for the User that represent a thing the User has.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "The value of an entitlement.", + "type": "string" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function.", + "type": "string" + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "roles": { + "description": "A list of roles for the User that collectively represent who the User is, e.g., 'Student', 'Faculty'.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "The value of a role.", + "type": "string" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function.", + "type": "string" + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + }, + "x509Certificates": { + "description": "A list of certificates issued to the User.", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "The value of an X.509 certificate.", + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream" + }, + "display": { + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "type": "string" + }, + "type": { + "description": "A label indicating the attribute's function.", + "type": "string" + }, + "primary": { + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "type": "boolean" + } + } + } + } + }, + "required": [ + "userName" + ] +} diff --git a/xml/xenc-schema.xsd b/schemas/xenc-schema.xsd similarity index 100% rename from xml/xenc-schema.xsd rename to schemas/xenc-schema.xsd diff --git a/xml/xml.xsd b/schemas/xml.xsd similarity index 100% rename from xml/xml.xsd rename to schemas/xml.xsd diff --git a/xml/xmldsig-core-schema.xsd b/schemas/xmldsig-core-schema.xsd similarity index 100% rename from xml/xmldsig-core-schema.xsd rename to schemas/xmldsig-core-schema.xsd diff --git a/web/src/admin/applications/ApplicationListPage.ts b/web/src/admin/applications/ApplicationListPage.ts index 6e7a8b044..add0fb340 100644 --- a/web/src/admin/applications/ApplicationListPage.ts +++ b/web/src/admin/applications/ApplicationListPage.ts @@ -97,7 +97,6 @@ export class ApplicationListPage extends TablePage { >
-
${t`About applications`}
diff --git a/web/src/admin/property-mappings/PropertyMappingListPage.ts b/web/src/admin/property-mappings/PropertyMappingListPage.ts index 3600a7b1b..1c4c5d384 100644 --- a/web/src/admin/property-mappings/PropertyMappingListPage.ts +++ b/web/src/admin/property-mappings/PropertyMappingListPage.ts @@ -1,6 +1,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; +import "@goauthentik/admin/property-mappings/PropertyMappingSCIMForm"; import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; import "@goauthentik/admin/property-mappings/PropertyMappingWizard"; diff --git a/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts b/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts index c312ecfe7..44367a8c6 100644 --- a/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts @@ -13,7 +13,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { PropertymappingsApi, SAMLPropertyMapping } from "@goauthentik/api"; @customElement("ak-property-mapping-saml-form") -export class PropertyMappingLDAPForm extends ModelForm { +export class PropertyMappingSAMLForm extends ModelForm { loadInstance(pk: string): Promise { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlRetrieve({ pmUuid: pk, diff --git a/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts b/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts new file mode 100644 index 000000000..63d86d746 --- /dev/null +++ b/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts @@ -0,0 +1,69 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { docLink } from "@goauthentik/common/global"; +import "@goauthentik/elements/CodeMirror"; +import "@goauthentik/elements/forms/HorizontalFormElement"; +import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; + +import { t } from "@lingui/macro"; + +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { PropertymappingsApi, SCIMMapping } from "@goauthentik/api"; + +@customElement("ak-property-mapping-scim-form") +export class PropertyMappingSCIMForm extends ModelForm { + loadInstance(pk: string): Promise { + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimRetrieve({ + pmUuid: pk, + }); + } + + getSuccessMessage(): string { + if (this.instance) { + return t`Successfully updated mapping.`; + } else { + return t`Successfully created mapping.`; + } + } + + send = (data: SCIMMapping): Promise => { + if (this.instance) { + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimUpdate({ + pmUuid: this.instance.pk || "", + sCIMMappingRequest: data, + }); + } else { + return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimCreate({ + sCIMMappingRequest: data, + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + + + +

+ ${t`Expression using Python.`} + + ${t`See documentation for a list of all variables.`} + +

+
+
`; + } +} diff --git a/web/src/admin/providers/ProviderListPage.ts b/web/src/admin/providers/ProviderListPage.ts index 96cc71b1f..be792f083 100644 --- a/web/src/admin/providers/ProviderListPage.ts +++ b/web/src/admin/providers/ProviderListPage.ts @@ -3,6 +3,7 @@ import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm"; +import "@goauthentik/admin/providers/scim/SCIMProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { uiConfig } from "@goauthentik/common/ui/config"; import "@goauthentik/elements/buttons/SpinnerButton"; @@ -81,16 +82,23 @@ export class ProviderListPage extends TablePage { } row(item: Provider): TemplateResult[] { + let app = html``; + if (item.component === "ak-provider-scim-form") { + app = html` + ${t`No application required.`}`; + } else if (!item.assignedApplicationName) { + app = html` + ${t`Warning: Provider not assigned to any application.`}`; + } else { + app = html` + ${t`Assigned to application `} + ${item.assignedApplicationName}`; + } return [ html` ${item.name} `, - item.assignedApplicationName - ? html` - ${t`Assigned to application `} - ${item.assignedApplicationName}` - : html` - ${t`Warning: Provider not assigned to any application.`}`, + app, html`${item.verboseName}`, html` ${t`Update`} diff --git a/web/src/admin/providers/ProviderViewPage.ts b/web/src/admin/providers/ProviderViewPage.ts index 3b8188698..14e4690b1 100644 --- a/web/src/admin/providers/ProviderViewPage.ts +++ b/web/src/admin/providers/ProviderViewPage.ts @@ -2,6 +2,7 @@ import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage"; import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage"; import "@goauthentik/admin/providers/saml/SAMLProviderViewPage"; +import "@goauthentik/admin/providers/scim/SCIMProviderViewPage"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; @@ -56,6 +57,10 @@ export class ProviderViewPage extends AKElement { return html``; + case "ak-provider-scim-form": + return html``; default: return html`

Invalid provider type ${this.provider?.component}

`; } diff --git a/web/src/admin/providers/saml/SAMLProviderImportForm.ts b/web/src/admin/providers/saml/SAMLProviderImportForm.ts index 9b7a9d359..ccddc06cb 100644 --- a/web/src/admin/providers/saml/SAMLProviderImportForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderImportForm.ts @@ -32,7 +32,7 @@ export class SAMLProviderImportForm extends Form { return new ProvidersApi(DEFAULT_CONFIG).providersSamlImportMetadataCreate({ file: file, name: data.name, - authorizationFlow: data.authorizationFlow, + authorizationFlow: data.authorizationFlow || "", }); }; diff --git a/web/src/admin/providers/scim/SCIMProviderForm.ts b/web/src/admin/providers/scim/SCIMProviderForm.ts new file mode 100644 index 000000000..808caa19a --- /dev/null +++ b/web/src/admin/providers/scim/SCIMProviderForm.ts @@ -0,0 +1,178 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; +import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; +import "@goauthentik/elements/forms/Radio"; +import "@goauthentik/elements/forms/SearchSelect"; + +import { t } from "@lingui/macro"; + +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { until } from "lit/directives/until.js"; + +import { PropertymappingsApi, ProvidersApi, SCIMProvider } from "@goauthentik/api"; + +@customElement("ak-provider-scim-form") +export class SCIMProviderFormPage extends ModelForm { + loadInstance(pk: number): Promise { + return new ProvidersApi(DEFAULT_CONFIG).providersScimRetrieve({ + id: pk, + }); + } + + getSuccessMessage(): string { + if (this.instance) { + return t`Successfully updated provider.`; + } else { + return t`Successfully created provider.`; + } + } + + send = (data: SCIMProvider): Promise => { + if (this.instance) { + return new ProvidersApi(DEFAULT_CONFIG).providersScimUpdate({ + id: this.instance.pk || 0, + sCIMProviderRequest: data, + }); + } else { + return new ProvidersApi(DEFAULT_CONFIG).providersScimCreate({ + sCIMProviderRequest: data, + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + + ${t`Protocol settings`} +
+ + +

+ ${t`SCIM base url, usually ends in /v2.`} +

+
+ + +

+ ${t`Token to authenticate with. Currently only bearer authentication is supported.`} +

+
+
+
+ + ${t`Attribute mapping`} +
+ + +

+ ${t`Property mappings used to user mapping.`} +

+

+ ${t`Hold control/command to select multiple items.`} +

+
+ + +

+ ${t`Property mappings used to group creation.`} +

+

+ ${t`Hold control/command to select multiple items.`} +

+
+
+
+
`; + } +} diff --git a/web/src/admin/providers/scim/SCIMProviderViewPage.ts b/web/src/admin/providers/scim/SCIMProviderViewPage.ts new file mode 100644 index 000000000..e97e231d5 --- /dev/null +++ b/web/src/admin/providers/scim/SCIMProviderViewPage.ts @@ -0,0 +1,211 @@ +import "@goauthentik/admin/providers/scim/SCIMProviderForm"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { EVENT_REFRESH } from "@goauthentik/common/constants"; +import { me } from "@goauthentik/common/users"; +import MDSCIMProvider from "@goauthentik/docs/providers/scim/index.md"; +import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/Markdown"; +import "@goauthentik/elements/Tabs"; +import "@goauthentik/elements/buttons/ActionButton"; +import "@goauthentik/elements/buttons/ModalButton"; +import "@goauthentik/elements/events/ObjectChangelog"; + +import { t } from "@lingui/macro"; + +import { CSSResult, TemplateResult, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { until } from "lit/directives/until.js"; + +import AKGlobal from "@goauthentik/common/styles/authentik.css"; +import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFList from "@patternfly/patternfly/components/List/list.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { ProvidersApi, SCIMProvider, SessionUser } from "@goauthentik/api"; + +@customElement("ak-provider-scim-view") +export class SCIMProviderViewPage extends AKElement { + @property() + set args(value: { [key: string]: number }) { + this.providerID = value.id; + } + + @property({ type: Number }) + set providerID(value: number) { + new ProvidersApi(DEFAULT_CONFIG) + .providersScimRetrieve({ + id: value, + }) + .then((prov) => (this.provider = prov)); + } + + @property({ attribute: false }) + provider?: SCIMProvider; + + @state() + me?: SessionUser; + + static get styles(): CSSResult[] { + return [ + PFBase, + PFButton, + PFBanner, + PFForm, + PFFormControl, + PFList, + PFGrid, + PFPage, + PFContent, + PFCard, + PFDescriptionList, + AKGlobal, + ]; + } + + constructor() { + super(); + this.addEventListener(EVENT_REFRESH, () => { + if (!this.provider?.pk) return; + this.providerID = this.provider?.pk; + }); + me().then((user) => { + this.me = user; + }); + } + + render(): TemplateResult { + if (!this.provider) { + return html``; + } + return html` +
+ ${this.renderTabOverview()} +
+
+
+
+ + +
+
+
+
`; + } + + renderTabOverview(): TemplateResult { + if (!this.provider) { + return html``; + } + return html`
+ ${t`SCIM provider is in preview.`} +
+
+
+
+
+
+
+
+ ${t`Name`} +
+
+
+ ${this.provider.name} +
+
+
+
+
+ ${t`URL`} +
+
+
+ ${this.provider.url} +
+
+
+
+
+ +
+
+
+

${t`Sync status`}

+
+
+ ${until( + new ProvidersApi(DEFAULT_CONFIG) + .providersScimSyncStatusRetrieve({ + id: this.provider.pk, + }) + .then((task) => { + return html`
    + ${task.messages.map((m) => { + return html`
  • ${m}
  • `; + })} +
`; + }) + .catch(() => { + return html`${t`Sync not run yet.`}`; + }), + "loading", + )} +
+ + +
+
+
+
+ +
+
+
`; + } +} diff --git a/web/src/elements/Markdown.ts b/web/src/elements/Markdown.ts index 1076f433f..d34831b6d 100644 --- a/web/src/elements/Markdown.ts +++ b/web/src/elements/Markdown.ts @@ -3,7 +3,7 @@ import "@goauthentik/elements/Alert"; import { Level } from "@goauthentik/elements/Alert"; import { AKElement } from "@goauthentik/elements/Base"; -import { CSSResult, TemplateResult, html } from "lit"; +import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; @@ -35,7 +35,16 @@ export class Markdown extends AKElement { ]; static get styles(): CSSResult[] { - return [PFList, PFContent, AKGlobal]; + return [ + PFList, + PFContent, + AKGlobal, + css` + h2:first-of-type { + margin-top: 0; + } + `, + ]; } replaceAdmonitions(input: string): string { diff --git a/website/docs/providers/scim/index.md b/website/docs/providers/scim/index.md new file mode 100644 index 000000000..3d94de300 --- /dev/null +++ b/website/docs/providers/scim/index.md @@ -0,0 +1,40 @@ +--- +title: SCIM Provider +--- + +SCIM (System for Cross-domain Identity Management) is a set of APIs to provision users and groups. The SCIM provider in authentik supports SCIM 2.0 and can be used to provision and sync users from authentik into other applications. + +:::info +The SCIM provider is currently in Preview. +::: + +### Configuration + +A SCIM provider requires a base URL and a token. SCIM works via HTTP requests, so authentik must be able to reach the specified endpoint. + +When configuring SCIM, you'll get an endpoint and a token from the application that accepts SCIM data. This endpoint usually ends in `/v2`, which corresponds to the SCIM version supported. + +The token given by the application will be sent with all outgoing SCIM requests to authenticate them. + +### Syncing + +Data is synchronized in multiple ways: + +- When a user/group is created/modified/deleted, that action is sent to all SCIM providers +- Periodically (once an hour), all SCIM providers are fully synchronized + +The actual synchronization process is run in the authentik worker. To allow this process to better to scale, a task is started for each 100 users and groups, so when multiple workers are available the workload will be distributed. + +### Supported features + +SCIM defines multiple optional features, some of which are supported by the SCIM provider. + +- Bulk updates +- Password changes +- Etag + +### Attribute mapping + +Attribute mapping from authentik to SCIM users is done via property mappings as with other providers. The default mappings for users and groups make some assumptions that should work for most setups, but it is also possible to define custom mappings to add fields. + +All selected mappings are applied in the order of their name, and are deeply merged onto the final user data. The final data is then validated against the SCIM schema, and if the data is not valid, the sync is stopped. diff --git a/website/sidebars.js b/website/sidebars.js index e77f03393..abe458471 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -77,6 +77,7 @@ module.exports = { }, items: ["providers/ldap/generic_setup"], }, + "providers/scim/index", ], }, {