From 99ad492951b0820ecdbea207c6316af820217dc3 Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 9 May 2024 15:41:23 +0200 Subject: [PATCH] enterprise/providers/microsoft_entra: initial account sync to microsoft entra (#9632) * initial Signed-off-by: Jens Langhammer * add entra mappings Signed-off-by: Jens Langhammer * fix some stuff Signed-off-by: Jens Langhammer * make API endpoints more consistent Signed-off-by: Jens Langhammer * implement more things Signed-off-by: Jens Langhammer * add user tests Signed-off-by: Jens Langhammer * fix most group tests + fix bugs Signed-off-by: Jens Langhammer * more group tests, fix bugs Signed-off-by: Jens Langhammer * fix missing __init__ Signed-off-by: Jens Langhammer * add ui for provisioned users Signed-off-by: Jens Langhammer * fix a bunch of bugs Signed-off-by: Jens Langhammer * add `creating` to property mapping env Signed-off-by: Jens Langhammer * always sync group members Signed-off-by: Jens Langhammer * fix stuff Signed-off-by: Jens Langhammer * fix group membership Signed-off-by: Jens Langhammer * fix some types Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer * add group member add test Signed-off-by: Jens Langhammer * create sync status component to dedupe Signed-off-by: Jens Langhammer * fix discovery tests Signed-off-by: Jens Langhammer * get rid of more code and fix more issues Signed-off-by: Jens Langhammer * add error handling for auth and transient Signed-off-by: Jens Langhammer * make sure autoretry is on Signed-off-by: Jens Langhammer * format web Signed-off-by: Jens Langhammer * wait for task in signal Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer * add squashed google migration Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .vscode/settings.json | 13 +- authentik/blueprints/v1/importer.py | 13 + .../providers/google_workspace/api/groups.py | 33 + .../google_workspace/api/property_mappings.py | 16 +- .../google_workspace/api/providers.py | 10 +- .../providers/google_workspace/api/users.py | 33 + .../google_workspace/clients/base.py | 13 +- .../google_workspace/clients/groups.py | 38 +- .../google_workspace/clients/users.py | 18 +- ...workspaceprovidergroup_options_and_more.py | 179 + ...workspaceprovidergroup_options_and_more.py | 27 + .../providers/google_workspace/models.py | 52 +- .../providers/google_workspace/tasks.py | 11 +- .../google_workspace/tests/test_groups.py | 4 +- .../google_workspace/tests/test_users.py | 6 +- .../providers/google_workspace/urls.py | 18 +- .../providers/microsoft_entra/__init__.py | 0 .../providers/microsoft_entra/api/__init__.py | 0 .../providers/microsoft_entra/api/groups.py | 33 + .../microsoft_entra/api/property_mappings.py | 39 + .../microsoft_entra/api/providers.py | 52 + .../providers/microsoft_entra/api/users.py | 33 + .../providers/microsoft_entra/apps.py | 9 + .../microsoft_entra/clients/__init__.py | 0 .../providers/microsoft_entra/clients/base.py | 100 + .../microsoft_entra/clients/groups.py | 241 ++ .../microsoft_entra/clients/users.py | 150 + .../migrations/0001_initial.py | 165 + .../microsoft_entra/migrations/__init__.py | 0 .../providers/microsoft_entra/models.py | 180 + .../providers/microsoft_entra/settings.py | 13 + .../providers/microsoft_entra/signals.py | 16 + .../providers/microsoft_entra/tasks.py | 37 + .../microsoft_entra/tests/__init__.py | 0 .../microsoft_entra/tests/test_groups.py | 392 ++ .../microsoft_entra/tests/test_users.py | 337 ++ .../providers/microsoft_entra/urls.py | 21 + authentik/enterprise/settings.py | 1 + authentik/lib/logging.py | 1 + authentik/lib/sync/outgoing/base.py | 15 +- authentik/lib/sync/outgoing/exceptions.py | 4 + authentik/lib/sync/outgoing/models.py | 11 +- authentik/lib/sync/outgoing/signals.py | 2 +- authentik/lib/sync/outgoing/tasks.py | 27 +- authentik/providers/scim/clients/groups.py | 9 +- authentik/providers/scim/clients/users.py | 9 +- authentik/providers/scim/tasks.py | 11 +- authentik/providers/scim/tests/test_group.py | 2 +- authentik/providers/scim/tests/test_user.py | 2 +- authentik/root/settings.py | 4 +- blueprints/schema.json | 173 + .../system/providers-google-workspace.yaml | 2 +- .../system/providers-microsoft-entra.yaml | 39 + poetry.lock | 594 ++- pyproject.toml | 3 +- schema.yml | 3515 +++++++++++++---- .../PropertyMappingGoogleWorkspaceForm.ts | 14 +- .../PropertyMappingLDAPForm.ts | 2 +- .../PropertyMappingListPage.ts | 1 + .../PropertyMappingMicrosoftEntraForm.ts | 72 + .../PropertyMappingNotification.ts | 2 +- .../PropertyMappingRACForm.ts | 2 +- .../PropertyMappingSAMLForm.ts | 2 +- .../PropertyMappingSCIMForm.ts | 2 +- .../PropertyMappingScopeForm.ts | 2 +- web/src/admin/providers/ProviderListPage.ts | 3 +- web/src/admin/providers/ProviderViewPage.ts | 7 +- .../GoogleWorkspaceProviderForm.ts | 42 +- .../GoogleWorkspaceProviderGroupList.ts | 42 + .../GoogleWorkspaceProviderUserList.ts | 43 + .../GoogleWorkspaceProviderViewPage.ts | 115 +- .../admin/providers/ldap/LDAPProviderForm.ts | 2 +- .../MicrosoftEntraProviderFormPage.ts | 286 ++ .../MicrosoftEntraProviderGroupList.ts | 42 + .../MicrosoftEntraProviderUserList.ts | 43 + .../MicrosoftEntraProviderViewPage.ts | 224 ++ .../providers/oauth2/OAuth2ProviderForm.ts | 2 +- .../providers/proxy/ProxyProviderForm.ts | 2 +- .../admin/providers/rac/RACProviderForm.ts | 2 +- .../providers/radius/RadiusProviderForm.ts | 2 +- .../admin/providers/saml/SAMLProviderForm.ts | 2 +- .../admin/providers/scim/SCIMProviderForm.ts | 2 +- .../providers/scim/SCIMProviderViewPage.ts | 102 +- .../admin/sources/ldap/LDAPSourceViewPage.ts | 84 +- web/src/elements/SyncStatusCard.ts | 119 + 85 files changed, 6930 insertions(+), 1061 deletions(-) create mode 100644 authentik/enterprise/providers/google_workspace/api/groups.py create mode 100644 authentik/enterprise/providers/google_workspace/api/users.py create mode 100644 authentik/enterprise/providers/google_workspace/migrations/0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more.py create mode 100644 authentik/enterprise/providers/google_workspace/migrations/0002_alter_googleworkspaceprovidergroup_options_and_more.py create mode 100644 authentik/enterprise/providers/microsoft_entra/__init__.py create mode 100644 authentik/enterprise/providers/microsoft_entra/api/__init__.py create mode 100644 authentik/enterprise/providers/microsoft_entra/api/groups.py create mode 100644 authentik/enterprise/providers/microsoft_entra/api/property_mappings.py create mode 100644 authentik/enterprise/providers/microsoft_entra/api/providers.py create mode 100644 authentik/enterprise/providers/microsoft_entra/api/users.py create mode 100644 authentik/enterprise/providers/microsoft_entra/apps.py create mode 100644 authentik/enterprise/providers/microsoft_entra/clients/__init__.py create mode 100644 authentik/enterprise/providers/microsoft_entra/clients/base.py create mode 100644 authentik/enterprise/providers/microsoft_entra/clients/groups.py create mode 100644 authentik/enterprise/providers/microsoft_entra/clients/users.py create mode 100644 authentik/enterprise/providers/microsoft_entra/migrations/0001_initial.py create mode 100644 authentik/enterprise/providers/microsoft_entra/migrations/__init__.py create mode 100644 authentik/enterprise/providers/microsoft_entra/models.py create mode 100644 authentik/enterprise/providers/microsoft_entra/settings.py create mode 100644 authentik/enterprise/providers/microsoft_entra/signals.py create mode 100644 authentik/enterprise/providers/microsoft_entra/tasks.py create mode 100644 authentik/enterprise/providers/microsoft_entra/tests/__init__.py create mode 100644 authentik/enterprise/providers/microsoft_entra/tests/test_groups.py create mode 100644 authentik/enterprise/providers/microsoft_entra/tests/test_users.py create mode 100644 authentik/enterprise/providers/microsoft_entra/urls.py create mode 100644 blueprints/system/providers-microsoft-entra.yaml create mode 100644 web/src/admin/property-mappings/PropertyMappingMicrosoftEntraForm.ts create mode 100644 web/src/admin/providers/google_workspace/GoogleWorkspaceProviderGroupList.ts create mode 100644 web/src/admin/providers/google_workspace/GoogleWorkspaceProviderUserList.ts create mode 100644 web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage.ts create mode 100644 web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList.ts create mode 100644 web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList.ts create mode 100644 web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage.ts create mode 100644 web/src/elements/SyncStatusCard.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 218800d1edda..ed87f96869fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,20 +4,21 @@ "asgi", "authentik", "authn", + "entra", "goauthentik", "jwks", + "kubernetes", "oidc", "openid", + "passwordless", "plex", "saml", + "scim", + "slo", + "sso", "totp", - "webauthn", "traefik", - "passwordless", - "kubernetes", - "sso", - "slo", - "scim", + "webauthn", ], "todo-tree.tree.showCountsInTree": true, "todo-tree.tree.showBadges": true, diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index 714667a2dd4a..7ea645c467a6 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -39,6 +39,14 @@ ) from authentik.enterprise.license import LicenseKey from authentik.enterprise.models import LicenseUsage +from authentik.enterprise.providers.google_workspace.models import ( + GoogleWorkspaceProviderGroup, + GoogleWorkspaceProviderUser, +) +from authentik.enterprise.providers.microsoft_entra.models import ( + MicrosoftEntraProviderGroup, + MicrosoftEntraProviderUser, +) from authentik.enterprise.providers.rac.models import ConnectionToken from authentik.events.logs import LogEvent, capture_logs from authentik.events.models import SystemTask @@ -86,6 +94,7 @@ def excluded_models() -> list[type[Model]]: # Classes that have other dependencies AuthenticatedSession, # Classes which are only internally managed + # FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin FlowToken, LicenseUsage, SCIMGroup, @@ -100,6 +109,10 @@ def excluded_models() -> list[type[Model]]: WebAuthnDeviceType, SCIMSourceUser, SCIMSourceGroup, + GoogleWorkspaceProviderUser, + GoogleWorkspaceProviderGroup, + MicrosoftEntraProviderUser, + MicrosoftEntraProviderGroup, ) diff --git a/authentik/enterprise/providers/google_workspace/api/groups.py b/authentik/enterprise/providers/google_workspace/api/groups.py new file mode 100644 index 000000000000..7317a5a733e2 --- /dev/null +++ b/authentik/enterprise/providers/google_workspace/api/groups.py @@ -0,0 +1,33 @@ +"""GoogleWorkspaceProviderGroup API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.sources import SourceSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.core.api.users import UserGroupSerializer +from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup + + +class GoogleWorkspaceProviderGroupSerializer(SourceSerializer): + """GoogleWorkspaceProviderGroup Serializer""" + + group_obj = UserGroupSerializer(source="group", read_only=True) + + class Meta: + + model = GoogleWorkspaceProviderGroup + fields = [ + "id", + "group", + "group_obj", + ] + + +class GoogleWorkspaceProviderGroupViewSet(UsedByMixin, ModelViewSet): + """GoogleWorkspaceProviderGroup Viewset""" + + queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group") + serializer_class = GoogleWorkspaceProviderGroupSerializer + filterset_fields = ["provider__id", "group__name", "group__group_uuid"] + search_fields = ["provider__name", "group__name"] + ordering = ["group__name"] diff --git a/authentik/enterprise/providers/google_workspace/api/property_mappings.py b/authentik/enterprise/providers/google_workspace/api/property_mappings.py index 86c42736d09b..eea3042b7f58 100644 --- a/authentik/enterprise/providers/google_workspace/api/property_mappings.py +++ b/authentik/enterprise/providers/google_workspace/api/property_mappings.py @@ -11,16 +11,16 @@ from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping -class GoogleProviderMappingSerializer(PropertyMappingSerializer): - """GoogleProviderMapping Serializer""" +class GoogleWorkspaceProviderMappingSerializer(PropertyMappingSerializer): + """GoogleWorkspaceProviderMapping Serializer""" class Meta: model = GoogleWorkspaceProviderMapping fields = PropertyMappingSerializer.Meta.fields -class GoogleProviderMappingFilter(FilterSet): - """Filter for GoogleProviderMapping""" +class GoogleWorkspaceProviderMappingFilter(FilterSet): + """Filter for GoogleWorkspaceProviderMapping""" managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed")) @@ -29,11 +29,11 @@ class Meta: fields = "__all__" -class GoogleProviderMappingViewSet(UsedByMixin, ModelViewSet): - """GoogleProviderMapping Viewset""" +class GoogleWorkspaceProviderMappingViewSet(UsedByMixin, ModelViewSet): + """GoogleWorkspaceProviderMapping Viewset""" queryset = GoogleWorkspaceProviderMapping.objects.all() - serializer_class = GoogleProviderMappingSerializer - filterset_class = GoogleProviderMappingFilter + serializer_class = GoogleWorkspaceProviderMappingSerializer + filterset_class = GoogleWorkspaceProviderMappingFilter search_fields = ["name"] ordering = ["name"] diff --git a/authentik/enterprise/providers/google_workspace/api/providers.py b/authentik/enterprise/providers/google_workspace/api/providers.py index 15a4b4bb720a..392ff9ea4fed 100644 --- a/authentik/enterprise/providers/google_workspace/api/providers.py +++ b/authentik/enterprise/providers/google_workspace/api/providers.py @@ -10,8 +10,8 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin -class GoogleProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer): - """GoogleProvider Serializer""" +class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer): + """GoogleWorkspaceProvider Serializer""" class Meta: model = GoogleWorkspaceProvider @@ -38,11 +38,11 @@ class Meta: extra_kwargs = {} -class GoogleProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet): - """GoogleProvider Viewset""" +class GoogleWorkspaceProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet): + """GoogleWorkspaceProvider Viewset""" queryset = GoogleWorkspaceProvider.objects.all() - serializer_class = GoogleProviderSerializer + serializer_class = GoogleWorkspaceProviderSerializer filterset_fields = [ "name", "exclude_users_service_account", diff --git a/authentik/enterprise/providers/google_workspace/api/users.py b/authentik/enterprise/providers/google_workspace/api/users.py new file mode 100644 index 000000000000..a0fa658e3df1 --- /dev/null +++ b/authentik/enterprise/providers/google_workspace/api/users.py @@ -0,0 +1,33 @@ +"""GoogleWorkspaceProviderUser API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.groups import GroupMemberSerializer +from authentik.core.api.sources import SourceSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser + + +class GoogleWorkspaceProviderUserSerializer(SourceSerializer): + """GoogleWorkspaceProviderUser Serializer""" + + user_obj = GroupMemberSerializer(source="user", read_only=True) + + class Meta: + + model = GoogleWorkspaceProviderUser + fields = [ + "id", + "user", + "user_obj", + ] + + +class GoogleWorkspaceProviderUserViewSet(UsedByMixin, ModelViewSet): + """GoogleWorkspaceProviderUser Viewset""" + + queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user") + serializer_class = GoogleWorkspaceProviderUserSerializer + filterset_fields = ["provider__id", "user__username", "user__id"] + search_fields = ["provider__name", "user__username"] + ordering = ["user__username"] diff --git a/authentik/enterprise/providers/google_workspace/clients/base.py b/authentik/enterprise/providers/google_workspace/clients/base.py index e8e30f38d51e..8eebe13f6c7f 100644 --- a/authentik/enterprise/providers/google_workspace/clients/base.py +++ b/authentik/enterprise/providers/google_workspace/clients/base.py @@ -1,5 +1,5 @@ from django.db.models import Model -from django.http import HttpResponseNotFound +from django.http import HttpResponseBadRequest, HttpResponseNotFound from google.auth.exceptions import GoogleAuthError, TransportError from googleapiclient.discovery import build from googleapiclient.errors import Error, HttpError @@ -10,6 +10,7 @@ from authentik.lib.sync.outgoing import HTTP_CONFLICT from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient from authentik.lib.sync.outgoing.exceptions import ( + BadRequestSyncException, NotFoundSyncException, ObjectExistsSyncException, StopSync, @@ -50,22 +51,24 @@ def _request(self, request: HttpRequest): raise StopSync(exc) from exc except HttpLib2Error as exc: if isinstance(exc, HttpLib2ErrorWithResponse): - self._response_handle_status_code(exc.response.status, exc) + self._response_handle_status_code(request.body, exc.response.status, exc) raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc except HttpError as exc: - self._response_handle_status_code(exc.status_code, exc) + self._response_handle_status_code(request.body, exc.status_code, exc) raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc except Error as exc: raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc return response - def _response_handle_status_code(self, status_code: int, root_exc: Exception): + def _response_handle_status_code(self, request: dict, status_code: int, root_exc: Exception): if status_code == HttpResponseNotFound.status_code: raise NotFoundSyncException("Object not found") from root_exc if status_code == HTTP_CONFLICT: raise ObjectExistsSyncException("Object exists") from root_exc + if status_code == HttpResponseBadRequest.status_code: + raise BadRequestSyncException("Bad request", request) from root_exc def check_email_valid(self, *emails: str): for email in emails: if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains): - raise TransientSyncException(f"Invalid email domain: {email}") + raise BadRequestSyncException(f"Invalid email domain: {email}") diff --git a/authentik/enterprise/providers/google_workspace/clients/groups.py b/authentik/enterprise/providers/google_workspace/clients/groups.py index 5499a8d93dd1..41c1fd556309 100644 --- a/authentik/enterprise/providers/google_workspace/clients/groups.py +++ b/authentik/enterprise/providers/google_workspace/clients/groups.py @@ -9,7 +9,6 @@ from authentik.core.models import Group from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient from authentik.enterprise.providers.google_workspace.models import ( - GoogleWorkspaceDeleteAction, GoogleWorkspaceProviderGroup, GoogleWorkspaceProviderMapping, GoogleWorkspaceProviderUser, @@ -22,6 +21,7 @@ StopSync, TransientSyncException, ) +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction from authentik.lib.utils.errors import exception_to_string @@ -34,7 +34,7 @@ class GoogleWorkspaceGroupClient( connection_type_query = "group" can_discover = True - def to_schema(self, obj: Group) -> dict: + def to_schema(self, obj: Group, creating: bool) -> dict: """Convert authentik group""" raw_google_group = { "email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}" @@ -45,12 +45,12 @@ def to_schema(self, obj: Group) -> dict: if not isinstance(mapping, GoogleWorkspaceProviderMapping): continue try: - mapping: GoogleWorkspaceProviderMapping value = mapping.evaluate( user=None, request=None, group=obj, provider=self.provider, + creating=creating, ) if value is None: continue @@ -79,7 +79,7 @@ def delete(self, obj: Group): self.logger.debug("Group does not exist in Google, skipping") return None with transaction.atomic(): - if self.provider.group_delete_action == GoogleWorkspaceDeleteAction.DELETE: + if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: self._request( self.directory_service.groups().delete(groupKey=google_group.google_id) ) @@ -87,7 +87,7 @@ def delete(self, obj: Group): def create(self, group: Group): """Create group from scratch and create a connection object""" - google_group = self.to_schema(group) + google_group = self.to_schema(group, True) self.check_email_valid(google_group["email"]) with transaction.atomic(): try: @@ -99,17 +99,17 @@ def create(self, group: Group): group_data = self._request( self.directory_service.groups().get(groupKey=google_group["email"]) ) - GoogleWorkspaceProviderGroup.objects.create( + return GoogleWorkspaceProviderGroup.objects.create( provider=self.provider, group=group, google_id=group_data["id"] ) else: - GoogleWorkspaceProviderGroup.objects.create( + return GoogleWorkspaceProviderGroup.objects.create( provider=self.provider, group=group, google_id=response["id"] ) def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): """Update existing group""" - google_group = self.to_schema(group) + google_group = self.to_schema(group, False) self.check_email_valid(google_group["email"]) try: return self._request( @@ -124,28 +124,16 @@ def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): def write(self, obj: Group): google_group, created = super().write(obj) - if created: - self.create_sync_members(obj, google_group) - return google_group + self.create_sync_members(obj, google_group) + return google_group, created - def create_sync_members(self, obj: Group, google_group: dict): + def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup): """Sync all members after a group was created""" users = list(obj.users.order_by("id").values_list("id", flat=True)) connections = GoogleWorkspaceProviderUser.objects.filter( provider=self.provider, user__pk__in=users - ) - for user in connections: - try: - self._request( - self.directory_service.members().insert( - groupKey=google_group["id"], - body={ - "email": user.google_id, - }, - ) - ) - except TransientSyncException: - continue + ).values_list("google_id", flat=True) + self._patch(google_group.google_id, Direction.add, connections) def update_group(self, group: Group, action: Direction, users_set: set[int]): """Update a groups members""" diff --git a/authentik/enterprise/providers/google_workspace/clients/users.py b/authentik/enterprise/providers/google_workspace/clients/users.py index 061bdeee5f48..5f91e01838f2 100644 --- a/authentik/enterprise/providers/google_workspace/clients/users.py +++ b/authentik/enterprise/providers/google_workspace/clients/users.py @@ -8,7 +8,6 @@ from authentik.core.models import User from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient from authentik.enterprise.providers.google_workspace.models import ( - GoogleWorkspaceDeleteAction, GoogleWorkspaceProviderMapping, GoogleWorkspaceProviderUser, ) @@ -18,6 +17,7 @@ StopSync, TransientSyncException, ) +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction from authentik.lib.utils.errors import exception_to_string from authentik.policies.utils import delete_none_values @@ -29,18 +29,18 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP connection_type_query = "user" can_discover = True - def to_schema(self, obj: User) -> dict: + def to_schema(self, obj: User, creating: bool) -> dict: """Convert authentik user""" raw_google_user = {} for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses(): if not isinstance(mapping, GoogleWorkspaceProviderMapping): continue try: - mapping: GoogleWorkspaceProviderMapping value = mapping.evaluate( user=obj, request=None, provider=self.provider, + creating=creating, ) if value is None: continue @@ -71,11 +71,11 @@ def delete(self, obj: User): return None with transaction.atomic(): response = None - if self.provider.user_delete_action == GoogleWorkspaceDeleteAction.DELETE: + if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE: response = self._request( self.directory_service.users().delete(userKey=google_user.google_id) ) - elif self.provider.user_delete_action == GoogleWorkspaceDeleteAction.SUSPEND: + elif self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND: response = self._request( self.directory_service.users().update( userKey=google_user.google_id, body={"suspended": True} @@ -86,7 +86,7 @@ def delete(self, obj: User): def create(self, user: User): """Create user from scratch and create a connection object""" - google_user = self.to_schema(user) + google_user = self.to_schema(user, True) self.check_email_valid( google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])] ) @@ -95,19 +95,19 @@ def create(self, user: User): response = self._request(self.directory_service.users().insert(body=google_user)) except ObjectExistsSyncException: # user already exists in google workspace, so we can connect them manually - GoogleWorkspaceProviderUser.objects.create( + return GoogleWorkspaceProviderUser.objects.create( provider=self.provider, user=user, google_id=user.email ) except TransientSyncException as exc: raise exc else: - GoogleWorkspaceProviderUser.objects.create( + return GoogleWorkspaceProviderUser.objects.create( provider=self.provider, user=user, google_id=response["primaryEmail"] ) def update(self, user: User, connection: GoogleWorkspaceProviderUser): """Update existing user""" - google_user = self.to_schema(user) + google_user = self.to_schema(user, False) self.check_email_valid( google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])] ) diff --git a/authentik/enterprise/providers/google_workspace/migrations/0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more.py b/authentik/enterprise/providers/google_workspace/migrations/0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more.py new file mode 100644 index 000000000000..e430328f7587 --- /dev/null +++ b/authentik/enterprise/providers/google_workspace/migrations/0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more.py @@ -0,0 +1,179 @@ +# Generated by Django 5.0.6 on 2024-05-09 12:57 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [ + ("authentik_providers_google_workspace", "0001_initial"), + ( + "authentik_providers_google_workspace", + "0002_alter_googleworkspaceprovidergroup_options_and_more", + ), + ] + + initial = True + + dependencies = [ + ("authentik_core", "0035_alter_group_options_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="GoogleWorkspaceProviderMapping", + 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": "Google Workspace Provider Mapping", + "verbose_name_plural": "Google Workspace Provider Mappings", + }, + bases=("authentik_core.propertymapping",), + ), + migrations.CreateModel( + name="GoogleWorkspaceProvider", + 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", + ), + ), + ("delegated_subject", models.EmailField(max_length=254)), + ("credentials", models.JSONField()), + ( + "scopes", + models.TextField( + default="https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.group,https://www.googleapis.com/auth/admin.directory.group.member,https://www.googleapis.com/auth/admin.directory.domain.readonly" + ), + ), + ("default_group_email_domain", models.TextField()), + ("exclude_users_service_account", models.BooleanField(default=False)), + ( + "user_delete_action", + models.TextField( + choices=[ + ("do_nothing", "Do Nothing"), + ("delete", "Delete"), + ("suspend", "Suspend"), + ], + default="delete", + ), + ), + ( + "group_delete_action", + models.TextField( + choices=[ + ("do_nothing", "Do Nothing"), + ("delete", "Delete"), + ("suspend", "Suspend"), + ], + default="delete", + ), + ), + ( + "filter_group", + models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="authentik_core.group", + ), + ), + ( + "property_mappings_group", + models.ManyToManyField( + blank=True, + default=None, + help_text="Property mappings used for group creation/updating.", + to="authentik_core.propertymapping", + ), + ), + ], + options={ + "verbose_name": "Google Workspace Provider", + "verbose_name_plural": "Google Workspace Providers", + }, + bases=("authentik_core.provider", models.Model), + ), + migrations.CreateModel( + name="GoogleWorkspaceProviderGroup", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("google_id", models.TextField()), + ( + "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_google_workspace.googleworkspaceprovider", + ), + ), + ], + options={ + "unique_together": {("google_id", "group", "provider")}, + "verbose_name": "Google Workspace Provider Group", + "verbose_name_plural": "Google Workspace Provider Groups", + }, + ), + migrations.CreateModel( + name="GoogleWorkspaceProviderUser", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("google_id", models.TextField()), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_google_workspace.googleworkspaceprovider", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "unique_together": {("google_id", "user", "provider")}, + "verbose_name": "Google Workspace Provider User", + "verbose_name_plural": "Google Workspace Provider Users", + }, + ), + ] diff --git a/authentik/enterprise/providers/google_workspace/migrations/0002_alter_googleworkspaceprovidergroup_options_and_more.py b/authentik/enterprise/providers/google_workspace/migrations/0002_alter_googleworkspaceprovidergroup_options_and_more.py new file mode 100644 index 000000000000..4f27a327e0dc --- /dev/null +++ b/authentik/enterprise/providers/google_workspace/migrations/0002_alter_googleworkspaceprovidergroup_options_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.6 on 2024-05-08 14:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_providers_google_workspace", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="googleworkspaceprovidergroup", + options={ + "verbose_name": "Google Workspace Provider Group", + "verbose_name_plural": "Google Workspace Provider Groups", + }, + ), + migrations.AlterModelOptions( + name="googleworkspaceprovideruser", + options={ + "verbose_name": "Google Workspace Provider User", + "verbose_name_plural": "Google Workspace Provider Users", + }, + ), + ] diff --git a/authentik/enterprise/providers/google_workspace/models.py b/authentik/enterprise/providers/google_workspace/models.py index 32cf303304f9..4d3d831e6000 100644 --- a/authentik/enterprise/providers/google_workspace/models.py +++ b/authentik/enterprise/providers/google_workspace/models.py @@ -16,8 +16,9 @@ User, UserTypes, ) +from authentik.lib.models import SerializerModel from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient -from authentik.lib.sync.outgoing.models import OutgoingSyncProvider +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider def default_scopes() -> list[str]: @@ -29,15 +30,6 @@ def default_scopes() -> list[str]: ] -class GoogleWorkspaceDeleteAction(models.TextChoices): - """Action taken when a user/group is deleted in authentik. Suspend is not available for groups, - and will be treated as `do_nothing`""" - - DO_NOTHING = "do_nothing" - DELETE = "delete" - SUSPEND = "suspend" - - class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): """Sync users from authentik into Google Workspace.""" @@ -48,10 +40,10 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): default_group_email_domain = models.TextField() exclude_users_service_account = models.BooleanField(default=False) user_delete_action = models.TextField( - choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE + choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE ) group_delete_action = models.TextField( - choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE + choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE ) filter_group = models.ForeignKey( @@ -113,10 +105,10 @@ def component(self) -> str: @property def serializer(self) -> type[Serializer]: from authentik.enterprise.providers.google_workspace.api.providers import ( - GoogleProviderSerializer, + GoogleWorkspaceProviderSerializer, ) - return GoogleProviderSerializer + return GoogleWorkspaceProviderSerializer def __str__(self): return f"Google Workspace Provider {self.name}" @@ -136,10 +128,10 @@ def component(self) -> str: @property def serializer(self) -> type[Serializer]: from authentik.enterprise.providers.google_workspace.api.property_mappings import ( - GoogleProviderMappingSerializer, + GoogleWorkspaceProviderMappingSerializer, ) - return GoogleProviderMappingSerializer + return GoogleWorkspaceProviderMappingSerializer def __str__(self): return f"Google Workspace Provider Mapping {self.name}" @@ -149,7 +141,7 @@ class Meta: verbose_name_plural = _("Google Workspace Provider Mappings") -class GoogleWorkspaceProviderUser(models.Model): +class GoogleWorkspaceProviderUser(SerializerModel): """Mapping of a user and provider to a Google user ID""" id = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -157,14 +149,24 @@ class GoogleWorkspaceProviderUser(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.google_workspace.api.users import ( + GoogleWorkspaceProviderUserSerializer, + ) + + return GoogleWorkspaceProviderUserSerializer + class Meta: + verbose_name = _("Google Workspace Provider User") + verbose_name_plural = _("Google Workspace Provider Users") unique_together = (("google_id", "user", "provider"),) def __str__(self) -> str: - return f"Google Workspace User {self.user_id} to {self.provider_id}" + return f"Google Workspace Provider User {self.user_id} to {self.provider_id}" -class GoogleWorkspaceProviderGroup(models.Model): +class GoogleWorkspaceProviderGroup(SerializerModel): """Mapping of a group and provider to a Google group ID""" id = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -172,8 +174,18 @@ class GoogleWorkspaceProviderGroup(models.Model): group = models.ForeignKey(Group, on_delete=models.CASCADE) provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.google_workspace.api.groups import ( + GoogleWorkspaceProviderGroupSerializer, + ) + + return GoogleWorkspaceProviderGroupSerializer + class Meta: + verbose_name = _("Google Workspace Provider Group") + verbose_name_plural = _("Google Workspace Provider Groups") unique_together = (("google_id", "group", "provider"),) def __str__(self) -> str: - return f"Google Workspace Group {self.group_id} to {self.provider_id}" + return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}" diff --git a/authentik/enterprise/providers/google_workspace/tasks.py b/authentik/enterprise/providers/google_workspace/tasks.py index fa9804a78075..237076411a03 100644 --- a/authentik/enterprise/providers/google_workspace/tasks.py +++ b/authentik/enterprise/providers/google_workspace/tasks.py @@ -2,18 +2,21 @@ from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider from authentik.events.system_tasks import SystemTask +from authentik.lib.sync.outgoing.exceptions import TransientSyncException from authentik.lib.sync.outgoing.tasks import SyncTasks from authentik.root.celery import CELERY_APP sync_tasks = SyncTasks(GoogleWorkspaceProvider) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def google_workspace_sync_objects(*args, **kwargs): return sync_tasks.sync_objects(*args, **kwargs) -@CELERY_APP.task(base=SystemTask, bind=True) +@CELERY_APP.task( + base=SystemTask, bind=True, autoretry_for=(TransientSyncException,), retry_backoff=True +) def google_workspace_sync(self, provider_pk: int, *args, **kwargs): """Run full sync for Google Workspace provider""" return sync_tasks.sync_single(self, provider_pk, google_workspace_sync_objects) @@ -24,11 +27,11 @@ def google_workspace_sync_all(): return sync_tasks.sync_all(google_workspace_sync) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def google_workspace_sync_direct(*args, **kwargs): return sync_tasks.sync_signal_direct(*args, **kwargs) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def google_workspace_sync_m2m(*args, **kwargs): return sync_tasks.sync_signal_m2m(*args, **kwargs) diff --git a/authentik/enterprise/providers/google_workspace/tests/test_groups.py b/authentik/enterprise/providers/google_workspace/tests/test_groups.py index 7b2bccd0f9c0..6df1668f14bb 100644 --- a/authentik/enterprise/providers/google_workspace/tests/test_groups.py +++ b/authentik/enterprise/providers/google_workspace/tests/test_groups.py @@ -9,7 +9,6 @@ from authentik.core.tests.utils import create_test_user from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP from authentik.enterprise.providers.google_workspace.models import ( - GoogleWorkspaceDeleteAction, GoogleWorkspaceProvider, GoogleWorkspaceProviderGroup, GoogleWorkspaceProviderMapping, @@ -17,6 +16,7 @@ from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync from authentik.events.models import Event, EventAction from authentik.lib.generators import generate_id +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction from authentik.lib.tests.utils import load_fixture from authentik.tenants.models import Tenant @@ -240,7 +240,7 @@ def test_group_create_member_remove(self): def test_group_create_delete_do_nothing(self): """Test group deletion (delete action = do nothing)""" - self.provider.group_delete_action = GoogleWorkspaceDeleteAction.DO_NOTHING + self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING self.provider.save() uid = generate_id() http = MockHTTP() diff --git a/authentik/enterprise/providers/google_workspace/tests/test_users.py b/authentik/enterprise/providers/google_workspace/tests/test_users.py index c1fa7b5cc329..a19e1d359176 100644 --- a/authentik/enterprise/providers/google_workspace/tests/test_users.py +++ b/authentik/enterprise/providers/google_workspace/tests/test_users.py @@ -9,7 +9,6 @@ from authentik.core.models import Application, Group, User from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP from authentik.enterprise.providers.google_workspace.models import ( - GoogleWorkspaceDeleteAction, GoogleWorkspaceProvider, GoogleWorkspaceProviderMapping, GoogleWorkspaceProviderUser, @@ -17,6 +16,7 @@ from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync from authentik.events.models import Event, EventAction from authentik.lib.generators import generate_id +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction from authentik.lib.tests.utils import load_fixture from authentik.tenants.models import Tenant @@ -160,7 +160,7 @@ def test_user_create_delete(self): def test_user_create_delete_suspend(self): """Test user deletion (delete action = Suspend)""" - self.provider.user_delete_action = GoogleWorkspaceDeleteAction.SUSPEND + self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND self.provider.save() uid = generate_id() http = MockHTTP() @@ -209,7 +209,7 @@ def test_user_create_delete_suspend(self): def test_user_create_delete_do_nothing(self): """Test user deletion (delete action = do nothing)""" - self.provider.user_delete_action = GoogleWorkspaceDeleteAction.DO_NOTHING + self.provider.user_delete_action = OutgoingSyncDeleteAction.DO_NOTHING self.provider.save() uid = generate_id() http = MockHTTP() diff --git a/authentik/enterprise/providers/google_workspace/urls.py b/authentik/enterprise/providers/google_workspace/urls.py index 165e285cd21b..27a5ddd47569 100644 --- a/authentik/enterprise/providers/google_workspace/urls.py +++ b/authentik/enterprise/providers/google_workspace/urls.py @@ -1,11 +1,21 @@ """google provider urls""" +from authentik.enterprise.providers.google_workspace.api.groups import ( + GoogleWorkspaceProviderGroupViewSet, +) from authentik.enterprise.providers.google_workspace.api.property_mappings import ( - GoogleProviderMappingViewSet, + GoogleWorkspaceProviderMappingViewSet, +) +from authentik.enterprise.providers.google_workspace.api.providers import ( + GoogleWorkspaceProviderViewSet, +) +from authentik.enterprise.providers.google_workspace.api.users import ( + GoogleWorkspaceProviderUserViewSet, ) -from authentik.enterprise.providers.google_workspace.api.providers import GoogleProviderViewSet api_urlpatterns = [ - ("providers/google_workspace", GoogleProviderViewSet), - ("propertymappings/provider/google_workspace", GoogleProviderMappingViewSet), + ("providers/google_workspace", GoogleWorkspaceProviderViewSet), + ("providers/google_workspace_users", GoogleWorkspaceProviderUserViewSet), + ("providers/google_workspace_groups", GoogleWorkspaceProviderGroupViewSet), + ("propertymappings/provider/google_workspace", GoogleWorkspaceProviderMappingViewSet), ] diff --git a/authentik/enterprise/providers/microsoft_entra/__init__.py b/authentik/enterprise/providers/microsoft_entra/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/providers/microsoft_entra/api/__init__.py b/authentik/enterprise/providers/microsoft_entra/api/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/providers/microsoft_entra/api/groups.py b/authentik/enterprise/providers/microsoft_entra/api/groups.py new file mode 100644 index 000000000000..6934a841ce28 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/api/groups.py @@ -0,0 +1,33 @@ +"""MicrosoftEntraProviderGroup API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.sources import SourceSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.core.api.users import UserGroupSerializer +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup + + +class MicrosoftEntraProviderGroupSerializer(SourceSerializer): + """MicrosoftEntraProviderGroup Serializer""" + + group_obj = UserGroupSerializer(source="group", read_only=True) + + class Meta: + + model = MicrosoftEntraProviderGroup + fields = [ + "id", + "group", + "group_obj", + ] + + +class MicrosoftEntraProviderGroupViewSet(UsedByMixin, ModelViewSet): + """MicrosoftEntraProviderGroup Viewset""" + + queryset = MicrosoftEntraProviderGroup.objects.all().select_related("group") + serializer_class = MicrosoftEntraProviderGroupSerializer + filterset_fields = ["provider__id", "group__name", "group__group_uuid"] + search_fields = ["provider__name", "group__name"] + ordering = ["group__name"] diff --git a/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py b/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py new file mode 100644 index 000000000000..28fdacc6174d --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py @@ -0,0 +1,39 @@ +"""microsoft 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.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderMapping + + +class MicrosoftEntraProviderMappingSerializer(PropertyMappingSerializer): + """MicrosoftEntraProviderMapping Serializer""" + + class Meta: + model = MicrosoftEntraProviderMapping + fields = PropertyMappingSerializer.Meta.fields + + +class MicrosoftEntraProviderMappingFilter(FilterSet): + """Filter for MicrosoftEntraProviderMapping""" + + managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed")) + + class Meta: + model = MicrosoftEntraProviderMapping + fields = "__all__" + + +class MicrosoftEntraProviderMappingViewSet(UsedByMixin, ModelViewSet): + """MicrosoftEntraProviderMapping Viewset""" + + queryset = MicrosoftEntraProviderMapping.objects.all() + serializer_class = MicrosoftEntraProviderMappingSerializer + filterset_class = MicrosoftEntraProviderMappingFilter + search_fields = ["name"] + ordering = ["name"] diff --git a/authentik/enterprise/providers/microsoft_entra/api/providers.py b/authentik/enterprise/providers/microsoft_entra/api/providers.py new file mode 100644 index 000000000000..a5552c560ee9 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/api/providers.py @@ -0,0 +1,52 @@ +"""Microsoft Provider API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.providers import ProviderSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.enterprise.api import EnterpriseRequiredMixin +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider +from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync +from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin + + +class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer): + """MicrosoftEntraProvider Serializer""" + + class Meta: + model = MicrosoftEntraProvider + fields = [ + "pk", + "name", + "property_mappings", + "property_mappings_group", + "component", + "assigned_backchannel_application_slug", + "assigned_backchannel_application_name", + "verbose_name", + "verbose_name_plural", + "meta_model_name", + "client_id", + "client_secret", + "tenant_id", + "exclude_users_service_account", + "filter_group", + "user_delete_action", + "group_delete_action", + ] + extra_kwargs = {} + + +class MicrosoftEntraProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet): + """MicrosoftEntraProvider Viewset""" + + queryset = MicrosoftEntraProvider.objects.all() + serializer_class = MicrosoftEntraProviderSerializer + filterset_fields = [ + "name", + "exclude_users_service_account", + "filter_group", + ] + search_fields = ["name"] + ordering = ["name"] + sync_single_task = microsoft_entra_sync diff --git a/authentik/enterprise/providers/microsoft_entra/api/users.py b/authentik/enterprise/providers/microsoft_entra/api/users.py new file mode 100644 index 000000000000..5b5efbafb897 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/api/users.py @@ -0,0 +1,33 @@ +"""MicrosoftEntraProviderUser API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.groups import GroupMemberSerializer +from authentik.core.api.sources import SourceSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser + + +class MicrosoftEntraProviderUserSerializer(SourceSerializer): + """MicrosoftEntraProviderUser Serializer""" + + user_obj = GroupMemberSerializer(source="user", read_only=True) + + class Meta: + + model = MicrosoftEntraProviderUser + fields = [ + "id", + "user", + "user_obj", + ] + + +class MicrosoftEntraProviderUserViewSet(UsedByMixin, ModelViewSet): + """MicrosoftEntraProviderUser Viewset""" + + queryset = MicrosoftEntraProviderUser.objects.all().select_related("user") + serializer_class = MicrosoftEntraProviderUserSerializer + filterset_fields = ["provider__id", "user__username", "user__id"] + search_fields = ["provider__name", "user__username"] + ordering = ["user__username"] diff --git a/authentik/enterprise/providers/microsoft_entra/apps.py b/authentik/enterprise/providers/microsoft_entra/apps.py new file mode 100644 index 000000000000..f58b3045beec --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/apps.py @@ -0,0 +1,9 @@ +from authentik.enterprise.apps import EnterpriseConfig + + +class AuthentikEnterpriseProviderMicrosoftEntraConfig(EnterpriseConfig): + + name = "authentik.enterprise.providers.microsoft_entra" + label = "authentik_providers_microsoft_entra" + verbose_name = "authentik Enterprise.Providers.Microsoft Entra" + default = True diff --git a/authentik/enterprise/providers/microsoft_entra/clients/__init__.py b/authentik/enterprise/providers/microsoft_entra/clients/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/providers/microsoft_entra/clients/base.py b/authentik/enterprise/providers/microsoft_entra/clients/base.py new file mode 100644 index 000000000000..f2a374736382 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/clients/base.py @@ -0,0 +1,100 @@ +from asyncio import run +from collections.abc import Coroutine +from typing import Any + +from azure.core.exceptions import ( + ClientAuthenticationError, + ServiceRequestError, + ServiceResponseError, +) +from azure.identity.aio import ClientSecretCredential +from django.db.models import Model +from django.http import HttpResponseBadRequest, HttpResponseNotFound +from kiota_abstractions.api_error import APIError +from kiota_authentication_azure.azure_identity_authentication_provider import ( + AzureIdentityAuthenticationProvider, +) +from kiota_http.kiota_client_factory import KiotaClientFactory +from msgraph.generated.models.o_data_errors.o_data_error import ODataError +from msgraph.graph_request_adapter import GraphRequestAdapter, options +from msgraph.graph_service_client import GraphServiceClient +from msgraph_core import GraphClientFactory + +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider +from authentik.lib.sync.outgoing import HTTP_CONFLICT +from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient +from authentik.lib.sync.outgoing.exceptions import ( + BadRequestSyncException, + NotFoundSyncException, + ObjectExistsSyncException, + StopSync, + TransientSyncException, +) + + +def get_request_adapter( + credentials: ClientSecretCredential, scopes: list[str] | None = None +) -> GraphRequestAdapter: + if scopes: + auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes) + else: + auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials) + + return GraphRequestAdapter( + auth_provider=auth_provider, + client=GraphClientFactory.create_with_default_middleware( + options=options, client=KiotaClientFactory.get_default_client() + ), + ) + + +class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]( + BaseOutgoingSyncClient[TModel, TConnection, TSchema, MicrosoftEntraProvider] +): + """Base client for syncing to microsoft entra""" + + domains: list + + def __init__(self, provider: MicrosoftEntraProvider) -> None: + super().__init__(provider) + self.credentials = provider.microsoft_credentials() + self.__prefetch_domains() + + @property + def client(self): + return GraphServiceClient(request_adapter=get_request_adapter(**self.credentials)) + + def _request[T](self, request: Coroutine[Any, Any, T]) -> T: + try: + return run(request) + except ClientAuthenticationError as exc: + raise StopSync(exc, None, None) from exc + except ODataError as exc: + raise StopSync(exc, None, None) from exc + except (ServiceRequestError, ServiceResponseError) as exc: + raise TransientSyncException("Failed to sent request") from exc + except APIError as exc: + if exc.response_status_code == HttpResponseNotFound.status_code: + raise NotFoundSyncException("Object not found") from exc + if exc.response_status_code == HttpResponseBadRequest.status_code: + raise BadRequestSyncException("Bad request", exc.response_headers) from exc + if exc.response_status_code == HTTP_CONFLICT: + raise ObjectExistsSyncException("Object exists", exc.response_headers) from exc + raise exc + + def __prefetch_domains(self): + self.domains = [] + organizations = self._request(self.client.organization.get()) + next_link = True + while next_link: + for org in organizations.value: + self.domains.extend([x.name for x in org.verified_domains]) + next_link = organizations.odata_next_link + if not next_link: + break + organizations = self._request(self.client.organization.with_url(next_link).get()) + + def check_email_valid(self, *emails: str): + for email in emails: + if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains): + raise BadRequestSyncException(f"Invalid email domain: {email}") diff --git a/authentik/enterprise/providers/microsoft_entra/clients/groups.py b/authentik/enterprise/providers/microsoft_entra/clients/groups.py new file mode 100644 index 000000000000..eba540099845 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/clients/groups.py @@ -0,0 +1,241 @@ +from deepmerge import always_merger +from django.db import transaction +from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder +from msgraph.generated.models.group import Group as MSGroup +from msgraph.generated.models.reference_create import ReferenceCreate + +from authentik.core.expression.exceptions import ( + PropertyMappingExpressionException, + SkipObjectException, +) +from authentik.core.models import Group +from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient +from authentik.enterprise.providers.microsoft_entra.models import ( + MicrosoftEntraProviderGroup, + MicrosoftEntraProviderMapping, + MicrosoftEntraProviderUser, +) +from authentik.events.models import Event, EventAction +from authentik.lib.sync.outgoing.base import Direction +from authentik.lib.sync.outgoing.exceptions import ( + NotFoundSyncException, + ObjectExistsSyncException, + StopSync, + TransientSyncException, +) +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction +from authentik.lib.utils.errors import exception_to_string + + +class MicrosoftEntraGroupClient( + MicrosoftEntraSyncClient[Group, MicrosoftEntraProviderGroup, MSGroup] +): + """Microsoft client for groups""" + + connection_type = MicrosoftEntraProviderGroup + connection_type_query = "group" + can_discover = True + + def to_schema(self, obj: Group, creating: bool) -> MSGroup: + """Convert authentik group""" + raw_microsoft_group = {} + for mapping in ( + self.provider.property_mappings_group.all().order_by("name").select_subclasses() + ): + if not isinstance(mapping, MicrosoftEntraProviderMapping): + continue + try: + value = mapping.evaluate( + user=None, + request=None, + group=obj, + provider=self.provider, + creating=creating, + ) + if value is None: + continue + always_merger.merge(raw_microsoft_group, value) + except SkipObjectException as exc: + raise exc from exc + 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_microsoft_group: + raise StopSync(ValueError("No group mappings configured"), obj) + try: + return MSGroup(**raw_microsoft_group) + except TypeError as exc: + raise StopSync(exc, obj) from exc + + def delete(self, obj: Group): + """Delete group""" + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=obj + ).first() + if not microsoft_group: + self.logger.debug("Group does not exist in Microsoft, skipping") + return None + with transaction.atomic(): + if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: + self._request(self.client.groups.by_group_id(microsoft_group.microsoft_id).delete()) + microsoft_group.delete() + + def create(self, group: Group): + """Create group from scratch and create a connection object""" + microsoft_group = self.to_schema(group, True) + with transaction.atomic(): + try: + response = self._request(self.client.groups.post(microsoft_group)) + except ObjectExistsSyncException: + # group already exists in microsoft entra, so we can connect them manually + # for groups we need to fetch the group from microsoft as we connect on + # ID and not group email + query_params = GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( + filter=f"displayName eq '{microsoft_group.display_name}'", + ) + request_configuration = ( + GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( + query_parameters=query_params, + ) + ) + group_data = self._request(self.client.groups.get(request_configuration)) + if group_data.odata_count < 1: + self.logger.warning( + "Group which could not be created also does not exist", group=group + ) + return + return MicrosoftEntraProviderGroup.objects.create( + provider=self.provider, group=group, microsoft_id=group_data.value[0].id + ) + else: + return MicrosoftEntraProviderGroup.objects.create( + provider=self.provider, group=group, microsoft_id=response.id + ) + + def update(self, group: Group, connection: MicrosoftEntraProviderGroup): + """Update existing group""" + microsoft_group = self.to_schema(group, False) + microsoft_group.id = connection.microsoft_id + try: + return self._request( + self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group) + ) + except NotFoundSyncException: + # Resource missing is handled by self.write, which will re-create the group + raise + + def write(self, obj: Group): + microsoft_group, created = super().write(obj) + self.create_sync_members(obj, microsoft_group) + return microsoft_group, created + + def create_sync_members(self, obj: Group, microsoft_group: MicrosoftEntraProviderGroup): + """Sync all members after a group was created""" + users = list(obj.users.order_by("id").values_list("id", flat=True)) + connections = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user__pk__in=users + ).values_list("microsoft_id", flat=True) + self._patch(microsoft_group.microsoft_id, Direction.add, connections) + + def update_group(self, group: Group, action: Direction, users_set: set[int]): + """Update a groups members""" + if action == Direction.add: + return self._patch_add_users(group, users_set) + if action == Direction.remove: + return self._patch_remove_users(group, users_set) + + def _patch(self, microsoft_group_id: str, direction: Direction, members: list[str]): + for user in members: + try: + if direction == Direction.add: + request_body = ReferenceCreate( + odata_id=f"https://graph.microsoft.com/v1.0/directoryObjects/{user}", + ) + self._request( + self.client.groups.by_group_id(microsoft_group_id).members.ref.post( + request_body + ) + ) + if direction == Direction.remove: + self._request( + self.client.groups.by_group_id(microsoft_group_id) + .members.by_directory_object_id(user) + .ref.delete() + ) + except ObjectExistsSyncException: + pass + except TransientSyncException: + raise + + def _patch_add_users(self, group: Group, users_set: set[int]): + """Add users in users_set to group""" + if len(users_set) < 1: + return + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + if not microsoft_group: + self.logger.warning( + "could not sync group membership, group does not exist", group=group + ) + return + user_ids = list( + MicrosoftEntraProviderUser.objects.filter( + user__pk__in=users_set, provider=self.provider + ).values_list("microsoft_id", flat=True) + ) + if len(user_ids) < 1: + return + self._patch(microsoft_group.microsoft_id, Direction.add, 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 + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + if not microsoft_group: + self.logger.warning( + "could not sync group membership, group does not exist", group=group + ) + return + user_ids = list( + MicrosoftEntraProviderUser.objects.filter( + user__pk__in=users_set, provider=self.provider + ).values_list("microsoft_id", flat=True) + ) + if len(user_ids) < 1: + return + self._patch(microsoft_group.microsoft_id, Direction.remove, user_ids) + + def discover(self): + """Iterate through all groups and connect them with authentik groups if possible""" + groups = self._request(self.client.groups.get()) + next_link = True + while next_link: + for group in groups.value: + self._discover_single_group(group) + next_link = groups.odata_next_link + if not next_link: + break + groups = self._request(self.client.groups.with_url(next_link).get()) + + def _discover_single_group(self, group: MSGroup): + """handle discovery of a single group""" + microsoft_name = group.unique_name + matching_authentik_group = ( + self.provider.get_object_qs(Group).filter(name=microsoft_name).first() + ) + if not matching_authentik_group: + return + MicrosoftEntraProviderGroup.objects.get_or_create( + provider=self.provider, + group=matching_authentik_group, + microsoft_id=group.id, + ) diff --git a/authentik/enterprise/providers/microsoft_entra/clients/users.py b/authentik/enterprise/providers/microsoft_entra/clients/users.py new file mode 100644 index 000000000000..99c682175e6a --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/clients/users.py @@ -0,0 +1,150 @@ +from deepmerge import always_merger +from django.db import transaction +from msgraph.generated.models.user import User as MSUser +from msgraph.generated.users.users_request_builder import UsersRequestBuilder + +from authentik.core.expression.exceptions import ( + PropertyMappingExpressionException, + SkipObjectException, +) +from authentik.core.models import User +from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient +from authentik.enterprise.providers.microsoft_entra.models import ( + MicrosoftEntraProviderMapping, + MicrosoftEntraProviderUser, +) +from authentik.events.models import Event, EventAction +from authentik.lib.sync.outgoing.exceptions import ( + ObjectExistsSyncException, + StopSync, + TransientSyncException, +) +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction +from authentik.lib.utils.errors import exception_to_string +from authentik.policies.utils import delete_none_values + + +class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]): + """Sync authentik users into microsoft entra""" + + connection_type = MicrosoftEntraProviderUser + connection_type_query = "user" + can_discover = True + + def to_schema(self, obj: User, creating: bool) -> MSUser: + """Convert authentik user""" + raw_microsoft_user = {} + for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses(): + if not isinstance(mapping, MicrosoftEntraProviderMapping): + continue + try: + value = mapping.evaluate( + user=obj, + request=None, + provider=self.provider, + creating=creating, + ) + if value is None: + continue + always_merger.merge(raw_microsoft_user, value) + except SkipObjectException as exc: + raise exc from exc + 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_microsoft_user: + raise StopSync(ValueError("No user mappings configured"), obj) + try: + return MSUser(**delete_none_values(raw_microsoft_user)) + except TypeError as exc: + raise StopSync(exc, obj) from exc + + def delete(self, obj: User): + """Delete user""" + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=obj + ).first() + if not microsoft_user: + self.logger.debug("User does not exist in Microsoft, skipping") + return None + with transaction.atomic(): + response = None + if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE: + response = self._request( + self.client.users.by_user_id(microsoft_user.microsoft_id).delete() + ) + elif self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND: + response = self._request( + self.client.users.by_user_id(microsoft_user.microsoft_id).patch( + MSUser(account_enabled=False) + ) + ) + microsoft_user.delete() + return response + + def create(self, user: User): + """Create user from scratch and create a connection object""" + microsoft_user = self.to_schema(user, True) + self.check_email_valid(microsoft_user.user_principal_name) + with transaction.atomic(): + try: + response = self._request(self.client.users.post(microsoft_user)) + except ObjectExistsSyncException: + # user already exists in microsoft entra, so we can connect them manually + query_params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters()( + filter=f"mail eq '{microsoft_user.mail}'", + ) + request_configuration = ( + UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( + query_parameters=query_params, + ) + ) + user_data = self._request(self.client.users.get(request_configuration)) + if user_data.odata_count < 1: + self.logger.warning( + "User which could not be created also does not exist", user=user + ) + return + return MicrosoftEntraProviderUser.objects.create( + provider=self.provider, user=user, microsoft_id=user_data.value[0].id + ) + except TransientSyncException as exc: + raise exc + else: + return MicrosoftEntraProviderUser.objects.create( + provider=self.provider, user=user, microsoft_id=response.id + ) + + def update(self, user: User, connection: MicrosoftEntraProviderUser): + """Update existing user""" + microsoft_user = self.to_schema(user, False) + self.check_email_valid(microsoft_user.user_principal_name) + self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)) + + def discover(self): + """Iterate through all users and connect them with authentik users if possible""" + users = self._request(self.client.users.get()) + next_link = True + while next_link: + for user in users.value: + self._discover_single_user(user) + next_link = users.odata_next_link + if not next_link: + break + users = self._request(self.client.users.with_url(next_link).get()) + + def _discover_single_user(self, user: MSUser): + """handle discovery of a single user""" + matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first() + if not matching_authentik_user: + return + MicrosoftEntraProviderUser.objects.get_or_create( + provider=self.provider, + user=matching_authentik_user, + microsoft_id=user.id, + ) diff --git a/authentik/enterprise/providers/microsoft_entra/migrations/0001_initial.py b/authentik/enterprise/providers/microsoft_entra/migrations/0001_initial.py new file mode 100644 index 000000000000..97bb770b700d --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/migrations/0001_initial.py @@ -0,0 +1,165 @@ +# Generated by Django 5.0.6 on 2024-05-08 14:35 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_core", "0035_alter_group_options_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="MicrosoftEntraProviderMapping", + 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": "Microsoft Entra Provider Mapping", + "verbose_name_plural": "Microsoft Entra Provider Mappings", + }, + bases=("authentik_core.propertymapping",), + ), + migrations.CreateModel( + name="MicrosoftEntraProvider", + 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", + ), + ), + ("client_id", models.TextField()), + ("client_secret", models.TextField()), + ("tenant_id", models.TextField()), + ("exclude_users_service_account", models.BooleanField(default=False)), + ( + "user_delete_action", + models.TextField( + choices=[ + ("do_nothing", "Do Nothing"), + ("delete", "Delete"), + ("suspend", "Suspend"), + ], + default="delete", + ), + ), + ( + "group_delete_action", + models.TextField( + choices=[ + ("do_nothing", "Do Nothing"), + ("delete", "Delete"), + ("suspend", "Suspend"), + ], + default="delete", + ), + ), + ( + "filter_group", + models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="authentik_core.group", + ), + ), + ( + "property_mappings_group", + models.ManyToManyField( + blank=True, + default=None, + help_text="Property mappings used for group creation/updating.", + to="authentik_core.propertymapping", + ), + ), + ], + options={ + "verbose_name": "Microsoft Entra Provider", + "verbose_name_plural": "Microsoft Entra Providers", + }, + bases=("authentik_core.provider", models.Model), + ), + migrations.CreateModel( + name="MicrosoftEntraProviderGroup", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("microsoft_id", models.TextField()), + ( + "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_microsoft_entra.microsoftentraprovider", + ), + ), + ], + options={ + "verbose_name": "Microsoft Entra Provider Group", + "verbose_name_plural": "Microsoft Entra Provider Groups", + "unique_together": {("microsoft_id", "group", "provider")}, + }, + ), + migrations.CreateModel( + name="MicrosoftEntraProviderUser", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("microsoft_id", models.TextField()), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_microsoft_entra.microsoftentraprovider", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "verbose_name": "Microsoft Entra Provider User", + "verbose_name_plural": "Microsoft Entra Provider User", + "unique_together": {("microsoft_id", "user", "provider")}, + }, + ), + ] diff --git a/authentik/enterprise/providers/microsoft_entra/migrations/__init__.py b/authentik/enterprise/providers/microsoft_entra/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/providers/microsoft_entra/models.py b/authentik/enterprise/providers/microsoft_entra/models.py new file mode 100644 index 000000000000..3cee3bf15dfa --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/models.py @@ -0,0 +1,180 @@ +"""Microsoft Entra sync provider""" + +from typing import Any, Self +from uuid import uuid4 + +from azure.identity.aio import ClientSecretCredential +from django.db import models +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer + +from authentik.core.models import ( + BackchannelProvider, + Group, + PropertyMapping, + User, + UserTypes, +) +from authentik.lib.models import SerializerModel +from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider + + +class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): + """Sync users from authentik into Microsoft Entra.""" + + client_id = models.TextField() + client_secret = models.TextField() + tenant_id = models.TextField() + + exclude_users_service_account = models.BooleanField(default=False) + user_delete_action = models.TextField( + choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE + ) + group_delete_action = models.TextField( + choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE + ) + filter_group = models.ForeignKey( + "authentik_core.group", on_delete=models.SET_DEFAULT, default=None, null=True + ) + + property_mappings_group = models.ManyToManyField( + PropertyMapping, + default=None, + blank=True, + help_text=_("Property mappings used for group creation/updating."), + ) + + def client_for_model( + self, model: type[User | Group] + ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: + if issubclass(model, User): + from authentik.enterprise.providers.microsoft_entra.clients.users import ( + MicrosoftEntraUserClient, + ) + + return MicrosoftEntraUserClient(self) + if issubclass(model, Group): + from authentik.enterprise.providers.microsoft_entra.clients.groups import ( + MicrosoftEntraGroupClient, + ) + + return MicrosoftEntraGroupClient(self) + raise ValueError(f"Invalid model {model}") + + def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]: + if type == User: + # Get queryset of all users with consistent ordering + # according to the provider's settings + base = User.objects.all().exclude_anonymous() + if self.exclude_users_service_account: + base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude( + type=UserTypes.INTERNAL_SERVICE_ACCOUNT + ) + if self.filter_group: + base = base.filter(ak_groups__in=[self.filter_group]) + return base.order_by("pk") + if type == Group: + # Get queryset of all groups with consistent ordering + return Group.objects.all().order_by("pk") + raise ValueError(f"Invalid type {type}") + + def microsoft_credentials(self): + return { + "credentials": ClientSecretCredential( + self.tenant_id, self.client_id, self.client_secret + ) + } + + @property + def component(self) -> str: + return "ak-provider-microsoft-entra-form" + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.microsoft_entra.api.providers import ( + MicrosoftEntraProviderSerializer, + ) + + return MicrosoftEntraProviderSerializer + + def __str__(self): + return f"Microsoft Entra Provider {self.name}" + + class Meta: + verbose_name = _("Microsoft Entra Provider") + verbose_name_plural = _("Microsoft Entra Providers") + + +class MicrosoftEntraProviderMapping(PropertyMapping): + """Map authentik data to outgoing Microsoft requests""" + + @property + def component(self) -> str: + return "ak-property-mapping-microsoft-entra-form" + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.microsoft_entra.api.property_mappings import ( + MicrosoftEntraProviderMappingSerializer, + ) + + return MicrosoftEntraProviderMappingSerializer + + def __str__(self): + return f"Microsoft Entra Provider Mapping {self.name}" + + class Meta: + verbose_name = _("Microsoft Entra Provider Mapping") + verbose_name_plural = _("Microsoft Entra Provider Mappings") + + +class MicrosoftEntraProviderUser(SerializerModel): + """Mapping of a user and provider to a Microsoft user ID""" + + id = models.UUIDField(primary_key=True, editable=False, default=uuid4) + microsoft_id = models.TextField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.microsoft_entra.api.users import ( + MicrosoftEntraProviderUserSerializer, + ) + + return MicrosoftEntraProviderUserSerializer + + class Meta: + verbose_name = _("Microsoft Entra Provider User") + verbose_name_plural = _("Microsoft Entra Provider User") + unique_together = (("microsoft_id", "user", "provider"),) + + def __str__(self) -> str: + return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}" + + +class MicrosoftEntraProviderGroup(SerializerModel): + """Mapping of a group and provider to a Microsoft group ID""" + + id = models.UUIDField(primary_key=True, editable=False, default=uuid4) + microsoft_id = models.TextField() + group = models.ForeignKey(Group, on_delete=models.CASCADE) + provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.microsoft_entra.api.groups import ( + MicrosoftEntraProviderGroupSerializer, + ) + + return MicrosoftEntraProviderGroupSerializer + + class Meta: + verbose_name = _("Microsoft Entra Provider Group") + verbose_name_plural = _("Microsoft Entra Provider Groups") + unique_together = (("microsoft_id", "group", "provider"),) + + def __str__(self) -> str: + return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}" diff --git a/authentik/enterprise/providers/microsoft_entra/settings.py b/authentik/enterprise/providers/microsoft_entra/settings.py new file mode 100644 index 000000000000..08ef592de864 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/settings.py @@ -0,0 +1,13 @@ +"""Microsoft Entra provider task Settings""" + +from celery.schedules import crontab + +from authentik.lib.utils.time import fqdn_rand + +CELERY_BEAT_SCHEDULE = { + "providers_microsoft_entra_sync": { + "task": "authentik.enterprise.providers.microsoft_entra.tasks.microsoft_entra_sync_all", + "schedule": crontab(minute=fqdn_rand("microsoft_entra_sync_all"), hour="*/4"), + "options": {"queue": "authentik_scheduled"}, + }, +} diff --git a/authentik/enterprise/providers/microsoft_entra/signals.py b/authentik/enterprise/providers/microsoft_entra/signals.py new file mode 100644 index 000000000000..b9063ccb8bab --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/signals.py @@ -0,0 +1,16 @@ +"""Microsoft provider signals""" + +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider +from authentik.enterprise.providers.microsoft_entra.tasks import ( + microsoft_entra_sync, + microsoft_entra_sync_direct, + microsoft_entra_sync_m2m, +) +from authentik.lib.sync.outgoing.signals import register_signals + +register_signals( + MicrosoftEntraProvider, + task_sync_single=microsoft_entra_sync, + task_sync_direct=microsoft_entra_sync_direct, + task_sync_m2m=microsoft_entra_sync_m2m, +) diff --git a/authentik/enterprise/providers/microsoft_entra/tasks.py b/authentik/enterprise/providers/microsoft_entra/tasks.py new file mode 100644 index 000000000000..6985b8acfa39 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/tasks.py @@ -0,0 +1,37 @@ +"""Microsoft Entra Provider tasks""" + +from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider +from authentik.events.system_tasks import SystemTask +from authentik.lib.sync.outgoing.exceptions import TransientSyncException +from authentik.lib.sync.outgoing.tasks import SyncTasks +from authentik.root.celery import CELERY_APP + +sync_tasks = SyncTasks(MicrosoftEntraProvider) + + +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) +def microsoft_entra_sync_objects(*args, **kwargs): + return sync_tasks.sync_objects(*args, **kwargs) + + +@CELERY_APP.task( + base=SystemTask, bind=True, autoretry_for=(TransientSyncException,), retry_backoff=True +) +def microsoft_entra_sync(self, provider_pk: int, *args, **kwargs): + """Run full sync for Microsoft Entra provider""" + return sync_tasks.sync_single(self, provider_pk, microsoft_entra_sync_objects) + + +@CELERY_APP.task() +def microsoft_entra_sync_all(): + return sync_tasks.sync_all(microsoft_entra_sync) + + +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) +def microsoft_entra_sync_direct(*args, **kwargs): + return sync_tasks.sync_signal_direct(*args, **kwargs) + + +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) +def microsoft_entra_sync_m2m(*args, **kwargs): + return sync_tasks.sync_signal_m2m(*args, **kwargs) diff --git a/authentik/enterprise/providers/microsoft_entra/tests/__init__.py b/authentik/enterprise/providers/microsoft_entra/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/providers/microsoft_entra/tests/test_groups.py b/authentik/enterprise/providers/microsoft_entra/tests/test_groups.py new file mode 100644 index 000000000000..8675374d7805 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/tests/test_groups.py @@ -0,0 +1,392 @@ +"""Microsoft Entra Group tests""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from azure.identity.aio import ClientSecretCredential +from django.test import TestCase +from msgraph.generated.models.group import Group as MSGroup +from msgraph.generated.models.group_collection_response import GroupCollectionResponse +from msgraph.generated.models.organization import Organization +from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse +from msgraph.generated.models.user import User as MSUser +from msgraph.generated.models.user_collection_response import UserCollectionResponse +from msgraph.generated.models.verified_domain import VerifiedDomain + +from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Application, Group, User +from authentik.core.tests.utils import create_test_user +from authentik.enterprise.providers.microsoft_entra.models import ( + MicrosoftEntraProvider, + MicrosoftEntraProviderGroup, + MicrosoftEntraProviderMapping, + MicrosoftEntraProviderUser, +) +from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync +from authentik.events.models import Event, EventAction +from authentik.lib.generators import generate_id +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction +from authentik.tenants.models import Tenant + + +class MicrosoftEntraGroupTests(TestCase): + """Microsoft Entra Group tests""" + + @apply_blueprint("system/providers-microsoft-entra.yaml") + def setUp(self) -> None: + # Delete all groups and groups as the mocked HTTP responses only return one ID + # which will cause errors with multiple groups + Tenant.objects.update(avatars="none") + User.objects.all().exclude_anonymous().delete() + Group.objects.all().delete() + self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create( + name=generate_id(), + client_id=generate_id(), + client_secret=generate_id(), + tenant_id=generate_id(), + exclude_users_service_account=True, + ) + self.app: Application = Application.objects.create( + name=generate_id(), + slug=generate_id(), + ) + self.app.backchannel_providers.add(self.provider) + self.provider.property_mappings.add( + MicrosoftEntraProviderMapping.objects.get( + managed="goauthentik.io/providers/microsoft_entra/user" + ) + ) + self.provider.property_mappings_group.add( + MicrosoftEntraProviderMapping.objects.get( + managed="goauthentik.io/providers/microsoft_entra/group" + ) + ) + self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id()) + + def test_group_create(self): + """Test group creation""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=generate_id())), + ) as group_create, + ): + group = Group.objects.create(name=uid) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + group_create.assert_called_once() + + def test_group_create_update(self): + """Test group updating""" + uid = generate_id() + ext_id = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=ext_id)), + ) as group_create, + patch( + "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch", + AsyncMock(return_value=MSGroup(id=ext_id)), + ) as group_patch, + ): + group = Group.objects.create(name=uid) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + + group.name = "new name" + group.save() + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + group_create.assert_called_once() + group_patch.assert_called_once() + + def test_group_create_delete(self): + """Test group deletion""" + uid = generate_id() + ext_id = generate_id() + with ( + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=ext_id)), + ) as group_create, + patch( + "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete", + AsyncMock(return_value=MSGroup(id=ext_id)), + ) as group_delete, + ): + group = Group.objects.create(name=uid) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + + group.delete() + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + group_create.assert_called_once() + group_delete.assert_called_once() + + def test_group_create_member_add(self): + """Test group creation""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=uid)), + ) as group_create, + patch( + "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post", + AsyncMock(), + ) as member_add, + ): + user = create_test_user(uid) + group = Group.objects.create(name=uid) + group.users.add(user) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_create.assert_called_once() + group_create.assert_called_once() + member_add.assert_called_once() + self.assertEqual( + member_add.call_args[0][0].odata_id, + f"https://graph.microsoft.com/v1.0/directoryObjects/{MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, + ).first().microsoft_id}", + ) + + def test_group_create_member_remove(self): + """Test group creation""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=uid)), + ) as group_create, + patch( + "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post", + AsyncMock(), + ) as member_add, + patch( + "msgraph.generated.groups.item.members.item.ref.ref_request_builder.RefRequestBuilder.delete", + AsyncMock(), + ) as member_remove, + ): + user = create_test_user(uid) + group = Group.objects.create(name=uid) + group.users.add(user) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + group.users.remove(user) + + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_create.assert_called_once() + group_create.assert_called_once() + member_add.assert_called_once() + self.assertEqual( + member_add.call_args[0][0].odata_id, + f"https://graph.microsoft.com/v1.0/directoryObjects/{MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, + ).first().microsoft_id}", + ) + member_remove.assert_called_once() + + def test_group_create_delete_do_nothing(self): + """Test group deletion (delete action = do nothing)""" + self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING + self.provider.save() + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=uid)), + ) as group_create, + patch( + "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete", + AsyncMock(return_value=MSGroup(id=uid)), + ) as group_delete, + ): + group = Group.objects.create(name=uid) + microsoft_group = MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group=group + ).first() + self.assertIsNotNone(microsoft_group) + + group.delete() + self.assertFalse( + MicrosoftEntraProviderGroup.objects.filter( + provider=self.provider, group__name=uid + ).exists() + ) + group_create.assert_called_once() + group_delete.assert_not_called() + + def test_sync_task(self): + """Test group discovery""" + uid = generate_id() + self.app.backchannel_providers.remove(self.provider) + different_group = Group.objects.create( + name=uid, + ) + self.app.backchannel_providers.add(self.provider) + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ), + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post", + AsyncMock(return_value=MSGroup(id=generate_id())), + ), + patch( + "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch", + AsyncMock(return_value=MSGroup(id=uid)), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get", + AsyncMock( + return_value=UserCollectionResponse( + value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)] + ) + ), + ) as user_list, + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get", + AsyncMock( + return_value=GroupCollectionResponse( + value=[MSGroup(display_name=uid, unique_name=uid, id=uid)] + ) + ), + ) as group_list, + ): + microsoft_entra_sync.delay(self.provider.pk).get() + self.assertTrue( + MicrosoftEntraProviderGroup.objects.filter( + group=different_group, provider=self.provider + ).exists() + ) + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_list.assert_called_once() + group_list.assert_called_once() diff --git a/authentik/enterprise/providers/microsoft_entra/tests/test_users.py b/authentik/enterprise/providers/microsoft_entra/tests/test_users.py new file mode 100644 index 000000000000..adef82f29e16 --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/tests/test_users.py @@ -0,0 +1,337 @@ +"""Microsoft Entra User tests""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from azure.identity.aio import ClientSecretCredential +from django.test import TestCase +from msgraph.generated.models.group_collection_response import GroupCollectionResponse +from msgraph.generated.models.organization import Organization +from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse +from msgraph.generated.models.user import User as MSUser +from msgraph.generated.models.user_collection_response import UserCollectionResponse +from msgraph.generated.models.verified_domain import VerifiedDomain + +from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Application, Group, User +from authentik.enterprise.providers.microsoft_entra.models import ( + MicrosoftEntraProvider, + MicrosoftEntraProviderMapping, + MicrosoftEntraProviderUser, +) +from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync +from authentik.events.models import Event, EventAction +from authentik.lib.generators import generate_id +from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction +from authentik.tenants.models import Tenant + + +class MicrosoftEntraUserTests(TestCase): + """Microsoft Entra User tests""" + + @apply_blueprint("system/providers-microsoft-entra.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 + Tenant.objects.update(avatars="none") + User.objects.all().exclude_anonymous().delete() + Group.objects.all().delete() + self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create( + name=generate_id(), + client_id=generate_id(), + client_secret=generate_id(), + tenant_id=generate_id(), + exclude_users_service_account=True, + ) + self.app: Application = Application.objects.create( + name=generate_id(), + slug=generate_id(), + ) + self.app.backchannel_providers.add(self.provider) + self.provider.property_mappings.add( + MicrosoftEntraProviderMapping.objects.get( + managed="goauthentik.io/providers/microsoft_entra/user" + ) + ) + self.provider.property_mappings_group.add( + MicrosoftEntraProviderMapping.objects.get( + managed="goauthentik.io/providers/microsoft_entra/group" + ) + ) + self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id()) + + def test_user_create(self): + """Test user creation""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + ): + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=user + ).first() + self.assertIsNotNone(microsoft_user) + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_create.assert_called_once() + + def test_user_create_update(self): + """Test user updating""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_patch, + ): + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=user + ).first() + self.assertIsNotNone(microsoft_user) + + user.name = "new name" + user.save() + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_create.assert_called_once() + user_patch.assert_called_once() + + def test_user_create_delete(self): + """Test user deletion""" + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete", + AsyncMock(), + ) as user_delete, + ): + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=user + ).first() + self.assertIsNotNone(microsoft_user) + + user.delete() + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_create.assert_called_once() + user_delete.assert_called_once() + + def test_user_create_delete_suspend(self): + """Test user deletion (delete action = Suspend)""" + self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND + self.provider.save() + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_patch, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete", + AsyncMock(), + ) as user_delete, + ): + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=user + ).first() + self.assertIsNotNone(microsoft_user) + + user.delete() + self.assertFalse( + MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user__username=uid + ).exists() + ) + user_create.assert_called_once() + user_patch.assert_called_once() + self.assertFalse(user_patch.call_args[0][0].account_enabled) + user_delete.assert_not_called() + + def test_user_create_delete_do_nothing(self): + """Test user deletion (delete action = do nothing)""" + self.provider.user_delete_action = OutgoingSyncDeleteAction.DO_NOTHING + self.provider.save() + uid = generate_id() + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_create, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ) as user_patch, + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete", + AsyncMock(), + ) as user_delete, + ): + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + microsoft_user = MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user=user + ).first() + self.assertIsNotNone(microsoft_user) + + user.delete() + self.assertFalse( + MicrosoftEntraProviderUser.objects.filter( + provider=self.provider, user__username=uid + ).exists() + ) + user_create.assert_called_once() + user_patch.assert_not_called() + user_delete.assert_not_called() + + def test_sync_task(self): + """Test user discovery""" + uid = generate_id() + self.app.backchannel_providers.remove(self.provider) + different_user = User.objects.create( + username=uid, + email=f"{uid}@goauthentik.io", + ) + self.app.backchannel_providers.add(self.provider) + with ( + patch( + "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", + MagicMock(return_value={"credentials": self.creds}), + ), + patch( + "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", + AsyncMock( + return_value=OrganizationCollectionResponse( + value=[ + Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) + ] + ) + ), + ), + patch( + "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch", + AsyncMock(return_value=MSUser(id=generate_id())), + ), + patch( + "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get", + AsyncMock( + return_value=UserCollectionResponse( + value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)] + ) + ), + ) as user_list, + patch( + "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get", + AsyncMock(return_value=GroupCollectionResponse(value=[])), + ), + ): + microsoft_entra_sync.delay(self.provider.pk).get() + self.assertTrue( + MicrosoftEntraProviderUser.objects.filter( + user=different_user, provider=self.provider + ).exists() + ) + self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) + user_list.assert_called_once() diff --git a/authentik/enterprise/providers/microsoft_entra/urls.py b/authentik/enterprise/providers/microsoft_entra/urls.py new file mode 100644 index 000000000000..269e5bfc988d --- /dev/null +++ b/authentik/enterprise/providers/microsoft_entra/urls.py @@ -0,0 +1,21 @@ +"""microsoft provider urls""" + +from authentik.enterprise.providers.microsoft_entra.api.groups import ( + MicrosoftEntraProviderGroupViewSet, +) +from authentik.enterprise.providers.microsoft_entra.api.property_mappings import ( + MicrosoftEntraProviderMappingViewSet, +) +from authentik.enterprise.providers.microsoft_entra.api.providers import ( + MicrosoftEntraProviderViewSet, +) +from authentik.enterprise.providers.microsoft_entra.api.users import ( + MicrosoftEntraProviderUserViewSet, +) + +api_urlpatterns = [ + ("providers/microsoft_entra", MicrosoftEntraProviderViewSet), + ("providers/microsoft_entra_users", MicrosoftEntraProviderUserViewSet), + ("providers/microsoft_entra_groups", MicrosoftEntraProviderGroupViewSet), + ("propertymappings/provider/microsoft_entra", MicrosoftEntraProviderMappingViewSet), +] diff --git a/authentik/enterprise/settings.py b/authentik/enterprise/settings.py index dcb72b23e54a..0a35d52d3572 100644 --- a/authentik/enterprise/settings.py +++ b/authentik/enterprise/settings.py @@ -15,6 +15,7 @@ TENANT_APPS = [ "authentik.enterprise.audit", "authentik.enterprise.providers.google_workspace", + "authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.rac", "authentik.enterprise.stages.source", ] diff --git a/authentik/lib/logging.py b/authentik/lib/logging.py index 08d6073c1ba3..60309658c489 100644 --- a/authentik/lib/logging.py +++ b/authentik/lib/logging.py @@ -101,6 +101,7 @@ def get_logger_config(): "uvicorn": "WARNING", "gunicorn": "INFO", "requests_mock": "WARNING", + "hpack": "WARNING", } for handler_name, level in handler_level_map.items(): base_config["loggers"][handler_name] = { diff --git a/authentik/lib/sync/outgoing/base.py b/authentik/lib/sync/outgoing/base.py index a10278ebe735..cb04bb329358 100644 --- a/authentik/lib/sync/outgoing/base.py +++ b/authentik/lib/sync/outgoing/base.py @@ -39,26 +39,25 @@ def create(self, obj: TModel) -> TConnection: """Create object in remote destination""" raise NotImplementedError() - def update(self, obj: TModel, connection: object): + def update(self, obj: TModel, connection: TConnection): """Update object in remote destination""" raise NotImplementedError() def write(self, obj: TModel) -> tuple[TConnection, bool]: """Write object to destination. Uses self.create and self.update, but can be overwritten for further logic""" - remote_obj = self.connection_type.objects.filter( + connection = self.connection_type.objects.filter( provider=self.provider, **{self.connection_type_query: obj} ).first() - connection: TConnection | None = None try: - if not remote_obj: + if not connection: connection = self.create(obj) return connection, True try: - self.update(obj, remote_obj) - return remote_obj, False + self.update(obj, connection) + return connection, False except NotFoundSyncException: - remote_obj.delete() + connection.delete() connection = self.create(obj) return connection, True except DatabaseError as exc: @@ -71,7 +70,7 @@ def delete(self, obj: TModel): """Delete object from destination""" raise NotImplementedError() - def to_schema(self, obj: TModel) -> TSchema: + def to_schema(self, obj: TModel, creating: bool) -> TSchema: """Convert object to destination schema""" raise NotImplementedError() diff --git a/authentik/lib/sync/outgoing/exceptions.py b/authentik/lib/sync/outgoing/exceptions.py index e7f621c3fae8..843436821860 100644 --- a/authentik/lib/sync/outgoing/exceptions.py +++ b/authentik/lib/sync/outgoing/exceptions.py @@ -17,6 +17,10 @@ class ObjectExistsSyncException(BaseSyncException): """Exception when an object already exists in the remote system""" +class BadRequestSyncException(BaseSyncException): + """Exception when invalid data was sent to the remote system""" + + class StopSync(BaseSyncException): """Exception raised when a configuration error should stop the sync process""" diff --git a/authentik/lib/sync/outgoing/models.py b/authentik/lib/sync/outgoing/models.py index e9f1680b73fd..1e4c598c2d02 100644 --- a/authentik/lib/sync/outgoing/models.py +++ b/authentik/lib/sync/outgoing/models.py @@ -1,7 +1,7 @@ from typing import Any, Self from django.core.cache import cache -from django.db.models import Model, QuerySet +from django.db.models import Model, QuerySet, TextChoices from redis.lock import Lock from authentik.core.models import Group, User @@ -9,6 +9,15 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient +class OutgoingSyncDeleteAction(TextChoices): + """Action taken when a user/group is deleted in authentik. Suspend is not available for groups, + and will be treated as `do_nothing`""" + + DO_NOTHING = "do_nothing" + DELETE = "delete" + SUSPEND = "suspend" + + class OutgoingSyncProvider(Model): class Meta: diff --git a/authentik/lib/sync/outgoing/signals.py b/authentik/lib/sync/outgoing/signals.py index f01353a91e9f..3fa448b24d03 100644 --- a/authentik/lib/sync/outgoing/signals.py +++ b/authentik/lib/sync/outgoing/signals.py @@ -47,7 +47,7 @@ def model_pre_delete(sender: type[Model], instance: User | Group, **_): return task_sync_direct.delay( class_to_path(instance.__class__), instance.pk, Direction.remove.value - ) + ).get() pre_delete.connect(model_pre_delete, User, dispatch_uid=uid, weak=False) pre_delete.connect(model_pre_delete, Group, dispatch_uid=uid, weak=False) diff --git a/authentik/lib/sync/outgoing/tasks.py b/authentik/lib/sync/outgoing/tasks.py index 1013bc9b1ca0..704378b27e53 100644 --- a/authentik/lib/sync/outgoing/tasks.py +++ b/authentik/lib/sync/outgoing/tasks.py @@ -14,7 +14,11 @@ from authentik.events.system_tasks import SystemTask from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT from authentik.lib.sync.outgoing.base import Direction -from authentik.lib.sync.outgoing.exceptions import StopSync, TransientSyncException +from authentik.lib.sync.outgoing.exceptions import ( + BadRequestSyncException, + StopSync, + TransientSyncException, +) from authentik.lib.sync.outgoing.models import OutgoingSyncProvider from authentik.lib.utils.reflection import class_to_path, path_to_class @@ -121,6 +125,27 @@ def sync_objects(self, object_type: str, page: int, provider_pk: int): client.write(obj) except SkipObjectException: continue + except BadRequestSyncException as exc: + self.logger.warning("failed to sync object", exc=exc, obj=obj) + messages.append( + LogEvent( + _( + ( + "Failed to sync {object_type} {object_name} " + "due to error: {error}" + ).format_map( + { + "object_type": obj._meta.verbose_name, + "object_name": str(obj), + "error": str(exc), + } + ) + ), + log_level="warning", + logger="", + attributes={"arguments": exc.args[1:]}, + ) + ) except TransientSyncException as exc: self.logger.warning("failed to sync object", exc=exc, user=obj) messages.append( diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py index fc8ea30d3efe..7b95d1184684 100644 --- a/authentik/providers/scim/clients/groups.py +++ b/authentik/providers/scim/clients/groups.py @@ -34,7 +34,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]): connection_type = SCIMGroup connection_type_query = "group" - def to_schema(self, obj: Group) -> SCIMGroupSchema: + def to_schema(self, obj: Group, creating: bool) -> SCIMGroupSchema: """Convert authentik user into SCIM""" raw_scim_group = { "schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",), @@ -51,6 +51,7 @@ def to_schema(self, obj: Group) -> SCIMGroupSchema: request=None, group=obj, provider=self.provider, + creating=creating, ) if value is None: continue @@ -99,7 +100,7 @@ def delete(self, obj: Group): def create(self, group: Group): """Create group from scratch and create a connection object""" - scim_group = self.to_schema(group) + scim_group = self.to_schema(group, True) response = self._request( "POST", "/Groups", @@ -111,11 +112,11 @@ def create(self, group: Group): scim_id = response.get("id") if not scim_id or scim_id == "": raise StopSync("SCIM Response with missing or invalid `id`") - SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id) + return SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id) def update(self, group: Group, connection: SCIMGroup): """Update existing group""" - scim_group = self.to_schema(group) + scim_group = self.to_schema(group, False) scim_group.id = connection.scim_id try: return self._request( diff --git a/authentik/providers/scim/clients/users.py b/authentik/providers/scim/clients/users.py index 31ff4c739e31..075559a766ad 100644 --- a/authentik/providers/scim/clients/users.py +++ b/authentik/providers/scim/clients/users.py @@ -23,7 +23,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]): connection_type = SCIMUser connection_type_query = "user" - def to_schema(self, obj: User) -> SCIMUserSchema: + def to_schema(self, obj: User, creating: bool) -> SCIMUserSchema: """Convert authentik user into SCIM""" raw_scim_user = { "schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",), @@ -37,6 +37,7 @@ def to_schema(self, obj: User) -> SCIMUserSchema: user=obj, request=None, provider=self.provider, + creating=creating, ) if value is None: continue @@ -73,7 +74,7 @@ def delete(self, obj: User): def create(self, user: User): """Create user from scratch and create a connection object""" - scim_user = self.to_schema(user) + scim_user = self.to_schema(user, True) response = self._request( "POST", "/Users", @@ -85,11 +86,11 @@ def create(self, user: User): scim_id = response.get("id") if not scim_id or scim_id == "": raise StopSync("SCIM Response with missing or invalid `id`") - SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id) + return SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id) def update(self, user: User, connection: SCIMUser): """Update existing user""" - scim_user = self.to_schema(user) + scim_user = self.to_schema(user, False) scim_user.id = connection.scim_id self._request( "PUT", diff --git a/authentik/providers/scim/tasks.py b/authentik/providers/scim/tasks.py index 342a3344a959..f3c2e4d49370 100644 --- a/authentik/providers/scim/tasks.py +++ b/authentik/providers/scim/tasks.py @@ -1,6 +1,7 @@ """SCIM Provider tasks""" from authentik.events.system_tasks import SystemTask +from authentik.lib.sync.outgoing.exceptions import TransientSyncException from authentik.lib.sync.outgoing.tasks import SyncTasks from authentik.providers.scim.models import SCIMProvider from authentik.root.celery import CELERY_APP @@ -8,12 +9,14 @@ sync_tasks = SyncTasks(SCIMProvider) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def scim_sync_objects(*args, **kwargs): return sync_tasks.sync_objects(*args, **kwargs) -@CELERY_APP.task(base=SystemTask, bind=True) +@CELERY_APP.task( + base=SystemTask, bind=True, autoretry_for=(TransientSyncException,), retry_backoff=True +) def scim_sync(self, provider_pk: int, *args, **kwargs): """Run full sync for SCIM provider""" return sync_tasks.sync_single(self, provider_pk, scim_sync_objects) @@ -24,11 +27,11 @@ def scim_sync_all(): return sync_tasks.sync_all(scim_sync) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def scim_sync_direct(*args, **kwargs): return sync_tasks.sync_signal_direct(*args, **kwargs) -@CELERY_APP.task() +@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True) def scim_sync_m2m(*args, **kwargs): return sync_tasks.sync_signal_m2m(*args, **kwargs) diff --git a/authentik/providers/scim/tests/test_group.py b/authentik/providers/scim/tests/test_group.py index 9aaa245a727f..b0b7058f524e 100644 --- a/authentik/providers/scim/tests/test_group.py +++ b/authentik/providers/scim/tests/test_group.py @@ -127,7 +127,7 @@ def test_group_create_delete(self, mock: Mocker): "id": scim_id, }, ) - mock.delete("https://localhost/Groups", status_code=204) + mock.delete(f"https://localhost/Groups/{scim_id}", status_code=204) uid = generate_id() group = Group.objects.create( name=uid, diff --git a/authentik/providers/scim/tests/test_user.py b/authentik/providers/scim/tests/test_user.py index ef9135ce83da..a8ca5e813705 100644 --- a/authentik/providers/scim/tests/test_user.py +++ b/authentik/providers/scim/tests/test_user.py @@ -230,7 +230,7 @@ def test_user_create_delete(self, mock: Mocker): "id": scim_id, }, ) - mock.delete("https://localhost/Users", status_code=204) + mock.delete(f"https://localhost/Users/{scim_id}", status_code=204) uid = generate_id() user = User.objects.create( username=uid, diff --git a/authentik/root/settings.py b/authentik/root/settings.py index bab291a3ec57..12d4350104f5 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -155,9 +155,7 @@ "LDAPAPIAccessMode": "authentik.providers.ldap.models.APIAccessMode", "UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification", "UserTypeEnum": "authentik.core.models.UserTypes", - "GoogleWorkspaceDeleteAction": ( - "authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceDeleteAction" - ), + "OutgoingSyncDeleteAction": "authentik.lib.sync.outgoing.models.OutgoingSyncDeleteAction", }, "ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False, "ENUM_GENERATE_CHOICE_DESCRIPTION": False, diff --git a/blueprints/schema.json b/blueprints/schema.json index 3278d848428e..32d40e4d4efb 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -2594,6 +2594,80 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_providers_microsoft_entra.microsoftentraprovider" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "present", + "created", + "must_created" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "attrs": { + "$ref": "#/$defs/model_authentik_providers_microsoft_entra.microsoftentraprovider" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_providers_microsoft_entra.microsoftentraprovider" + } + } + }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_providers_microsoft_entra.microsoftentraprovidermapping" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "present", + "created", + "must_created" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "attrs": { + "$ref": "#/$defs/model_authentik_providers_microsoft_entra.microsoftentraprovidermapping" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_providers_microsoft_entra.microsoftentraprovidermapping" + } + } + }, { "type": "object", "required": [ @@ -3412,6 +3486,7 @@ "authentik.enterprise", "authentik.enterprise.audit", "authentik.enterprise.providers.google_workspace", + "authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.rac", "authentik.enterprise.stages.source", "authentik.events" @@ -3495,6 +3570,8 @@ "authentik_enterprise.license", "authentik_providers_google_workspace.googleworkspaceprovider", "authentik_providers_google_workspace.googleworkspaceprovidermapping", + "authentik_providers_microsoft_entra.microsoftentraprovider", + "authentik_providers_microsoft_entra.microsoftentraprovidermapping", "authentik_providers_rac.racprovider", "authentik_providers_rac.endpoint", "authentik_providers_rac.racpropertymapping", @@ -8302,6 +8379,102 @@ }, "required": [] }, + "model_authentik_providers_microsoft_entra.microsoftentraprovider": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "property_mappings": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "title": "Property mappings" + }, + "property_mappings_group": { + "type": "array", + "items": { + "type": "string", + "format": "uuid", + "description": "Property mappings used for group creation/updating." + }, + "title": "Property mappings group", + "description": "Property mappings used for group creation/updating." + }, + "client_id": { + "type": "string", + "minLength": 1, + "title": "Client id" + }, + "client_secret": { + "type": "string", + "minLength": 1, + "title": "Client secret" + }, + "tenant_id": { + "type": "string", + "minLength": 1, + "title": "Tenant id" + }, + "exclude_users_service_account": { + "type": "boolean", + "title": "Exclude users service account" + }, + "filter_group": { + "type": "string", + "format": "uuid", + "title": "Filter group" + }, + "user_delete_action": { + "type": "string", + "enum": [ + "do_nothing", + "delete", + "suspend" + ], + "title": "User delete action" + }, + "group_delete_action": { + "type": "string", + "enum": [ + "do_nothing", + "delete", + "suspend" + ], + "title": "Group delete action" + } + }, + "required": [] + }, + "model_authentik_providers_microsoft_entra.microsoftentraprovidermapping": { + "type": "object", + "properties": { + "managed": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "title": "Managed by authentik", + "description": "Objects that are managed by authentik. These objects are created and updated automatically. This 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, + "title": "Name" + }, + "expression": { + "type": "string", + "minLength": 1, + "title": "Expression" + } + }, + "required": [] + }, "model_authentik_providers_rac.racprovider": { "type": "object", "properties": { diff --git a/blueprints/system/providers-google-workspace.yaml b/blueprints/system/providers-google-workspace.yaml index e6e5df87ef2b..243712854f7b 100644 --- a/blueprints/system/providers-google-workspace.yaml +++ b/blueprints/system/providers-google-workspace.yaml @@ -2,7 +2,7 @@ version: 1 metadata: labels: blueprints.goauthentik.io/system: "true" - name: System - Google Provider - Mappings + name: System - Google Workspace Provider - Mappings entries: - identifiers: managed: goauthentik.io/providers/google_workspace/user diff --git a/blueprints/system/providers-microsoft-entra.yaml b/blueprints/system/providers-microsoft-entra.yaml new file mode 100644 index 000000000000..9a69f9594097 --- /dev/null +++ b/blueprints/system/providers-microsoft-entra.yaml @@ -0,0 +1,39 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/system: "true" + name: System - Microsoft Entra Provider - Mappings +entries: + - identifiers: + managed: goauthentik.io/providers/microsoft_entra/user + model: authentik_providers_microsoft_entra.microsoftentraprovidermapping + attrs: + name: "authentik default Microsoft Entra Mapping: User" + # https://learn.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0 + expression: | + from msgraph.generated.models.password_profile import PasswordProfile + + user = { + "display_name": request.user.name, + "account_enabled": request.user.is_active, + "mail_nickname": request.user.username, + "user_principal_name": request.user.email, + } + if creating: + user["password_profile"] = PasswordProfile( + password=request.user.password + ) + return user + - identifiers: + managed: goauthentik.io/providers/microsoft_entra/group + model: authentik_providers_microsoft_entra.microsoftentraprovidermapping + attrs: + name: "authentik default Microsoft Entra Mapping: Group" + # https://learn.microsoft.com/en-us/graph/api/group-post-groups?view=graph-rest-1.0&tabs=http#request-body + expression: | + return { + "display_name": group.name, + "mail_enabled": False, + "security_enabled": True, + "mail_nickname": slugify(group.name), + } diff --git a/poetry.lock b/poetry.lock index 38527ba283cd..bc9526a93336 100644 --- a/poetry.lock +++ b/poetry.lock @@ -315,6 +315,42 @@ six = "*" [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] +[[package]] +name = "azure-core" +version = "1.30.1" +description = "Microsoft Azure Core Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "azure-core-1.30.1.tar.gz", hash = "sha256:26273a254131f84269e8ea4464f3560c731f29c0c1f69ac99010845f239c1a8f"}, + {file = "azure_core-1.30.1-py3-none-any.whl", hash = "sha256:7c5ee397e48f281ec4dd773d67a0a47a0962ed6fa833036057f9ea067f688e74"}, +] + +[package.dependencies] +requests = ">=2.21.0" +six = ">=1.11.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] + +[[package]] +name = "azure-identity" +version = "1.16.0" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure-identity-1.16.0.tar.gz", hash = "sha256:6ff1d667cdcd81da1ceab42f80a0be63ca846629f518a922f7317a7e3c844e1b"}, + {file = "azure_identity-1.16.0-py3-none-any.whl", hash = "sha256:722fdb60b8fdd55fa44dc378b8072f4b419b56a5e54c0de391f644949f3a826f"}, +] + +[package.dependencies] +azure-core = ">=1.23.0" +cryptography = ">=2.5" +msal = ">=1.24.0" +msal-extensions = ">=0.3.0" + [[package]] name = "bandit" version = "1.7.8" @@ -1121,6 +1157,23 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + [[package]] name = "django" version = "5.0.6" @@ -1716,6 +1769,53 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + [[package]] name = "httplib2" version = "0.22.0" @@ -1778,6 +1878,31 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "humanize" version = "4.9.0" @@ -1792,6 +1917,17 @@ files = [ [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + [[package]] name = "hyperlink" version = "21.0.0" @@ -1819,22 +1955,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "7.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, + {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "incremental" @@ -2385,6 +2521,154 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "microsoft-kiota-abstractions" +version = "1.3.2" +description = "Core abstractions for kiota generated libraries in Python" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_abstractions-1.3.2-py2.py3-none-any.whl", hash = "sha256:ec4335df425874b1c0171a97c4b5ccdc4a9d076e1ecd3a5c2582af1cacc25016"}, + {file = "microsoft_kiota_abstractions-1.3.2.tar.gz", hash = "sha256:acac0b34b443d3fc10a3a86dd996cdf92248080553a3768a77c23350541f1aa2"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.19.0" +opentelemetry-sdk = ">=1.19.0" +std-uritemplate = ">=0.0.38" + +[[package]] +name = "microsoft-kiota-authentication-azure" +version = "1.0.0" +description = "Authentication provider for Kiota using Azure Identity" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_authentication_azure-1.0.0-py2.py3-none-any.whl", hash = "sha256:289fe002951ae661415a6d3fa7c422c096b739165acb32d786316988120a1b27"}, + {file = "microsoft_kiota_authentication_azure-1.0.0.tar.gz", hash = "sha256:752304f8d94b884cfec12583dd763ec0478805c7f80b29344e78c6d55a97bd01"}, +] + +[package.dependencies] +aiohttp = ">=3.8.0" +azure-core = ">=1.21.1" +microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.20.0" +opentelemetry-sdk = ">=1.20.0" + +[[package]] +name = "microsoft-kiota-http" +version = "1.3.1" +description = "Kiota http request adapter implementation for httpx library" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_http-1.3.1-py2.py3-none-any.whl", hash = "sha256:d62972c6ed4c785f9808a15479a7421abb38a9519b39e6933e5d05555b9fb427"}, + {file = "microsoft_kiota_http-1.3.1.tar.gz", hash = "sha256:09d85310379f88af0a0967925d1fcbe82f2520a9fe6fa1fd50e79af813bc451d"}, +] + +[package.dependencies] +httpx = {version = ">=0.23.0", extras = ["http2"]} +microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.20.0" +opentelemetry-sdk = ">=1.20.0" + +[[package]] +name = "microsoft-kiota-serialization-form" +version = "0.1.0" +description = "Implementation of Kiota Serialization Interfaces for URI-Form encoded serialization" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_serialization_form-0.1.0-py2.py3-none-any.whl", hash = "sha256:5bc76fb2fc67d7c1f878f876d252ea814e4fc38df505099b9b86de52d974380a"}, + {file = "microsoft_kiota_serialization_form-0.1.0.tar.gz", hash = "sha256:663ece0cb1a41fe9ddfc9195aa3f15f219e14d2a1ee51e98c53ad8d795b2785d"}, +] + +[package.dependencies] +microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" +pendulum = ">=3.0.0" + +[[package]] +name = "microsoft-kiota-serialization-json" +version = "1.2.0" +description = "Implementation of Kiota Serialization interfaces for JSON" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_serialization_json-1.2.0-py2.py3-none-any.whl", hash = "sha256:cf68ef323157b3566b043d2282b292479bca6af0ffcf08385c806c812e507a58"}, + {file = "microsoft_kiota_serialization_json-1.2.0.tar.gz", hash = "sha256:89a4ec0128958bc92287db0cf5b6616a9f66ac42f6c7bcfe8894393d2156bed9"}, +] + +[package.dependencies] +microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" +pendulum = ">=3.0.0b1" + +[[package]] +name = "microsoft-kiota-serialization-multipart" +version = "0.1.0" +description = "Implementation of Kiota Serialization Interfaces for Multipart serialization" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_serialization_multipart-0.1.0-py2.py3-none-any.whl", hash = "sha256:ef183902e77807806b8a181cdde53ba5bc04c6c9bdb2f7d80f8bad5d720e0015"}, + {file = "microsoft_kiota_serialization_multipart-0.1.0.tar.gz", hash = "sha256:14e89e92582e6630ddbc70ac67b70bf189dacbfc41a96d3e1d10339e86c8dde5"}, +] + +[package.dependencies] +microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" + +[[package]] +name = "microsoft-kiota-serialization-text" +version = "1.0.0" +description = "Implementation of Kiota Serialization interfaces for text/plain" +optional = false +python-versions = "*" +files = [ + {file = "microsoft_kiota_serialization_text-1.0.0-py2.py3-none-any.whl", hash = "sha256:1d3789e012b603e059a36cc675d1fd08cb81e0dde423d970c0af2eabce9c0d43"}, + {file = "microsoft_kiota_serialization_text-1.0.0.tar.gz", hash = "sha256:c3dd3f409b1c4f4963bd1e41d51b65f7e53e852130bb441d79b77dad88ee76ed"}, +] + +[package.dependencies] +microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" +python-dateutil = ">=2.8.2" + +[[package]] +name = "msal" +version = "1.28.0" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal-1.28.0-py3-none-any.whl", hash = "sha256:3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b"}, + {file = "msal-1.28.0.tar.gz", hash = "sha256:80bbabe34567cb734efd2ec1869b2d98195c927455369d8077b3c542088c5c9d"}, +] + +[package.dependencies] +cryptography = ">=0.6,<45" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.13.2,<0.15)"] + +[[package]] +name = "msal-extensions" +version = "1.1.0" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal-extensions-1.1.0.tar.gz", hash = "sha256:6ab357867062db7b253d0bd2df6d411c7891a0ee7308d54d1e4317c1d1c54252"}, + {file = "msal_extensions-1.1.0-py3-none-any.whl", hash = "sha256:01be9711b4c0b1a151450068eeb2c4f0997df3bba085ac299de3a66f585e382f"}, +] + +[package.dependencies] +msal = ">=0.4.1,<2.0.0" +packaging = "*" +portalocker = [ + {version = ">=1.0,<3", markers = "platform_system != \"Windows\""}, + {version = ">=1.6,<3", markers = "platform_system == \"Windows\""}, +] + [[package]] name = "msgpack" version = "1.0.8" @@ -2450,6 +2734,51 @@ files = [ {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] +[[package]] +name = "msgraph-core" +version = "1.0.0" +description = "Core component of the Microsoft Graph Python SDK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgraph-core-1.0.0.tar.gz", hash = "sha256:f26bcbbb3cd149dd7f1613159e0c2ed862888d61bfd20ef0b08b9408eb670c9d"}, + {file = "msgraph_core-1.0.0-py3-none-any.whl", hash = "sha256:f3de5149e246833b4b03605590d0b4eacf58d9c5a10fd951c37e53f0a345afd5"}, +] + +[package.dependencies] +httpx = {version = ">=0.23.0", extras = ["http2"]} +microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" +microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" +microsoft-kiota-http = ">=1.0.0,<2.0.0" + +[package.extras] +dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] + +[[package]] +name = "msgraph-sdk" +version = "1.2.0" +description = "The Microsoft Graph Python SDK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgraph-sdk-1.2.0.tar.gz", hash = "sha256:689eec74fcb5cb29446947e4761fa57edeeb3ec1dccd7975c44d12d8d9db9c4f"}, + {file = "msgraph_sdk-1.2.0-py3-none-any.whl", hash = "sha256:4a9f706413c0a497cdfffd0b741122a5e73206333d566d115089cef9f4adadb7"}, +] + +[package.dependencies] +azure-identity = ">=1.12.0" +microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" +microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" +microsoft-kiota-http = ">=1.0.0,<2.0.0" +microsoft-kiota-serialization-form = ">=0.1.0" +microsoft-kiota-serialization-json = ">=1.0.0,<2.0.0" +microsoft-kiota-serialization-multipart = ">=0.1.0" +microsoft-kiota-serialization-text = ">=1.0.0,<2.0.0" +msgraph-core = ">=1.0.0" + +[package.extras] +dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] + [[package]] name = "multidict" version = "6.0.5" @@ -2600,6 +2929,48 @@ files = [ {file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"}, ] +[[package]] +name = "opentelemetry-api" +version = "1.24.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_api-1.24.0-py3-none-any.whl", hash = "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2"}, + {file = "opentelemetry_api-1.24.0.tar.gz", hash = "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<=7.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.24.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_sdk-1.24.0-py3-none-any.whl", hash = "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59"}, + {file = "opentelemetry_sdk-1.24.0.tar.gz", hash = "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5"}, +] + +[package.dependencies] +opentelemetry-api = "1.24.0" +opentelemetry-semantic-conventions = "0.45b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.45b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_semantic_conventions-0.45b0-py3-none-any.whl", hash = "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864"}, + {file = "opentelemetry_semantic_conventions-0.45b0.tar.gz", hash = "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118"}, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2687,6 +3058,105 @@ pygments = ">=2.12.0" [package.extras] dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] +[[package]] +name = "pendulum" +version = "3.0.0" +description = "Python datetimes made easy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, + {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[package.dependencies] +python-dateutil = ">=2.6" +tzdata = ">=2020.1" + +[package.extras] +test = ["time-machine (>=2.6.0)"] + [[package]] name = "platformdirs" version = "4.2.1" @@ -2718,6 +3188,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "portalocker" +version = "2.8.2" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.8" +files = [ + {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, + {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] + [[package]] name = "prometheus-client" version = "0.20.0" @@ -3007,6 +3496,9 @@ files = [ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, ] +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] @@ -3853,6 +4345,17 @@ files = [ dev = ["build", "hatch"] doc = ["sphinx"] +[[package]] +name = "std-uritemplate" +version = "0.0.57" +description = "std-uritemplate implementation for Python" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "std_uritemplate-0.0.57-py3-none-any.whl", hash = "sha256:66691cb6ff1d1b3612741053d6f5573ec7eb1c1a33ffb5ca49557e8aa2372aa8"}, + {file = "std_uritemplate-0.0.57.tar.gz", hash = "sha256:f4adc717aec138562e652b95da74fc6815a942231d971314856b81f434c1b94c"}, +] + [[package]] name = "stevedore" version = "5.2.0" @@ -4464,6 +4967,85 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "wsproto" version = "1.2.0" @@ -4732,4 +5314,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.12" -content-hash = "1ef87ed82de9c403cc569f4858d874e85da73924a610190379f7db3a76458d5b" +content-hash = "6399c90a2adca3e7119277bf4d0649fe0826d5fb4454a23b1b1fad3e64a1fe90" diff --git a/pyproject.toml b/pyproject.toml index e03038e0923c..967576b11d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ show_missing = true DJANGO_SETTINGS_MODULE = "authentik.root.settings" python_files = ["tests.py", "test_*.py", "*_tests.py"] junit_family = "xunit2" -addopts = "-p no:celery -p authentik.root.test_plugin --junitxml=unittest.xml -vv --full-trace --doctest-modules" +addopts = "-p no:celery -p authentik.root.test_plugin --junitxml=unittest.xml -vv --full-trace --doctest-modules --import-mode=importlib" filterwarnings = [ "ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning", "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning", @@ -118,6 +118,7 @@ jsonpatch = "*" kubernetes = "*" ldap3 = "*" lxml = "*" +msgraph-sdk = "*" opencontainers = { extras = ["reggie"], version = "*" } packaging = "*" paramiko = "*" diff --git a/schema.yml b/schema.yml index 1990e7412a04..1b88abae1fc3 100644 --- a/schema.yml +++ b/schema.yml @@ -14022,7 +14022,7 @@ paths: /propertymappings/provider/google_workspace/: get: operationId: propertymappings_provider_google_workspace_list - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset parameters: - in: query name: expression @@ -14078,7 +14078,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedGoogleProviderMappingList' + $ref: '#/components/schemas/PaginatedGoogleWorkspaceProviderMappingList' description: '' '400': content: @@ -14094,14 +14094,14 @@ paths: description: '' post: operationId: propertymappings_provider_google_workspace_create - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset tags: - propertymappings requestBody: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMappingRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderMappingRequest' required: true security: - authentik: [] @@ -14110,7 +14110,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMapping' + $ref: '#/components/schemas/GoogleWorkspaceProviderMapping' description: '' '400': content: @@ -14127,7 +14127,7 @@ paths: /propertymappings/provider/google_workspace/{pm_uuid}/: get: operationId: propertymappings_provider_google_workspace_retrieve - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset parameters: - in: path name: pm_uuid @@ -14145,7 +14145,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMapping' + $ref: '#/components/schemas/GoogleWorkspaceProviderMapping' description: '' '400': content: @@ -14161,7 +14161,7 @@ paths: description: '' put: operationId: propertymappings_provider_google_workspace_update - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset parameters: - in: path name: pm_uuid @@ -14176,7 +14176,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMappingRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderMappingRequest' required: true security: - authentik: [] @@ -14185,7 +14185,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMapping' + $ref: '#/components/schemas/GoogleWorkspaceProviderMapping' description: '' '400': content: @@ -14201,7 +14201,7 @@ paths: description: '' patch: operationId: propertymappings_provider_google_workspace_partial_update - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset parameters: - in: path name: pm_uuid @@ -14216,7 +14216,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedGoogleProviderMappingRequest' + $ref: '#/components/schemas/PatchedGoogleWorkspaceProviderMappingRequest' security: - authentik: [] responses: @@ -14224,7 +14224,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderMapping' + $ref: '#/components/schemas/GoogleWorkspaceProviderMapping' description: '' '400': content: @@ -14240,7 +14240,7 @@ paths: description: '' delete: operationId: propertymappings_provider_google_workspace_destroy - description: GoogleProviderMapping Viewset + description: GoogleWorkspaceProviderMapping Viewset parameters: - in: path name: pm_uuid @@ -14305,6 +14305,292 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /propertymappings/provider/microsoft_entra/: + get: + operationId: propertymappings_provider_microsoft_entra_list + description: MicrosoftEntraProviderMapping 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/PaginatedMicrosoftEntraProviderMappingList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: propertymappings_provider_microsoft_entra_create + description: MicrosoftEntraProviderMapping Viewset + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMappingRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /propertymappings/provider/microsoft_entra/{pm_uuid}/: + get: + operationId: propertymappings_provider_microsoft_entra_retrieve + description: MicrosoftEntraProviderMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Mapping. + required: true + tags: + - propertymappings + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: propertymappings_provider_microsoft_entra_update + description: MicrosoftEntraProviderMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMappingRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: propertymappings_provider_microsoft_entra_partial_update + description: MicrosoftEntraProviderMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Mapping. + required: true + tags: + - propertymappings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMicrosoftEntraProviderMappingRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MicrosoftEntraProviderMapping' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: propertymappings_provider_microsoft_entra_destroy + description: MicrosoftEntraProviderMapping Viewset + parameters: + - in: path + name: pm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider 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/provider/microsoft_entra/{pm_uuid}/used_by/: + get: + operationId: propertymappings_provider_microsoft_entra_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 Microsoft Entra Provider 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/rac/: get: operationId: propertymappings_rac_list @@ -15634,7 +15920,7 @@ paths: /providers/google_workspace/: get: operationId: providers_google_workspace_list - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset parameters: - in: query name: delegated_subject @@ -15686,7 +15972,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedGoogleProviderList' + $ref: '#/components/schemas/PaginatedGoogleWorkspaceProviderList' description: '' '400': content: @@ -15702,14 +15988,14 @@ paths: description: '' post: operationId: providers_google_workspace_create - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderRequest' required: true security: - authentik: [] @@ -15718,7 +16004,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProvider' + $ref: '#/components/schemas/GoogleWorkspaceProvider' description: '' '400': content: @@ -15735,7 +16021,7 @@ paths: /providers/google_workspace/{id}/: get: operationId: providers_google_workspace_retrieve - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset parameters: - in: path name: id @@ -15752,7 +16038,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProvider' + $ref: '#/components/schemas/GoogleWorkspaceProvider' description: '' '400': content: @@ -15768,7 +16054,7 @@ paths: description: '' put: operationId: providers_google_workspace_update - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset parameters: - in: path name: id @@ -15782,7 +16068,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderRequest' required: true security: - authentik: [] @@ -15791,7 +16077,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProvider' + $ref: '#/components/schemas/GoogleWorkspaceProvider' description: '' '400': content: @@ -15807,7 +16093,7 @@ paths: description: '' patch: operationId: providers_google_workspace_partial_update - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset parameters: - in: path name: id @@ -15821,7 +16107,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedGoogleProviderRequest' + $ref: '#/components/schemas/PatchedGoogleWorkspaceProviderRequest' security: - authentik: [] responses: @@ -15829,7 +16115,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GoogleProvider' + $ref: '#/components/schemas/GoogleWorkspaceProvider' description: '' '400': content: @@ -15845,7 +16131,7 @@ paths: description: '' delete: operationId: providers_google_workspace_destroy - description: GoogleProvider Viewset + description: GoogleWorkspaceProvider Viewset parameters: - in: path name: id @@ -15944,38 +16230,18 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/ldap/: + /providers/google_workspace_groups/: get: - operationId: providers_ldap_list - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_list + description: GoogleWorkspaceProviderGroup Viewset parameters: - in: query - name: application__isnull - schema: - type: boolean - - in: query - name: authorization_flow__slug__iexact - schema: - type: string - - in: query - name: base_dn__iexact - schema: - type: string - - in: query - name: certificate__kp_uuid__iexact + name: group__group_uuid schema: type: string format: uuid - in: query - name: certificate__name__iexact - schema: - type: string - - in: query - name: gid_start_number__iexact - schema: - type: integer - - in: query - name: name__iexact + name: group__name schema: type: string - name: ordering @@ -15996,29 +16262,16 @@ paths: description: Number of results to return per page. schema: type: integer + - in: query + name: provider__id + schema: + type: integer - name: search required: false in: query description: A search term. schema: type: string - - in: query - name: search_group__group_uuid__iexact - schema: - type: string - format: uuid - - in: query - name: search_group__name__iexact - schema: - type: string - - in: query - name: tls_server_name__iexact - schema: - type: string - - in: query - name: uid_start_number__iexact - schema: - type: integer tags: - providers security: @@ -16028,7 +16281,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedLDAPProviderList' + $ref: '#/components/schemas/PaginatedGoogleWorkspaceProviderGroupList' description: '' '400': content: @@ -16043,15 +16296,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_ldap_create - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_create + description: GoogleWorkspaceProviderGroup Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/LDAPProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroupRequest' required: true security: - authentik: [] @@ -16060,7 +16313,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LDAPProvider' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroup' description: '' '400': content: @@ -16074,16 +16327,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/ldap/{id}/: + /providers/google_workspace_groups/{id}/: get: - operationId: providers_ldap_retrieve - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_retrieve + description: GoogleWorkspaceProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this LDAP Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider Group. required: true tags: - providers @@ -16094,7 +16348,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LDAPProvider' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroup' description: '' '400': content: @@ -16109,14 +16363,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_ldap_update - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_update + description: GoogleWorkspaceProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this LDAP Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider Group. required: true tags: - providers @@ -16124,7 +16379,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LDAPProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroupRequest' required: true security: - authentik: [] @@ -16133,7 +16388,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LDAPProvider' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroup' description: '' '400': content: @@ -16148,14 +16403,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_ldap_partial_update - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_partial_update + description: GoogleWorkspaceProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this LDAP Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider Group. required: true tags: - providers @@ -16163,7 +16419,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedLDAPProviderRequest' + $ref: '#/components/schemas/PatchedGoogleWorkspaceProviderGroupRequest' security: - authentik: [] responses: @@ -16171,7 +16427,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LDAPProvider' + $ref: '#/components/schemas/GoogleWorkspaceProviderGroup' description: '' '400': content: @@ -16186,14 +16442,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_ldap_destroy - description: LDAPProvider Viewset + operationId: providers_google_workspace_groups_destroy + description: GoogleWorkspaceProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this LDAP Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider Group. required: true tags: - providers @@ -16214,16 +16471,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/ldap/{id}/used_by/: + /providers/google_workspace_groups/{id}/used_by/: get: - operationId: providers_ldap_used_by_list + operationId: providers_google_workspace_groups_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 LDAP Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider Group. required: true tags: - providers @@ -16250,61 +16508,11 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/oauth2/: + /providers/google_workspace_users/: get: - operationId: providers_oauth2_list - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_list + description: GoogleWorkspaceProviderUser Viewset parameters: - - in: query - name: access_code_validity - schema: - type: string - - in: query - name: access_token_validity - schema: - type: string - - in: query - name: application - schema: - type: string - format: uuid - - in: query - name: authorization_flow - schema: - type: string - format: uuid - - in: query - name: client_id - schema: - type: string - - in: query - name: client_type - schema: - type: string - enum: - - confidential - - public - description: |+ - Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable - - - in: query - name: include_claims_in_id_token - schema: - type: boolean - - in: query - name: issuer_mode - schema: - type: string - enum: - - global - - per_provider - description: |+ - Configure how the issuer field of the ID Token should be filled. - - - in: query - name: name - schema: - type: string - name: ordering required: false in: query @@ -16324,22 +16532,9 @@ paths: schema: type: integer - in: query - name: property_mappings - schema: - type: array - items: - type: string - format: uuid - explode: true - style: form - - in: query - name: redirect_uris - schema: - type: string - - in: query - name: refresh_token_validity + name: provider__id schema: - type: string + type: integer - name: search required: false in: query @@ -16347,24 +16542,13 @@ paths: schema: type: string - in: query - name: signing_key + name: user__id schema: - type: string - format: uuid + type: integer - in: query - name: sub_mode + name: user__username schema: type: string - enum: - - hashed_user_id - - user_email - - user_id - - user_upn - - user_username - - user_uuid - description: |+ - Configure what data should be used as unique User Identifier. For most cases, the default should be fine. - tags: - providers security: @@ -16374,7 +16558,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedOAuth2ProviderList' + $ref: '#/components/schemas/PaginatedGoogleWorkspaceProviderUserList' description: '' '400': content: @@ -16389,15 +16573,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_oauth2_create - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_create + description: GoogleWorkspaceProviderUser Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/OAuth2ProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderUserRequest' required: true security: - authentik: [] @@ -16406,7 +16590,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/GoogleWorkspaceProviderUser' description: '' '400': content: @@ -16420,16 +16604,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/oauth2/{id}/: + /providers/google_workspace_users/{id}/: get: - operationId: providers_oauth2_retrieve - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_retrieve + description: GoogleWorkspaceProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider User. required: true tags: - providers @@ -16440,7 +16625,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/GoogleWorkspaceProviderUser' description: '' '400': content: @@ -16455,14 +16640,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_oauth2_update - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_update + description: GoogleWorkspaceProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider User. required: true tags: - providers @@ -16470,7 +16656,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OAuth2ProviderRequest' + $ref: '#/components/schemas/GoogleWorkspaceProviderUserRequest' required: true security: - authentik: [] @@ -16479,7 +16665,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/GoogleWorkspaceProviderUser' description: '' '400': content: @@ -16494,14 +16680,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_oauth2_partial_update - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_partial_update + description: GoogleWorkspaceProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider User. required: true tags: - providers @@ -16509,7 +16696,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedOAuth2ProviderRequest' + $ref: '#/components/schemas/PatchedGoogleWorkspaceProviderUserRequest' security: - authentik: [] responses: @@ -16517,7 +16704,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/GoogleWorkspaceProviderUser' description: '' '400': content: @@ -16532,14 +16719,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_oauth2_destroy - description: OAuth2Provider Viewset + operationId: providers_google_workspace_users_destroy + description: GoogleWorkspaceProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider User. required: true tags: - providers @@ -16560,86 +16748,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/oauth2/{id}/preview_user/: - get: - operationId: providers_oauth2_preview_user_retrieve - description: Preview user data for provider - parameters: - - in: query - name: for_user - schema: - type: integer - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. - required: true - tags: - - providers - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PropertyMappingPreview' - description: '' - '400': - description: Bad request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /providers/oauth2/{id}/setup_urls/: + /providers/google_workspace_users/{id}/used_by/: get: - operationId: providers_oauth2_setup_urls_retrieve - description: Get Providers setup URLs - parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this OAuth2/OpenID Provider. - required: true - tags: - - providers - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/OAuth2ProviderSetupURLs' - description: '' - '404': - description: Provider has no application assigned - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /providers/oauth2/{id}/used_by/: - get: - operationId: providers_oauth2_used_by_list + operationId: providers_google_workspace_users_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 OAuth2/OpenID Provider. + type: string + format: uuid + description: A UUID string identifying this Google Workspace Provider User. required: true tags: - providers @@ -16666,10 +16785,10 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/proxy/: + /providers/ldap/: get: - operationId: providers_proxy_list - description: ProxyProvider Viewset + operationId: providers_ldap_list + description: LDAPProvider Viewset parameters: - in: query name: application__isnull @@ -16680,15 +16799,7 @@ paths: schema: type: string - in: query - name: basic_auth_enabled__iexact - schema: - type: boolean - - in: query - name: basic_auth_password_attribute__iexact - schema: - type: string - - in: query - name: basic_auth_user_attribute__iexact + name: base_dn__iexact schema: type: string - in: query @@ -16701,25 +16812,9 @@ paths: schema: type: string - in: query - name: cookie_domain__iexact - schema: - type: string - - in: query - name: external_host__iexact - schema: - type: string - - in: query - name: internal_host__iexact - schema: - type: string - - in: query - name: internal_host_ssl_validation__iexact - schema: - type: boolean - - in: query - name: mode__iexact + name: gid_start_number__iexact schema: - type: string + type: integer - in: query name: name__iexact schema: @@ -16742,19 +16837,6 @@ paths: description: Number of results to return per page. schema: type: integer - - in: query - name: property_mappings__iexact - schema: - type: array - items: - type: string - format: uuid - explode: true - style: form - - in: query - name: redirect_uris__iexact - schema: - type: string - name: search required: false in: query @@ -16762,9 +16844,22 @@ paths: schema: type: string - in: query - name: skip_path_regex__iexact + name: search_group__group_uuid__iexact schema: type: string + format: uuid + - in: query + name: search_group__name__iexact + schema: + type: string + - in: query + name: tls_server_name__iexact + schema: + type: string + - in: query + name: uid_start_number__iexact + schema: + type: integer tags: - providers security: @@ -16774,7 +16869,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedProxyProviderList' + $ref: '#/components/schemas/PaginatedLDAPProviderList' description: '' '400': content: @@ -16789,15 +16884,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_proxy_create - description: ProxyProvider Viewset + operationId: providers_ldap_create + description: LDAPProvider Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/ProxyProviderRequest' + $ref: '#/components/schemas/LDAPProviderRequest' required: true security: - authentik: [] @@ -16806,7 +16901,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProxyProvider' + $ref: '#/components/schemas/LDAPProvider' description: '' '400': content: @@ -16820,16 +16915,16 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/proxy/{id}/: + /providers/ldap/{id}/: get: - operationId: providers_proxy_retrieve - description: ProxyProvider Viewset + operationId: providers_ldap_retrieve + description: LDAPProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this Proxy Provider. + description: A unique integer value identifying this LDAP Provider. required: true tags: - providers @@ -16840,7 +16935,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProxyProvider' + $ref: '#/components/schemas/LDAPProvider' description: '' '400': content: @@ -16855,14 +16950,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_proxy_update - description: ProxyProvider Viewset + operationId: providers_ldap_update + description: LDAPProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this Proxy Provider. + description: A unique integer value identifying this LDAP Provider. required: true tags: - providers @@ -16870,7 +16965,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProxyProviderRequest' + $ref: '#/components/schemas/LDAPProviderRequest' required: true security: - authentik: [] @@ -16879,7 +16974,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProxyProvider' + $ref: '#/components/schemas/LDAPProvider' description: '' '400': content: @@ -16894,14 +16989,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_proxy_partial_update - description: ProxyProvider Viewset + operationId: providers_ldap_partial_update + description: LDAPProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this Proxy Provider. + description: A unique integer value identifying this LDAP Provider. required: true tags: - providers @@ -16909,7 +17004,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedProxyProviderRequest' + $ref: '#/components/schemas/PatchedLDAPProviderRequest' security: - authentik: [] responses: @@ -16917,7 +17012,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProxyProvider' + $ref: '#/components/schemas/LDAPProvider' description: '' '400': content: @@ -16932,14 +17027,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_proxy_destroy - description: ProxyProvider Viewset + operationId: providers_ldap_destroy + description: LDAPProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this Proxy Provider. + description: A unique integer value identifying this LDAP Provider. required: true tags: - providers @@ -16960,16 +17055,16 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/proxy/{id}/used_by/: + /providers/ldap/{id}/used_by/: get: - operationId: providers_proxy_used_by_list + operationId: providers_ldap_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 Proxy Provider. + description: A unique integer value identifying this LDAP Provider. required: true tags: - providers @@ -16996,17 +17091,22 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/rac/: + /providers/microsoft_entra/: get: - operationId: providers_rac_list - description: RACProvider Viewset + operationId: providers_microsoft_entra_list + description: MicrosoftEntraProvider Viewset parameters: - in: query - name: application__isnull + name: exclude_users_service_account schema: type: boolean - in: query - name: name__iexact + name: filter_group + schema: + type: string + format: uuid + - in: query + name: name schema: type: string - name: ordering @@ -17042,7 +17142,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedRACProviderList' + $ref: '#/components/schemas/PaginatedMicrosoftEntraProviderList' description: '' '400': content: @@ -17057,15 +17157,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_rac_create - description: RACProvider Viewset + operationId: providers_microsoft_entra_create + description: MicrosoftEntraProvider Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/RACProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderRequest' required: true security: - authentik: [] @@ -17074,7 +17174,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RACProvider' + $ref: '#/components/schemas/MicrosoftEntraProvider' description: '' '400': content: @@ -17088,16 +17188,16 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/rac/{id}/: + /providers/microsoft_entra/{id}/: get: - operationId: providers_rac_retrieve - description: RACProvider Viewset + operationId: providers_microsoft_entra_retrieve + description: MicrosoftEntraProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this RAC Provider. + description: A unique integer value identifying this Microsoft Entra Provider. required: true tags: - providers @@ -17108,7 +17208,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RACProvider' + $ref: '#/components/schemas/MicrosoftEntraProvider' description: '' '400': content: @@ -17123,14 +17223,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_rac_update - description: RACProvider Viewset + operationId: providers_microsoft_entra_update + description: MicrosoftEntraProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this RAC Provider. + description: A unique integer value identifying this Microsoft Entra Provider. required: true tags: - providers @@ -17138,7 +17238,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RACProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderRequest' required: true security: - authentik: [] @@ -17147,7 +17247,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RACProvider' + $ref: '#/components/schemas/MicrosoftEntraProvider' description: '' '400': content: @@ -17162,14 +17262,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_rac_partial_update - description: RACProvider Viewset + operationId: providers_microsoft_entra_partial_update + description: MicrosoftEntraProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this RAC Provider. + description: A unique integer value identifying this Microsoft Entra Provider. required: true tags: - providers @@ -17177,7 +17277,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedRACProviderRequest' + $ref: '#/components/schemas/PatchedMicrosoftEntraProviderRequest' security: - authentik: [] responses: @@ -17185,7 +17285,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RACProvider' + $ref: '#/components/schemas/MicrosoftEntraProvider' description: '' '400': content: @@ -17200,14 +17300,14 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_rac_destroy - description: RACProvider Viewset + operationId: providers_microsoft_entra_destroy + description: MicrosoftEntraProvider Viewset parameters: - in: path name: id schema: type: integer - description: A unique integer value identifying this RAC Provider. + description: A unique integer value identifying this Microsoft Entra Provider. required: true tags: - providers @@ -17228,16 +17328,52 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/rac/{id}/used_by/: + /providers/microsoft_entra/{id}/sync/status/: get: - operationId: providers_rac_used_by_list + operationId: providers_microsoft_entra_sync_status_retrieve + description: Get provider's sync status + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Microsoft Entra Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SyncStatus' + 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/microsoft_entra/{id}/used_by/: + get: + operationId: providers_microsoft_entra_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 RAC Provider. + description: A unique integer value identifying this Microsoft Entra Provider. required: true tags: - providers @@ -17264,25 +17400,18 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/radius/: + /providers/microsoft_entra_groups/: get: - operationId: providers_radius_list - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_list + description: MicrosoftEntraProviderGroup Viewset parameters: - in: query - name: application__isnull - schema: - type: boolean - - in: query - name: authorization_flow__slug__iexact - schema: - type: string - - in: query - name: client_networks__iexact + name: group__group_uuid schema: type: string + format: uuid - in: query - name: name__iexact + name: group__name schema: type: string - name: ordering @@ -17303,6 +17432,10 @@ paths: description: Number of results to return per page. schema: type: integer + - in: query + name: provider__id + schema: + type: integer - name: search required: false in: query @@ -17318,7 +17451,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedRadiusProviderList' + $ref: '#/components/schemas/PaginatedMicrosoftEntraProviderGroupList' description: '' '400': content: @@ -17333,15 +17466,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_radius_create - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_create + description: MicrosoftEntraProviderGroup Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/RadiusProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderGroupRequest' required: true security: - authentik: [] @@ -17350,7 +17483,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RadiusProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderGroup' description: '' '400': content: @@ -17364,16 +17497,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/radius/{id}/: + /providers/microsoft_entra_groups/{id}/: get: - operationId: providers_radius_retrieve - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_retrieve + description: MicrosoftEntraProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this Radius Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Group. required: true tags: - providers @@ -17384,7 +17518,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RadiusProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderGroup' description: '' '400': content: @@ -17399,14 +17533,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_radius_update - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_update + description: MicrosoftEntraProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this Radius Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Group. required: true tags: - providers @@ -17414,7 +17549,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RadiusProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderGroupRequest' required: true security: - authentik: [] @@ -17423,7 +17558,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RadiusProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderGroup' description: '' '400': content: @@ -17438,14 +17573,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_radius_partial_update - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_partial_update + description: MicrosoftEntraProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this Radius Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Group. required: true tags: - providers @@ -17453,7 +17589,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedRadiusProviderRequest' + $ref: '#/components/schemas/PatchedMicrosoftEntraProviderGroupRequest' security: - authentik: [] responses: @@ -17461,7 +17597,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RadiusProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderGroup' description: '' '400': content: @@ -17476,14 +17612,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_radius_destroy - description: RadiusProvider Viewset + operationId: providers_microsoft_entra_groups_destroy + description: MicrosoftEntraProviderGroup Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this Radius Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Group. required: true tags: - providers @@ -17504,16 +17641,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/radius/{id}/used_by/: + /providers/microsoft_entra_groups/{id}/used_by/: get: - operationId: providers_radius_used_by_list + operationId: providers_microsoft_entra_groups_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 Radius Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider Group. required: true tags: - providers @@ -17540,72 +17678,11 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/saml/: + /providers/microsoft_entra_users/: get: - operationId: providers_saml_list - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_list + description: MicrosoftEntraProviderUser Viewset parameters: - - in: query - name: acs_url - schema: - type: string - - in: query - name: assertion_valid_not_before - schema: - type: string - - in: query - name: assertion_valid_not_on_or_after - schema: - type: string - - in: query - name: audience - schema: - type: string - - in: query - name: authentication_flow - schema: - type: string - format: uuid - - in: query - name: authorization_flow - schema: - type: string - format: uuid - - in: query - name: backchannel_application - schema: - type: string - format: uuid - - in: query - name: default_relay_state - schema: - type: string - - in: query - name: digest_algorithm - schema: - type: string - enum: - - http://www.w3.org/2000/09/xmldsig#sha1 - - 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 - - in: query - name: is_backchannel - schema: - type: boolean - - in: query - name: issuer - schema: - type: string - - in: query - name: name - schema: - type: string - - in: query - name: name_id_mapping - schema: - type: string - format: uuid - name: ordering required: false in: query @@ -17625,14 +17702,9 @@ paths: schema: type: integer - in: query - name: property_mappings + name: provider__id schema: - type: array - items: - type: string - format: uuid - explode: true - style: form + type: integer - name: search required: false in: query @@ -17640,44 +17712,13 @@ paths: schema: type: string - in: query - name: session_valid_not_on_or_after - schema: - type: string - - in: query - name: signature_algorithm - schema: - type: string - enum: - - http://www.w3.org/2000/09/xmldsig#dsa-sha1 - - http://www.w3.org/2000/09/xmldsig#rsa-sha1 - - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1 - - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256 - - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384 - - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512 - - 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 - - in: query - name: signing_kp - schema: - type: string - format: uuid - - in: query - name: sp_binding + name: user__id schema: - type: string - title: Service Provider Binding - enum: - - post - - redirect - description: |+ - This determines how authentik sends the response back to the Service Provider. - + type: integer - in: query - name: verification_kp + name: user__username schema: type: string - format: uuid tags: - providers security: @@ -17687,7 +17728,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedSAMLProviderList' + $ref: '#/components/schemas/PaginatedMicrosoftEntraProviderUserList' description: '' '400': content: @@ -17702,15 +17743,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: providers_saml_create - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_create + description: MicrosoftEntraProviderUser Viewset tags: - providers requestBody: content: application/json: schema: - $ref: '#/components/schemas/SAMLProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderUserRequest' required: true security: - authentik: [] @@ -17719,7 +17760,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SAMLProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderUser' description: '' '400': content: @@ -17733,16 +17774,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/saml/{id}/: + /providers/microsoft_entra_users/{id}/: get: - operationId: providers_saml_retrieve - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_retrieve + description: MicrosoftEntraProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this SAML Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider User. required: true tags: - providers @@ -17753,7 +17795,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SAMLProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderUser' description: '' '400': content: @@ -17768,14 +17810,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: providers_saml_update - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_update + description: MicrosoftEntraProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this SAML Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider User. required: true tags: - providers @@ -17783,7 +17826,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SAMLProviderRequest' + $ref: '#/components/schemas/MicrosoftEntraProviderUserRequest' required: true security: - authentik: [] @@ -17792,7 +17835,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SAMLProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderUser' description: '' '400': content: @@ -17807,14 +17850,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: providers_saml_partial_update - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_partial_update + description: MicrosoftEntraProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this SAML Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider User. required: true tags: - providers @@ -17822,7 +17866,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedSAMLProviderRequest' + $ref: '#/components/schemas/PatchedMicrosoftEntraProviderUserRequest' security: - authentik: [] responses: @@ -17830,7 +17874,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SAMLProvider' + $ref: '#/components/schemas/MicrosoftEntraProviderUser' description: '' '400': content: @@ -17845,14 +17889,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: providers_saml_destroy - description: SAMLProvider Viewset + operationId: providers_microsoft_entra_users_destroy + description: MicrosoftEntraProviderUser Viewset parameters: - in: path name: id schema: - type: integer - description: A unique integer value identifying this SAML Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider User. required: true tags: - providers @@ -17873,99 +17918,1759 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /providers/saml/{id}/metadata/: - get: - operationId: providers_saml_metadata_retrieve - description: Return metadata as XML string - parameters: - - in: query - name: download - schema: - type: boolean - - in: query - name: force_binding - schema: - type: string - enum: - - urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST - - urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect - description: Optionally force the metadata to only include one binding. - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this SAML Provider. - required: true - tags: - - providers - security: - - authentik: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/SAMLMetadata' - description: '' - '404': - description: Provider has no application assigned - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /providers/saml/{id}/preview_user/: - get: - operationId: providers_saml_preview_user_retrieve - description: Preview user data for provider - parameters: - - in: query - name: for_user - schema: - type: integer - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this SAML Provider. - required: true - tags: - - providers - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PropertyMappingPreview' - description: '' - '400': - description: Bad request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /providers/saml/{id}/used_by/: + /providers/microsoft_entra_users/{id}/used_by/: get: - operationId: providers_saml_used_by_list + operationId: providers_microsoft_entra_users_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 SAML Provider. + type: string + format: uuid + description: A UUID string identifying this Microsoft Entra Provider User. + 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: '' + /providers/oauth2/: + get: + operationId: providers_oauth2_list + description: OAuth2Provider Viewset + parameters: + - in: query + name: access_code_validity + schema: + type: string + - in: query + name: access_token_validity + schema: + type: string + - in: query + name: application + schema: + type: string + format: uuid + - in: query + name: authorization_flow + schema: + type: string + format: uuid + - in: query + name: client_id + schema: + type: string + - in: query + name: client_type + schema: + type: string + enum: + - confidential + - public + description: |+ + Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + + - in: query + name: include_claims_in_id_token + schema: + type: boolean + - in: query + name: issuer_mode + schema: + type: string + enum: + - global + - per_provider + description: |+ + Configure how the issuer field of the ID Token should be filled. + + - 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: property_mappings + schema: + type: array + items: + type: string + format: uuid + explode: true + style: form + - in: query + name: redirect_uris + schema: + type: string + - in: query + name: refresh_token_validity + schema: + type: string + - name: search + required: false + in: query + description: A search term. + schema: + type: string + - in: query + name: signing_key + schema: + type: string + format: uuid + - in: query + name: sub_mode + schema: + type: string + enum: + - hashed_user_id + - user_email + - user_id + - user_upn + - user_username + - user_uuid + description: |+ + Configure what data should be used as unique User Identifier. For most cases, the default should be fine. + + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedOAuth2ProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_oauth2_create + description: OAuth2Provider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2ProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2Provider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/oauth2/{id}/: + get: + operationId: providers_oauth2_retrieve + description: OAuth2Provider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2Provider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_oauth2_update + description: OAuth2Provider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2ProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2Provider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_oauth2_partial_update + description: OAuth2Provider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOAuth2ProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2Provider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_oauth2_destroy + description: OAuth2Provider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID 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/oauth2/{id}/preview_user/: + get: + operationId: providers_oauth2_preview_user_retrieve + description: Preview user data for provider + parameters: + - in: query + name: for_user + schema: + type: integer + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PropertyMappingPreview' + description: '' + '400': + description: Bad request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/oauth2/{id}/setup_urls/: + get: + operationId: providers_oauth2_setup_urls_retrieve + description: Get Providers setup URLs + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2ProviderSetupURLs' + description: '' + '404': + description: Provider has no application assigned + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/oauth2/{id}/used_by/: + get: + operationId: providers_oauth2_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 OAuth2/OpenID 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: '' + /providers/proxy/: + get: + operationId: providers_proxy_list + description: ProxyProvider Viewset + parameters: + - in: query + name: application__isnull + schema: + type: boolean + - in: query + name: authorization_flow__slug__iexact + schema: + type: string + - in: query + name: basic_auth_enabled__iexact + schema: + type: boolean + - in: query + name: basic_auth_password_attribute__iexact + schema: + type: string + - in: query + name: basic_auth_user_attribute__iexact + schema: + type: string + - in: query + name: certificate__kp_uuid__iexact + schema: + type: string + format: uuid + - in: query + name: certificate__name__iexact + schema: + type: string + - in: query + name: cookie_domain__iexact + schema: + type: string + - in: query + name: external_host__iexact + schema: + type: string + - in: query + name: internal_host__iexact + schema: + type: string + - in: query + name: internal_host_ssl_validation__iexact + schema: + type: boolean + - in: query + name: mode__iexact + schema: + type: string + - in: query + name: name__iexact + 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: property_mappings__iexact + schema: + type: array + items: + type: string + format: uuid + explode: true + style: form + - in: query + name: redirect_uris__iexact + schema: + type: string + - name: search + required: false + in: query + description: A search term. + schema: + type: string + - in: query + name: skip_path_regex__iexact + schema: + type: string + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedProxyProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_proxy_create + description: ProxyProvider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/proxy/{id}/: + get: + operationId: providers_proxy_retrieve + description: ProxyProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Proxy Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_proxy_update + description: ProxyProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Proxy Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_proxy_partial_update + description: ProxyProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Proxy Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedProxyProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_proxy_destroy + description: ProxyProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Proxy 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/proxy/{id}/used_by/: + get: + operationId: providers_proxy_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 Proxy 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: '' + /providers/rac/: + get: + operationId: providers_rac_list + description: RACProvider Viewset + parameters: + - in: query + name: application__isnull + schema: + type: boolean + - in: query + name: name__iexact + 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 + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedRACProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_rac_create + description: RACProvider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RACProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/RACProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/rac/{id}/: + get: + operationId: providers_rac_retrieve + description: RACProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this RAC Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RACProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_rac_update + description: RACProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this RAC Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RACProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RACProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_rac_partial_update + description: RACProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this RAC Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedRACProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RACProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_rac_destroy + description: RACProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this RAC 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/rac/{id}/used_by/: + get: + operationId: providers_rac_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 RAC 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: '' + /providers/radius/: + get: + operationId: providers_radius_list + description: RadiusProvider Viewset + parameters: + - in: query + name: application__isnull + schema: + type: boolean + - in: query + name: authorization_flow__slug__iexact + schema: + type: string + - in: query + name: client_networks__iexact + schema: + type: string + - in: query + name: name__iexact + 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 + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedRadiusProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_radius_create + description: RadiusProvider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/radius/{id}/: + get: + operationId: providers_radius_retrieve + description: RadiusProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Radius Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_radius_update + description: RadiusProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Radius Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_radius_partial_update + description: RadiusProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Radius Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedRadiusProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RadiusProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_radius_destroy + description: RadiusProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Radius 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/radius/{id}/used_by/: + get: + operationId: providers_radius_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 Radius 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: '' + /providers/saml/: + get: + operationId: providers_saml_list + description: SAMLProvider Viewset + parameters: + - in: query + name: acs_url + schema: + type: string + - in: query + name: assertion_valid_not_before + schema: + type: string + - in: query + name: assertion_valid_not_on_or_after + schema: + type: string + - in: query + name: audience + schema: + type: string + - in: query + name: authentication_flow + schema: + type: string + format: uuid + - in: query + name: authorization_flow + schema: + type: string + format: uuid + - in: query + name: backchannel_application + schema: + type: string + format: uuid + - in: query + name: default_relay_state + schema: + type: string + - in: query + name: digest_algorithm + schema: + type: string + enum: + - http://www.w3.org/2000/09/xmldsig#sha1 + - 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 + - in: query + name: is_backchannel + schema: + type: boolean + - in: query + name: issuer + schema: + type: string + - in: query + name: name + schema: + type: string + - in: query + name: name_id_mapping + schema: + type: string + format: uuid + - 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: property_mappings + schema: + type: array + items: + type: string + format: uuid + explode: true + style: form + - name: search + required: false + in: query + description: A search term. + schema: + type: string + - in: query + name: session_valid_not_on_or_after + schema: + type: string + - in: query + name: signature_algorithm + schema: + type: string + enum: + - http://www.w3.org/2000/09/xmldsig#dsa-sha1 + - http://www.w3.org/2000/09/xmldsig#rsa-sha1 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512 + - 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 + - in: query + name: signing_kp + schema: + type: string + format: uuid + - in: query + name: sp_binding + schema: + type: string + title: Service Provider Binding + enum: + - post + - redirect + description: |+ + This determines how authentik sends the response back to the Service Provider. + + - in: query + name: verification_kp + schema: + type: string + format: uuid + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSAMLProviderList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: providers_saml_create + description: SAMLProvider Viewset + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProviderRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/saml/{id}/: + get: + operationId: providers_saml_retrieve + description: SAMLProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: providers_saml_update + description: SAMLProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProviderRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: providers_saml_partial_update + description: SAMLProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML Provider. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSAMLProviderRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLProvider' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: providers_saml_destroy + description: SAMLProvider Viewset + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML 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/saml/{id}/metadata/: + get: + operationId: providers_saml_metadata_retrieve + description: Return metadata as XML string + parameters: + - in: query + name: download + schema: + type: boolean + - in: query + name: force_binding + schema: + type: string + enum: + - urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST + - urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect + description: Optionally force the metadata to only include one binding. + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML Provider. + required: true + tags: + - providers + security: + - authentik: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SAMLMetadata' + description: '' + '404': + description: Provider has no application assigned + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/saml/{id}/preview_user/: + get: + operationId: providers_saml_preview_user_retrieve + description: Preview user data for provider + parameters: + - in: query + name: for_user + schema: + type: integer + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SAML Provider. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PropertyMappingPreview' + description: '' + '400': + description: Bad request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /providers/saml/{id}/used_by/: + get: + operationId: providers_saml_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 SAML Provider. required: true tags: - providers @@ -18991,6 +20696,8 @@ paths: - authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovidermapping - authentik_providers_ldap.ldapprovider + - authentik_providers_microsoft_entra.microsoftentraprovider + - authentik_providers_microsoft_entra.microsoftentraprovidermapping - authentik_providers_oauth2.oauth2provider - authentik_providers_oauth2.scopemapping - authentik_providers_proxy.proxyprovider @@ -19208,6 +20915,8 @@ paths: - authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovidermapping - authentik_providers_ldap.ldapprovider + - authentik_providers_microsoft_entra.microsoftentraprovider + - authentik_providers_microsoft_entra.microsoftentraprovidermapping - authentik_providers_oauth2.oauth2provider - authentik_providers_oauth2.scopemapping - authentik_providers_proxy.proxyprovider @@ -31131,6 +32840,7 @@ components: - authentik.enterprise - authentik.enterprise.audit - authentik.enterprise.providers.google_workspace + - authentik.enterprise.providers.microsoft_entra - authentik.enterprise.providers.rac - authentik.enterprise.stages.source - authentik.events @@ -34826,9 +36536,9 @@ components: - bind_continent_country - bind_continent_country_city type: string - GoogleProvider: + GoogleWorkspaceProvider: type: object - description: GoogleProvider Serializer + description: GoogleWorkspaceProvider Serializer properties: pk: type: integer @@ -34885,9 +36595,9 @@ components: format: uuid nullable: true user_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' group_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' default_group_email_domain: type: string required: @@ -34902,9 +36612,37 @@ components: - pk - verbose_name - verbose_name_plural - GoogleProviderMapping: + GoogleWorkspaceProviderGroup: + type: object + description: GoogleWorkspaceProviderGroup Serializer + properties: + id: + type: string + format: uuid + readOnly: true + group: + type: string + format: uuid + group_obj: + allOf: + - $ref: '#/components/schemas/UserGroup' + readOnly: true + required: + - group + - group_obj + - id + GoogleWorkspaceProviderGroupRequest: + type: object + description: GoogleWorkspaceProviderGroup Serializer + properties: + group: + type: string + format: uuid + required: + - group + GoogleWorkspaceProviderMapping: type: object - description: GoogleProviderMapping Serializer + description: GoogleWorkspaceProviderMapping Serializer properties: pk: type: string @@ -34947,9 +36685,9 @@ components: - pk - verbose_name - verbose_name_plural - GoogleProviderMappingRequest: + GoogleWorkspaceProviderMappingRequest: type: object - description: GoogleProviderMapping Serializer + description: GoogleWorkspaceProviderMapping Serializer properties: managed: type: string @@ -34969,9 +36707,9 @@ components: required: - expression - name - GoogleProviderRequest: + GoogleWorkspaceProviderRequest: type: object - description: GoogleProvider Serializer + description: GoogleWorkspaceProvider Serializer properties: name: type: string @@ -35003,9 +36741,9 @@ components: format: uuid nullable: true user_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' group_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' default_group_email_domain: type: string minLength: 1 @@ -35014,12 +36752,32 @@ components: - default_group_email_domain - delegated_subject - name - GoogleWorkspaceDeleteAction: - enum: - - do_nothing - - delete - - suspend - type: string + GoogleWorkspaceProviderUser: + type: object + description: GoogleWorkspaceProviderUser Serializer + properties: + id: + type: string + format: uuid + readOnly: true + user: + type: integer + user_obj: + allOf: + - $ref: '#/components/schemas/GroupMember' + readOnly: true + required: + - id + - user + - user_obj + GoogleWorkspaceProviderUserRequest: + type: object + description: GoogleWorkspaceProviderUser Serializer + properties: + user: + type: integer + required: + - user Group: type: object description: Group Serializer @@ -36435,6 +38193,242 @@ components: required: - labels - name + MicrosoftEntraProvider: + type: object + description: MicrosoftEntraProvider 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_backchannel_application_slug: + type: string + description: Internal application name, used in URLs. + readOnly: true + assigned_backchannel_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 + client_id: + type: string + client_secret: + type: string + tenant_id: + type: string + exclude_users_service_account: + type: boolean + filter_group: + type: string + format: uuid + nullable: true + user_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + group_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + required: + - assigned_backchannel_application_name + - assigned_backchannel_application_slug + - client_id + - client_secret + - component + - meta_model_name + - name + - pk + - tenant_id + - verbose_name + - verbose_name_plural + MicrosoftEntraProviderGroup: + type: object + description: MicrosoftEntraProviderGroup Serializer + properties: + id: + type: string + format: uuid + readOnly: true + group: + type: string + format: uuid + group_obj: + allOf: + - $ref: '#/components/schemas/UserGroup' + readOnly: true + required: + - group + - group_obj + - id + MicrosoftEntraProviderGroupRequest: + type: object + description: MicrosoftEntraProviderGroup Serializer + properties: + group: + type: string + format: uuid + required: + - group + MicrosoftEntraProviderMapping: + type: object + description: MicrosoftEntraProviderMapping Serializer + properties: + pk: + type: string + format: uuid + readOnly: true + title: Pm uuid + managed: + type: string + nullable: true + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This 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 + MicrosoftEntraProviderMappingRequest: + type: object + description: MicrosoftEntraProviderMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This 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 + MicrosoftEntraProviderRequest: + type: object + description: MicrosoftEntraProvider 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. + client_id: + type: string + minLength: 1 + client_secret: + type: string + minLength: 1 + tenant_id: + type: string + minLength: 1 + exclude_users_service_account: + type: boolean + filter_group: + type: string + format: uuid + nullable: true + user_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + group_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + required: + - client_id + - client_secret + - name + - tenant_id + MicrosoftEntraProviderUser: + type: object + description: MicrosoftEntraProviderUser Serializer + properties: + id: + type: string + format: uuid + readOnly: true + user: + type: integer + user_obj: + allOf: + - $ref: '#/components/schemas/GroupMember' + readOnly: true + required: + - id + - user + - user_obj + MicrosoftEntraProviderUserRequest: + type: object + description: MicrosoftEntraProviderUser Serializer + properties: + user: + type: integer + required: + - user ModelEnum: enum: - authentik_tenants.domain @@ -36506,6 +38500,8 @@ components: - authentik_enterprise.license - authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovidermapping + - authentik_providers_microsoft_entra.microsoftentraprovider + - authentik_providers_microsoft_entra.microsoftentraprovidermapping - authentik_providers_rac.racprovider - authentik_providers_rac.endpoint - authentik_providers_rac.racpropertymapping @@ -37285,6 +39281,12 @@ components: - token_endpoint - token_endpoint_auth_methods_supported - userinfo_endpoint + OutgoingSyncDeleteAction: + enum: + - do_nothing + - delete + - suspend + type: string Outpost: type: object description: Outpost Serializer @@ -37792,7 +39794,31 @@ components: required: - pagination - results - PaginatedGoogleProviderList: + PaginatedGoogleWorkspaceProviderGroupList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/GoogleWorkspaceProviderGroup' + required: + - pagination + - results + PaginatedGoogleWorkspaceProviderList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/GoogleWorkspaceProvider' + required: + - pagination + - results + PaginatedGoogleWorkspaceProviderMappingList: type: object properties: pagination: @@ -37800,11 +39826,11 @@ components: results: type: array items: - $ref: '#/components/schemas/GoogleProvider' + $ref: '#/components/schemas/GoogleWorkspaceProviderMapping' required: - pagination - results - PaginatedGoogleProviderMappingList: + PaginatedGoogleWorkspaceProviderUserList: type: object properties: pagination: @@ -37812,7 +39838,7 @@ components: results: type: array items: - $ref: '#/components/schemas/GoogleProviderMapping' + $ref: '#/components/schemas/GoogleWorkspaceProviderUser' required: - pagination - results @@ -37936,6 +39962,54 @@ components: required: - pagination - results + PaginatedMicrosoftEntraProviderGroupList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/MicrosoftEntraProviderGroup' + required: + - pagination + - results + PaginatedMicrosoftEntraProviderList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/MicrosoftEntraProvider' + required: + - pagination + - results + PaginatedMicrosoftEntraProviderMappingList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/MicrosoftEntraProviderMapping' + required: + - pagination + - results + PaginatedMicrosoftEntraProviderUserList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/MicrosoftEntraProviderUser' + required: + - pagination + - results PaginatedNotificationList: type: object properties: @@ -39736,9 +41810,16 @@ components: 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. - PatchedGoogleProviderMappingRequest: + PatchedGoogleWorkspaceProviderGroupRequest: type: object - description: GoogleProviderMapping Serializer + description: GoogleWorkspaceProviderGroup Serializer + properties: + group: + type: string + format: uuid + PatchedGoogleWorkspaceProviderMappingRequest: + type: object + description: GoogleWorkspaceProviderMapping Serializer properties: managed: type: string @@ -39755,9 +41836,9 @@ components: expression: type: string minLength: 1 - PatchedGoogleProviderRequest: + PatchedGoogleWorkspaceProviderRequest: type: object - description: GoogleProvider Serializer + description: GoogleWorkspaceProvider Serializer properties: name: type: string @@ -39789,12 +41870,18 @@ components: format: uuid nullable: true user_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' group_delete_action: - $ref: '#/components/schemas/GoogleWorkspaceDeleteAction' + $ref: '#/components/schemas/OutgoingSyncDeleteAction' default_group_email_domain: type: string minLength: 1 + PatchedGoogleWorkspaceProviderUserRequest: + type: object + description: GoogleWorkspaceProviderUser Serializer + properties: + user: + type: integer PatchedGroupRequest: type: object description: Group Serializer @@ -40150,6 +42237,75 @@ components: key: type: string minLength: 1 + PatchedMicrosoftEntraProviderGroupRequest: + type: object + description: MicrosoftEntraProviderGroup Serializer + properties: + group: + type: string + format: uuid + PatchedMicrosoftEntraProviderMappingRequest: + type: object + description: MicrosoftEntraProviderMapping Serializer + properties: + managed: + type: string + nullable: true + minLength: 1 + title: Managed by authentik + description: Objects that are managed by authentik. These objects are created + and updated automatically. This 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 + PatchedMicrosoftEntraProviderRequest: + type: object + description: MicrosoftEntraProvider 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. + client_id: + type: string + minLength: 1 + client_secret: + type: string + minLength: 1 + tenant_id: + type: string + minLength: 1 + exclude_users_service_account: + type: boolean + filter_group: + type: string + format: uuid + nullable: true + user_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + group_delete_action: + $ref: '#/components/schemas/OutgoingSyncDeleteAction' + PatchedMicrosoftEntraProviderUserRequest: + type: object + description: MicrosoftEntraProviderUser Serializer + properties: + user: + type: integer PatchedNotificationRequest: type: object description: Notification Serializer @@ -42340,6 +44496,7 @@ components: enum: - authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_ldap.ldapprovider + - authentik_providers_microsoft_entra.microsoftentraprovider - authentik_providers_oauth2.oauth2provider - authentik_providers_proxy.proxyprovider - authentik_providers_rac.racprovider @@ -46369,8 +48526,9 @@ components: - count modelRequest: oneOf: - - $ref: '#/components/schemas/GoogleProviderRequest' + - $ref: '#/components/schemas/GoogleWorkspaceProviderRequest' - $ref: '#/components/schemas/LDAPProviderRequest' + - $ref: '#/components/schemas/MicrosoftEntraProviderRequest' - $ref: '#/components/schemas/OAuth2ProviderRequest' - $ref: '#/components/schemas/ProxyProviderRequest' - $ref: '#/components/schemas/RACProviderRequest' @@ -46380,8 +48538,9 @@ components: discriminator: propertyName: provider_model mapping: - authentik_providers_google_workspace.googleworkspaceprovider: '#/components/schemas/GoogleProviderRequest' + authentik_providers_google_workspace.googleworkspaceprovider: '#/components/schemas/GoogleWorkspaceProviderRequest' authentik_providers_ldap.ldapprovider: '#/components/schemas/LDAPProviderRequest' + authentik_providers_microsoft_entra.microsoftentraprovider: '#/components/schemas/MicrosoftEntraProviderRequest' authentik_providers_oauth2.oauth2provider: '#/components/schemas/OAuth2ProviderRequest' authentik_providers_proxy.proxyprovider: '#/components/schemas/ProxyProviderRequest' authentik_providers_rac.racprovider: '#/components/schemas/RACProviderRequest' diff --git a/web/src/admin/property-mappings/PropertyMappingGoogleWorkspaceForm.ts b/web/src/admin/property-mappings/PropertyMappingGoogleWorkspaceForm.ts index a69daead3cc6..3677fc3a991c 100644 --- a/web/src/admin/property-mappings/PropertyMappingGoogleWorkspaceForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingGoogleWorkspaceForm.ts @@ -10,11 +10,11 @@ import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { GoogleProviderMapping, PropertymappingsApi } from "@goauthentik/api"; +import { GoogleWorkspaceProviderMapping, PropertymappingsApi } from "@goauthentik/api"; @customElement("ak-property-mapping-google-workspace-form") -export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm { - loadInstance(pk: string): Promise { +export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm { + loadInstance(pk: string): Promise { return new PropertymappingsApi( DEFAULT_CONFIG, ).propertymappingsProviderGoogleWorkspaceRetrieve({ @@ -22,19 +22,19 @@ export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm< }); } - async send(data: GoogleProviderMapping): Promise { + async send(data: GoogleWorkspaceProviderMapping): Promise { if (this.instance) { return new PropertymappingsApi( DEFAULT_CONFIG, ).propertymappingsProviderGoogleWorkspaceUpdate({ - pmUuid: this.instance.pk || "", - googleProviderMappingRequest: data, + pmUuid: this.instance.pk, + googleWorkspaceProviderMappingRequest: data, }); } else { return new PropertymappingsApi( DEFAULT_CONFIG, ).propertymappingsProviderGoogleWorkspaceCreate({ - googleProviderMappingRequest: data, + googleWorkspaceProviderMappingRequest: data, }); } } diff --git a/web/src/admin/property-mappings/PropertyMappingLDAPForm.ts b/web/src/admin/property-mappings/PropertyMappingLDAPForm.ts index 0b5afe81b0a8..4647a3e70a4b 100644 --- a/web/src/admin/property-mappings/PropertyMappingLDAPForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingLDAPForm.ts @@ -23,7 +23,7 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, lDAPPropertyMappingRequest: data, }); } else { diff --git a/web/src/admin/property-mappings/PropertyMappingListPage.ts b/web/src/admin/property-mappings/PropertyMappingListPage.ts index 4b45cbc53bee..be9eb3ed6679 100644 --- a/web/src/admin/property-mappings/PropertyMappingListPage.ts +++ b/web/src/admin/property-mappings/PropertyMappingListPage.ts @@ -1,5 +1,6 @@ import "@goauthentik/admin/property-mappings/PropertyMappingGoogleWorkspaceForm"; import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; +import "@goauthentik/admin/property-mappings/PropertyMappingMicrosoftEntraForm"; import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; import "@goauthentik/admin/property-mappings/PropertyMappingRACForm"; import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; diff --git a/web/src/admin/property-mappings/PropertyMappingMicrosoftEntraForm.ts b/web/src/admin/property-mappings/PropertyMappingMicrosoftEntraForm.ts new file mode 100644 index 000000000000..32a389b409d1 --- /dev/null +++ b/web/src/admin/property-mappings/PropertyMappingMicrosoftEntraForm.ts @@ -0,0 +1,72 @@ +import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { docLink } from "@goauthentik/common/global"; +import "@goauthentik/elements/CodeMirror"; +import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { MicrosoftEntraProviderMapping, PropertymappingsApi } from "@goauthentik/api"; + +@customElement("ak-property-mapping-microsoft-entra-form") +export class PropertyMappingMicrosoftEntraForm extends BasePropertyMappingForm { + loadInstance(pk: string): Promise { + return new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderGoogleWorkspaceRetrieve({ + pmUuid: pk, + }); + } + + async send(data: MicrosoftEntraProviderMapping): Promise { + if (this.instance) { + return new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderMicrosoftEntraUpdate({ + pmUuid: this.instance.pk, + microsoftEntraProviderMappingRequest: data, + }); + } else { + return new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderMicrosoftEntraCreate({ + microsoftEntraProviderMappingRequest: data, + }); + } + } + + renderForm(): TemplateResult { + return html` + + + + + +

+ ${msg("Expression using Python.")} + + ${msg("See documentation for a list of all variables.")} + +

+
`; + } +} diff --git a/web/src/admin/property-mappings/PropertyMappingNotification.ts b/web/src/admin/property-mappings/PropertyMappingNotification.ts index 9991d2a32f0e..fe88b1a8c1a7 100644 --- a/web/src/admin/property-mappings/PropertyMappingNotification.ts +++ b/web/src/admin/property-mappings/PropertyMappingNotification.ts @@ -29,7 +29,7 @@ export class PropertyMappingNotification extends ModelForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, notificationWebhookMappingRequest: data, }); } else { diff --git a/web/src/admin/property-mappings/PropertyMappingRACForm.ts b/web/src/admin/property-mappings/PropertyMappingRACForm.ts index 145a63556b7f..23609c7bb7b7 100644 --- a/web/src/admin/property-mappings/PropertyMappingRACForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingRACForm.ts @@ -51,7 +51,7 @@ export class PropertyMappingLDAPForm extends ModelForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, rACPropertyMappingRequest: data, }); } else { diff --git a/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts b/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts index 101455c84c8a..9371bb5caaa2 100644 --- a/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingSAMLForm.ts @@ -23,7 +23,7 @@ export class PropertyMappingSAMLForm extends BasePropertyMappingForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, sAMLPropertyMappingRequest: data, }); } else { diff --git a/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts b/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts index 30993cbbd53f..51bcfc65d33b 100644 --- a/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingSCIMForm.ts @@ -23,7 +23,7 @@ export class PropertyMappingSCIMForm extends BasePropertyMappingForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, sCIMMappingRequest: data, }); } else { diff --git a/web/src/admin/property-mappings/PropertyMappingScopeForm.ts b/web/src/admin/property-mappings/PropertyMappingScopeForm.ts index 3df60889f8aa..6403e0c6ec85 100644 --- a/web/src/admin/property-mappings/PropertyMappingScopeForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingScopeForm.ts @@ -23,7 +23,7 @@ export class PropertyMappingScopeForm extends BasePropertyMappingForm { if (this.instance) { return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeUpdate({ - pmUuid: this.instance.pk || "", + pmUuid: this.instance.pk, scopeMappingRequest: data, }); } else { diff --git a/web/src/admin/providers/ProviderListPage.ts b/web/src/admin/providers/ProviderListPage.ts index 0e840cb57c0c..c2190d9f3b47 100644 --- a/web/src/admin/providers/ProviderListPage.ts +++ b/web/src/admin/providers/ProviderListPage.ts @@ -1,13 +1,14 @@ import "@goauthentik/admin/applications/ApplicationWizardHint"; import "@goauthentik/admin/providers/ProviderWizard"; +import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm"; import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; +import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/rac/RACProviderForm"; import "@goauthentik/admin/providers/radius/RadiusProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm"; import "@goauthentik/admin/providers/scim/SCIMProviderForm"; -import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { uiConfig } from "@goauthentik/common/ui/config"; import "@goauthentik/elements/buttons/SpinnerButton"; diff --git a/web/src/admin/providers/ProviderViewPage.ts b/web/src/admin/providers/ProviderViewPage.ts index e76386eb3533..a1e2a4ce2ef8 100644 --- a/web/src/admin/providers/ProviderViewPage.ts +++ b/web/src/admin/providers/ProviderViewPage.ts @@ -1,11 +1,12 @@ +import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage"; import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage"; +import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage"; import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage"; import "@goauthentik/admin/providers/rac/RACProviderViewPage"; import "@goauthentik/admin/providers/radius/RadiusProviderViewPage"; import "@goauthentik/admin/providers/saml/SAMLProviderViewPage"; import "@goauthentik/admin/providers/scim/SCIMProviderViewPage"; -import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; @@ -75,6 +76,10 @@ export class ProviderViewPage extends AKElement { return html``; + case "ak-provider-microsoft-entra-form": + return html``; default: return html`

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

`; } diff --git a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts index e314e608baae..67f1f800ffeb 100644 --- a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts +++ b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts @@ -16,17 +16,17 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { CoreApi, CoreGroupsListRequest, - GoogleProvider, - GoogleWorkspaceDeleteAction, + GoogleWorkspaceProvider, Group, - PaginatedGoogleProviderMappingList, + OutgoingSyncDeleteAction, + PaginatedGoogleWorkspaceProviderMappingList, PropertymappingsApi, ProvidersApi, } from "@goauthentik/api"; @customElement("ak-provider-google-workspace-form") -export class GoogleWorkspaceProviderFormPage extends BaseProviderForm { - loadInstance(pk: number): Promise { +export class GoogleWorkspaceProviderFormPage extends BaseProviderForm { + loadInstance(pk: number): Promise { return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceRetrieve({ id: pk, }); @@ -40,17 +40,17 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm { + async send(data: GoogleWorkspaceProvider): Promise { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUpdate({ - id: this.instance.pk || 0, - googleProviderRequest: data, + id: this.instance.pk, + googleWorkspaceProviderRequest: data, }); } else { return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceCreate({ - googleProviderRequest: data, + googleWorkspaceProviderRequest: data, }); } } @@ -76,7 +76,9 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm -

${msg("TODO")}

+

+ ${msg("Google Cloud credentials file.")} +

-

${msg("TODO")}

+

+ ${msg( + "Email address of the user the actions of authentik will be delegated to.", + )} +

{ + @property({ type: Number }) + providerId?: number; + + searchEnabled(): boolean { + return true; + } + + async apiEndpoint(page: number): Promise> { + return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceGroupsList({ + page: page, + pageSize: (await uiConfig()).pagination.perPage, + ordering: this.order, + search: this.search || "", + providerId: this.providerId, + }); + } + + columns(): TableColumn[] { + return [new TableColumn(msg("Name")), new TableColumn(msg("ID"))]; + } + + row(item: GoogleWorkspaceProviderGroup): TemplateResult[] { + return [ + html` +
${item.groupObj.name}
+
`, + html`${item.id}`, + ]; + } +} diff --git a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderUserList.ts b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderUserList.ts new file mode 100644 index 000000000000..d8bf35b249cf --- /dev/null +++ b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderUserList.ts @@ -0,0 +1,43 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { uiConfig } from "@goauthentik/common/ui/config"; +import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import { GoogleWorkspaceProviderUser, ProvidersApi } from "@goauthentik/api"; + +@customElement("ak-provider-google-workspace-users-list") +export class GoogleWorkspaceProviderUserList extends Table { + @property({ type: Number }) + providerId?: number; + + searchEnabled(): boolean { + return true; + } + + async apiEndpoint(page: number): Promise> { + return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersList({ + page: page, + pageSize: (await uiConfig()).pagination.perPage, + ordering: this.order, + search: this.search || "", + providerId: this.providerId, + }); + } + + columns(): TableColumn[] { + return [new TableColumn(msg("Username")), new TableColumn(msg("ID"))]; + } + + row(item: GoogleWorkspaceProviderUser): TemplateResult[] { + return [ + html` +
${item.userObj.username}
+ ${item.userObj.name} +
`, + html`${item.id}`, + ]; + } +} diff --git a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage.ts b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage.ts index 215bd5f7904c..4ca5b16a9073 100644 --- a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage.ts +++ b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage.ts @@ -1,16 +1,18 @@ -import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm"; +import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm"; +import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderGroupList"; +import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderUserList"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; import "@goauthentik/components/events/ObjectChangelog"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/Markdown"; +import "@goauthentik/elements/SyncStatusCard"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ModalButton"; -import "@goauthentik/elements/events/LogViewer"; import "@goauthentik/elements/rbac/ObjectPermissionsPage"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; @@ -28,11 +30,10 @@ import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { - GoogleProvider, + GoogleWorkspaceProvider, ProvidersApi, RbacPermissionsAssignedByUsersListModelEnum, SyncStatus, - SystemTaskStatusEnum, } from "@goauthentik/api"; @customElement("ak-provider-google-workspace-view") @@ -41,7 +42,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement { providerID?: number; @state() - provider?: GoogleProvider; + provider?: GoogleWorkspaceProvider; @state() syncState?: SyncStatus; @@ -121,6 +122,28 @@ export class GoogleWorkspaceProviderViewPage extends AKElement { +
+
+ +
+
+
+
+ +
+
`; } - renderSyncStatus(): TemplateResult { - if (!this.syncState) { - return html`${msg("No sync status.")}`; - } - if (this.syncState.isRunning) { - return html`${msg("Sync currently running.")}`; - } - if (this.syncState.tasks.length < 1) { - return html`${msg("Not synced yet.")}`; - } - return html` -
    - ${this.syncState.tasks.map((task) => { - let header = ""; - if (task.status === SystemTaskStatusEnum.Warning) { - header = msg("Task finished with warnings"); - } else if (task.status === SystemTaskStatusEnum.Error) { - header = msg("Task finished with errors"); - } else { - header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`); - } - return html`
  • -

    ${task.name}

    -
      -
    • ${header}
    • - -
    -
  • `; - })} -
- `; - } - renderTabOverview(): TemplateResult { if (!this.provider) { return html``; @@ -197,7 +187,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement { -
-
-

${msg("Sync status")}

-
-
${this.renderSyncStatus()}
- +
+ { + return new ProvidersApi( + DEFAULT_CONFIG, + ).providersGoogleWorkspaceSyncStatusRetrieve({ + id: this.provider?.pk || 0, + }); + }} + .triggerSync=${() => { + return new ProvidersApi( + DEFAULT_CONFIG, + ).providersGoogleWorkspacePartialUpdate({ + id: this.provider?.pk || 0, + patchedGoogleWorkspaceProviderRequest: {}, + }); + }} + >
`; } diff --git a/web/src/admin/providers/ldap/LDAPProviderForm.ts b/web/src/admin/providers/ldap/LDAPProviderForm.ts index db426cc1a42e..2011f7cace73 100644 --- a/web/src/admin/providers/ldap/LDAPProviderForm.ts +++ b/web/src/admin/providers/ldap/LDAPProviderForm.ts @@ -35,7 +35,7 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, lDAPProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage.ts b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage.ts new file mode 100644 index 000000000000..5d981b0507c9 --- /dev/null +++ b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage.ts @@ -0,0 +1,286 @@ +import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; +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 "@goauthentik/elements/forms/Radio"; +import "@goauthentik/elements/forms/SearchSelect"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + CoreApi, + CoreGroupsListRequest, + Group, + MicrosoftEntraProvider, + OutgoingSyncDeleteAction, + PaginatedMicrosoftEntraProviderMappingList, + PropertymappingsApi, + ProvidersApi, +} from "@goauthentik/api"; + +@customElement("ak-provider-microsoft-entra-form") +export class MicrosoftEntraProviderFormPage extends BaseProviderForm { + loadInstance(pk: number): Promise { + return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraRetrieve({ + id: pk, + }); + } + + async load(): Promise { + this.propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderMicrosoftEntraList({ + ordering: "managed", + }); + } + + propertyMappings?: PaginatedMicrosoftEntraProviderMappingList; + + async send(data: MicrosoftEntraProvider): Promise { + if (this.instance) { + return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUpdate({ + id: this.instance.pk, + microsoftEntraProviderRequest: data, + }); + } else { + return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraCreate({ + microsoftEntraProviderRequest: data, + }); + } + } + + renderForm(): TemplateResult { + return html` + + + + ${msg("Protocol settings")} +
+ + +

+ ${msg("Client ID for the app registration.")} +

+
+ + +

+ ${msg("Client secret for the app registration.")} +

+
+ + +

+ ${msg("ID of the tenant accounts will be synced into.")} +

+
+ + + + +
+
+ + ${msg("User filtering")} +
+ + + + + => { + const args: CoreGroupsListRequest = { + ordering: "name", + }; + if (query !== undefined) { + args.search = query; + } + const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList( + args, + ); + return groups.results; + }} + .renderElement=${(group: Group): string => { + return group.name; + }} + .value=${(group: Group | undefined): string | undefined => { + return group ? group.pk : undefined; + }} + .selected=${(group: Group): boolean => { + return group.pk === this.instance?.filterGroup; + }} + ?blankable=${true} + > + +

+ ${msg("Only sync users within the selected group.")} +

+
+
+
+ + ${msg("Attribute mapping")} +
+ + +

+ ${msg("Property mappings used to user mapping.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + +

+ ${msg("Property mappings used to group creation.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+
+
`; + } +} diff --git a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList.ts b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList.ts new file mode 100644 index 000000000000..6760ead66e56 --- /dev/null +++ b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList.ts @@ -0,0 +1,42 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { uiConfig } from "@goauthentik/common/ui/config"; +import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import { MicrosoftEntraProviderGroup, ProvidersApi } from "@goauthentik/api"; + +@customElement("ak-provider-microsoft-entra-groups-list") +export class MicrosoftEntraProviderGroupList extends Table { + @property({ type: Number }) + providerId?: number; + + searchEnabled(): boolean { + return true; + } + + async apiEndpoint(page: number): Promise> { + return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraGroupsList({ + page: page, + pageSize: (await uiConfig()).pagination.perPage, + ordering: this.order, + search: this.search || "", + providerId: this.providerId, + }); + } + + columns(): TableColumn[] { + return [new TableColumn(msg("Name")), new TableColumn(msg("ID"))]; + } + + row(item: MicrosoftEntraProviderGroup): TemplateResult[] { + return [ + html` +
${item.groupObj.name}
+
`, + html`${item.id}`, + ]; + } +} diff --git a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList.ts b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList.ts new file mode 100644 index 000000000000..aadb8abc9c65 --- /dev/null +++ b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList.ts @@ -0,0 +1,43 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { uiConfig } from "@goauthentik/common/ui/config"; +import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import { MicrosoftEntraProviderUser, ProvidersApi } from "@goauthentik/api"; + +@customElement("ak-provider-microsoft-entra-users-list") +export class MicrosoftEntraProviderUserList extends Table { + @property({ type: Number }) + providerId?: number; + + searchEnabled(): boolean { + return true; + } + + async apiEndpoint(page: number): Promise> { + return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUsersList({ + page: page, + pageSize: (await uiConfig()).pagination.perPage, + ordering: this.order, + search: this.search || "", + providerId: this.providerId, + }); + } + + columns(): TableColumn[] { + return [new TableColumn(msg("Username")), new TableColumn(msg("ID"))]; + } + + row(item: MicrosoftEntraProviderUser): TemplateResult[] { + return [ + html` +
${item.userObj.username}
+ ${item.userObj.name} +
`, + html`${item.id}`, + ]; + } +} diff --git a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage.ts b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage.ts new file mode 100644 index 000000000000..c92c944639f5 --- /dev/null +++ b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage.ts @@ -0,0 +1,224 @@ +import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage"; +import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList"; +import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { EVENT_REFRESH } from "@goauthentik/common/constants"; +import "@goauthentik/components/events/ObjectChangelog"; +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/LogViewer"; +import "@goauthentik/elements/rbac/ObjectPermissionsPage"; + +import { msg } from "@lit/localize"; +import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +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 PFStack from "@patternfly/patternfly/layouts/Stack/stack.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { + MicrosoftEntraProvider, + ProvidersApi, + RbacPermissionsAssignedByUsersListModelEnum, + SyncStatus, +} from "@goauthentik/api"; + +@customElement("ak-provider-microsoft-entra-view") +export class MicrosoftEntraProviderViewPage extends AKElement { + @property({ type: Number }) + providerID?: number; + + @state() + provider?: MicrosoftEntraProvider; + + @state() + syncState?: SyncStatus; + + static get styles(): CSSResult[] { + return [ + PFBase, + PFButton, + PFBanner, + PFForm, + PFFormControl, + PFStack, + PFList, + PFGrid, + PFPage, + PFContent, + PFCard, + PFDescriptionList, + ]; + } + + constructor() { + super(); + this.addEventListener(EVENT_REFRESH, () => { + if (!this.provider?.pk) return; + this.providerID = this.provider?.pk; + }); + } + + fetchProvider(id: number) { + new ProvidersApi(DEFAULT_CONFIG) + .providersMicrosoftEntraRetrieve({ id }) + .then((prov) => (this.provider = prov)); + } + + willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("providerID") && this.providerID) { + this.fetchProvider(this.providerID); + } + } + + render(): TemplateResult { + if (!this.provider) { + return html``; + } + return html` +
{ + new ProvidersApi(DEFAULT_CONFIG) + .providersMicrosoftEntraSyncStatusRetrieve({ + id: this.provider?.pk || 0, + }) + .then((state) => { + this.syncState = state; + }) + .catch(() => { + this.syncState = undefined; + }); + }} + > + ${this.renderTabOverview()} +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
`; + } + + renderTabOverview(): TemplateResult { + if (!this.provider) { + return html``; + } + return html`
+ ${msg("Microsoft Entra Provider is in preview.")} + ${msg("Send us feedback!")} +
+ ${!this.provider?.assignedBackchannelApplicationName + ? html`
+ ${msg( + "Warning: Provider is not assigned to an application as backchannel provider.", + )} +
` + : html``} +
+
+
+
+
+
+ ${msg("Name")} +
+
+
+ ${this.provider.name} +
+
+
+
+
+ +
+ +
+ { + return new ProvidersApi( + DEFAULT_CONFIG, + ).providersMicrosoftEntraSyncStatusRetrieve({ + id: this.provider?.pk || 0, + }); + }} + .triggerSync=${() => { + return new ProvidersApi( + DEFAULT_CONFIG, + ).providersMicrosoftEntraPartialUpdate({ + id: this.provider?.pk || 0, + patchedMicrosoftEntraProviderRequest: {}, + }); + }} + > +
+
`; + } +} diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index 74f3acbeb8fa..81f0605cbc50 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -146,7 +146,7 @@ export class OAuth2ProviderFormPage extends BaseProviderForm { async send(data: OAuth2Provider): Promise { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({ - id: this.instance.pk || 0, + id: this.instance.pk, oAuth2ProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index ddd554572548..d05c433912f7 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -72,7 +72,7 @@ export class ProxyProviderFormPage extends BaseProviderForm { } if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, proxyProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/rac/RACProviderForm.ts b/web/src/admin/providers/rac/RACProviderForm.ts index be8de96376de..0fef06697ee9 100644 --- a/web/src/admin/providers/rac/RACProviderForm.ts +++ b/web/src/admin/providers/rac/RACProviderForm.ts @@ -54,7 +54,7 @@ export class RACProviderFormPage extends ModelForm { async send(data: RACProvider): Promise { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersRacUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, rACProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index 2d08fc0b604a..fd6d74ef0094 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -24,7 +24,7 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersRadiusUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, radiusProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index 9ea33bf76d8d..9cb5670d0f7d 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -47,7 +47,7 @@ export class SAMLProviderFormPage extends BaseProviderForm { async send(data: SAMLProvider): Promise { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersSamlUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, sAMLProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/scim/SCIMProviderForm.ts b/web/src/admin/providers/scim/SCIMProviderForm.ts index 1e66268e0c94..a97fc270a0fa 100644 --- a/web/src/admin/providers/scim/SCIMProviderForm.ts +++ b/web/src/admin/providers/scim/SCIMProviderForm.ts @@ -42,7 +42,7 @@ export class SCIMProviderFormPage extends BaseProviderForm { async send(data: SCIMProvider): Promise { if (this.instance) { return new ProvidersApi(DEFAULT_CONFIG).providersScimUpdate({ - id: this.instance.pk || 0, + id: this.instance.pk, sCIMProviderRequest: data, }); } else { diff --git a/web/src/admin/providers/scim/SCIMProviderViewPage.ts b/web/src/admin/providers/scim/SCIMProviderViewPage.ts index 948c6f0dc811..5e852946b27d 100644 --- a/web/src/admin/providers/scim/SCIMProviderViewPage.ts +++ b/web/src/admin/providers/scim/SCIMProviderViewPage.ts @@ -5,13 +5,13 @@ import "@goauthentik/components/events/ObjectChangelog"; import MDSCIMProvider from "@goauthentik/docs/providers/scim/index.md"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/Markdown"; +import "@goauthentik/elements/SyncStatusCard"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ModalButton"; -import "@goauthentik/elements/events/LogViewer"; import "@goauthentik/elements/rbac/ObjectPermissionsPage"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; @@ -32,8 +32,6 @@ import { ProvidersApi, RbacPermissionsAssignedByUsersListModelEnum, SCIMProvider, - SyncStatus, - SystemTaskStatusEnum, } from "@goauthentik/api"; @customElement("ak-provider-scim-view") @@ -44,9 +42,6 @@ export class SCIMProviderViewPage extends AKElement { @state() provider?: SCIMProvider; - @state() - syncState?: SyncStatus; - static get styles(): CSSResult[] { return [ PFBase, @@ -89,22 +84,7 @@ export class SCIMProviderViewPage extends AKElement { return html``; } return html` -
{ - new ProvidersApi(DEFAULT_CONFIG) - .providersScimSyncStatusRetrieve({ - id: this.provider?.pk || 0, - }) - .then((state) => { - this.syncState = state; - }) - .catch(() => { - this.syncState = undefined; - }); - }} - > +
${this.renderTabOverview()}
`; } - renderSyncStatus(): TemplateResult { - if (!this.syncState) { - return html`${msg("No sync status.")}`; - } - if (this.syncState.isRunning) { - return html`${msg("Sync currently running.")}`; - } - if (this.syncState.tasks.length < 1) { - return html`${msg("Not synced yet.")}`; - } - return html` -
    - ${this.syncState.tasks.map((task) => { - let header = ""; - if (task.status === SystemTaskStatusEnum.Warning) { - header = msg("Task finished with warnings"); - } else if (task.status === SystemTaskStatusEnum.Error) { - header = msg("Task finished with errors"); - } else { - header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`); - } - return html`
  • -

    ${task.name}

    -
      -
    • ${header}
    • - -
    -
  • `; - })} -
- `; - } - renderTabOverview(): TemplateResult { if (!this.provider) { return html``; @@ -218,33 +165,22 @@ export class SCIMProviderViewPage extends AKElement { -
-
-

${msg("Sync status")}

-
-
${this.renderSyncStatus()}
- +
+ { + return new ProvidersApi( + DEFAULT_CONFIG, + ).providersScimSyncStatusRetrieve({ + id: this.provider?.pk || 0, + }); + }} + .triggerSync=${() => { + return new ProvidersApi(DEFAULT_CONFIG).providersScimPartialUpdate({ + id: this.provider?.pk || 0, + patchedSCIMProviderRequest: {}, + }); + }} + >
diff --git a/web/src/admin/sources/ldap/LDAPSourceViewPage.ts b/web/src/admin/sources/ldap/LDAPSourceViewPage.ts index 8d4a905a2f6e..b5fe955aa953 100644 --- a/web/src/admin/sources/ldap/LDAPSourceViewPage.ts +++ b/web/src/admin/sources/ldap/LDAPSourceViewPage.ts @@ -5,13 +5,14 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; import "@goauthentik/components/events/ObjectChangelog"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/CodeMirror"; +import "@goauthentik/elements/SyncStatusCard"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/rbac/ObjectPermissionsPage"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; @@ -29,7 +30,6 @@ import { RbacPermissionsAssignedByUsersListModelEnum, SourcesApi, SyncStatus, - SystemTaskStatusEnum, } from "@goauthentik/api"; @customElement("ak-source-ldap-view") @@ -63,41 +63,6 @@ export class LDAPSourceViewPage extends AKElement { }); } - renderSyncStatus(): TemplateResult { - if (!this.syncState) { - return html`${msg("No sync status.")}`; - } - if (this.syncState.isRunning) { - return html`${msg("Sync currently running.")}`; - } - if (this.syncState.tasks.length < 1) { - return html`${msg("Not synced yet.")}`; - } - return html` -
    - ${this.syncState.tasks.map((task) => { - let header = ""; - if (task.status === SystemTaskStatusEnum.Warning) { - header = msg("Task finished with warnings"); - } else if (task.status === SystemTaskStatusEnum.Error) { - header = msg("Task finished with errors"); - } else { - header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`); - } - return html`
  • -

    ${task.name}

    -
      -
    • ${header}
    • - ${task.messages.map((m) => { - return html`
    • ${m}
    • `; - })} -
    -
  • `; - })} -
- `; - } - load(): void { new SourcesApi(DEFAULT_CONFIG) .sourcesLdapSyncStatusRetrieve({ @@ -187,35 +152,22 @@ export class LDAPSourceViewPage extends AKElement { >
-
-
-

${msg("Sync status")}

-
-
${this.renderSyncStatus()}
- +
+ { + return new SourcesApi(DEFAULT_CONFIG).sourcesLdapSyncStatusRetrieve( + { + slug: this.source?.slug, + }, + ); + }} + .triggerSync=${() => { + return new SourcesApi(DEFAULT_CONFIG).sourcesLdapPartialUpdate({ + slug: this.source?.slug || "", + patchedLDAPSourceRequest: {}, + }); + }} + >
diff --git a/web/src/elements/SyncStatusCard.ts b/web/src/elements/SyncStatusCard.ts new file mode 100644 index 000000000000..db8a4f17d75e --- /dev/null +++ b/web/src/elements/SyncStatusCard.ts @@ -0,0 +1,119 @@ +import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants"; +import { getRelativeTime } from "@goauthentik/authentik/common/utils"; +import { AKElement } from "@goauthentik/authentik/elements/Base"; +import "@goauthentik/components/ak-status-label"; +import "@goauthentik/elements/EmptyState"; +import "@goauthentik/elements/events/LogViewer"; + +import { msg, str } from "@lit/localize"; +import { CSSResult, TemplateResult, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api"; + +@customElement("ak-sync-status-card") +export class SyncStatusCard extends AKElement { + @state() + syncState?: SyncStatus; + + @state() + loading = false; + + @property({ attribute: false }) + fetch!: () => Promise; + + @property({ attribute: false }) + triggerSync!: () => Promise; + + static get styles(): CSSResult[] { + return [PFBase, PFCard]; + } + + firstUpdated() { + this.loading = true; + this.fetch().then((status) => { + this.syncState = status; + this.loading = false; + }); + } + + renderSyncTask(task: SystemTask): TemplateResult { + return html`
  • + ${(this.syncState?.tasks || []).length > 1 ? html`${task.name}` : nothing} + + ${msg( + str`Finished ${getRelativeTime(task.finishTimestamp)} (${task.finishTimestamp.toLocaleString()})`, + )} + +
  • `; + } + + renderSyncStatus(): TemplateResult { + if (this.loading) { + return html``; + } + if (!this.syncState) { + return html`${msg("No sync status.")}`; + } + if (this.syncState.isRunning) { + return html`${msg("Sync currently running.")}`; + } + if (this.syncState.tasks.length < 1) { + return html`${msg("Not synced yet.")}`; + } + return html` +
      + ${this.syncState.tasks.map((task) => { + return this.renderSyncTask(task); + })} +
    + `; + } + + render(): TemplateResult { + return html`
    +
    ${msg("Sync status")}
    +
    ${this.renderSyncStatus()}
    + +
    `; + } +}