Merge branch 'main' into dev
* main: web: bump API Client version (#9656) enterprise/providers/microsoft_entra: initial account sync to microsoft entra (#9632) web: bump chromedriver from 124.0.1 to 124.0.2 in /tests/wdio (#9652) web: bump @sentry/browser from 7.113.0 to 7.114.0 in /web in the sentry group (#9653) core, web: update translations (#9650) translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9644) translate: Updates for file web/xliff/en.xlf in zh-Hans (#9646) translate: Updates for file web/xliff/en.xlf in zh_CN (#9645) translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9647)
This commit is contained in:
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@ -4,20 +4,21 @@
|
|||||||
"asgi",
|
"asgi",
|
||||||
"authentik",
|
"authentik",
|
||||||
"authn",
|
"authn",
|
||||||
|
"entra",
|
||||||
"goauthentik",
|
"goauthentik",
|
||||||
"jwks",
|
"jwks",
|
||||||
|
"kubernetes",
|
||||||
"oidc",
|
"oidc",
|
||||||
"openid",
|
"openid",
|
||||||
|
"passwordless",
|
||||||
"plex",
|
"plex",
|
||||||
"saml",
|
"saml",
|
||||||
"totp",
|
|
||||||
"webauthn",
|
|
||||||
"traefik",
|
|
||||||
"passwordless",
|
|
||||||
"kubernetes",
|
|
||||||
"sso",
|
|
||||||
"slo",
|
|
||||||
"scim",
|
"scim",
|
||||||
|
"slo",
|
||||||
|
"sso",
|
||||||
|
"totp",
|
||||||
|
"traefik",
|
||||||
|
"webauthn",
|
||||||
],
|
],
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
"todo-tree.tree.showBadges": true,
|
"todo-tree.tree.showBadges": true,
|
||||||
|
@ -39,6 +39,14 @@ from authentik.core.models import (
|
|||||||
)
|
)
|
||||||
from authentik.enterprise.license import LicenseKey
|
from authentik.enterprise.license import LicenseKey
|
||||||
from authentik.enterprise.models import LicenseUsage
|
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.enterprise.providers.rac.models import ConnectionToken
|
||||||
from authentik.events.logs import LogEvent, capture_logs
|
from authentik.events.logs import LogEvent, capture_logs
|
||||||
from authentik.events.models import SystemTask
|
from authentik.events.models import SystemTask
|
||||||
@ -86,6 +94,7 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
# Classes that have other dependencies
|
# Classes that have other dependencies
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
# Classes which are only internally managed
|
# Classes which are only internally managed
|
||||||
|
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
||||||
FlowToken,
|
FlowToken,
|
||||||
LicenseUsage,
|
LicenseUsage,
|
||||||
SCIMGroup,
|
SCIMGroup,
|
||||||
@ -100,6 +109,10 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
WebAuthnDeviceType,
|
WebAuthnDeviceType,
|
||||||
SCIMSourceUser,
|
SCIMSourceUser,
|
||||||
SCIMSourceGroup,
|
SCIMSourceGroup,
|
||||||
|
GoogleWorkspaceProviderUser,
|
||||||
|
GoogleWorkspaceProviderGroup,
|
||||||
|
MicrosoftEntraProviderUser,
|
||||||
|
MicrosoftEntraProviderGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"]
|
@ -11,16 +11,16 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping
|
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping
|
||||||
|
|
||||||
|
|
||||||
class GoogleProviderMappingSerializer(PropertyMappingSerializer):
|
class GoogleWorkspaceProviderMappingSerializer(PropertyMappingSerializer):
|
||||||
"""GoogleProviderMapping Serializer"""
|
"""GoogleWorkspaceProviderMapping Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GoogleWorkspaceProviderMapping
|
model = GoogleWorkspaceProviderMapping
|
||||||
fields = PropertyMappingSerializer.Meta.fields
|
fields = PropertyMappingSerializer.Meta.fields
|
||||||
|
|
||||||
|
|
||||||
class GoogleProviderMappingFilter(FilterSet):
|
class GoogleWorkspaceProviderMappingFilter(FilterSet):
|
||||||
"""Filter for GoogleProviderMapping"""
|
"""Filter for GoogleWorkspaceProviderMapping"""
|
||||||
|
|
||||||
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
|
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
|
||||||
|
|
||||||
@ -29,11 +29,11 @@ class GoogleProviderMappingFilter(FilterSet):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class GoogleProviderMappingViewSet(UsedByMixin, ModelViewSet):
|
class GoogleWorkspaceProviderMappingViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""GoogleProviderMapping Viewset"""
|
"""GoogleWorkspaceProviderMapping Viewset"""
|
||||||
|
|
||||||
queryset = GoogleWorkspaceProviderMapping.objects.all()
|
queryset = GoogleWorkspaceProviderMapping.objects.all()
|
||||||
serializer_class = GoogleProviderMappingSerializer
|
serializer_class = GoogleWorkspaceProviderMappingSerializer
|
||||||
filterset_class = GoogleProviderMappingFilter
|
filterset_class = GoogleWorkspaceProviderMappingFilter
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
@ -10,8 +10,8 @@ from authentik.enterprise.providers.google_workspace.tasks import google_workspa
|
|||||||
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
|
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
|
||||||
|
|
||||||
|
|
||||||
class GoogleProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
||||||
"""GoogleProvider Serializer"""
|
"""GoogleWorkspaceProvider Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GoogleWorkspaceProvider
|
model = GoogleWorkspaceProvider
|
||||||
@ -38,11 +38,11 @@ class GoogleProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
|||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
|
||||||
class GoogleProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
|
class GoogleWorkspaceProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
|
||||||
"""GoogleProvider Viewset"""
|
"""GoogleWorkspaceProvider Viewset"""
|
||||||
|
|
||||||
queryset = GoogleWorkspaceProvider.objects.all()
|
queryset = GoogleWorkspaceProvider.objects.all()
|
||||||
serializer_class = GoogleProviderSerializer
|
serializer_class = GoogleWorkspaceProviderSerializer
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"name",
|
"name",
|
||||||
"exclude_users_service_account",
|
"exclude_users_service_account",
|
||||||
|
33
authentik/enterprise/providers/google_workspace/api/users.py
Normal file
33
authentik/enterprise/providers/google_workspace/api/users.py
Normal file
@ -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"]
|
@ -1,5 +1,5 @@
|
|||||||
from django.db.models import Model
|
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 google.auth.exceptions import GoogleAuthError, TransportError
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
from googleapiclient.errors import Error, HttpError
|
from googleapiclient.errors import Error, HttpError
|
||||||
@ -10,6 +10,7 @@ from authentik.enterprise.providers.google_workspace.models import GoogleWorkspa
|
|||||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
||||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||||
from authentik.lib.sync.outgoing.exceptions import (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
|
BadRequestSyncException,
|
||||||
NotFoundSyncException,
|
NotFoundSyncException,
|
||||||
ObjectExistsSyncException,
|
ObjectExistsSyncException,
|
||||||
StopSync,
|
StopSync,
|
||||||
@ -50,22 +51,24 @@ class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict
|
|||||||
raise StopSync(exc) from exc
|
raise StopSync(exc) from exc
|
||||||
except HttpLib2Error as exc:
|
except HttpLib2Error as exc:
|
||||||
if isinstance(exc, HttpLib2ErrorWithResponse):
|
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
|
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
|
||||||
except HttpError as 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
|
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
|
||||||
except Error as exc:
|
except Error as exc:
|
||||||
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
|
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
|
||||||
return response
|
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:
|
if status_code == HttpResponseNotFound.status_code:
|
||||||
raise NotFoundSyncException("Object not found") from root_exc
|
raise NotFoundSyncException("Object not found") from root_exc
|
||||||
if status_code == HTTP_CONFLICT:
|
if status_code == HTTP_CONFLICT:
|
||||||
raise ObjectExistsSyncException("Object exists") from root_exc
|
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):
|
def check_email_valid(self, *emails: str):
|
||||||
for email in emails:
|
for email in emails:
|
||||||
if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains):
|
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}")
|
||||||
|
@ -9,7 +9,6 @@ from authentik.core.expression.exceptions import (
|
|||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
||||||
from authentik.enterprise.providers.google_workspace.models import (
|
from authentik.enterprise.providers.google_workspace.models import (
|
||||||
GoogleWorkspaceDeleteAction,
|
|
||||||
GoogleWorkspaceProviderGroup,
|
GoogleWorkspaceProviderGroup,
|
||||||
GoogleWorkspaceProviderMapping,
|
GoogleWorkspaceProviderMapping,
|
||||||
GoogleWorkspaceProviderUser,
|
GoogleWorkspaceProviderUser,
|
||||||
@ -22,6 +21,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
StopSync,
|
StopSync,
|
||||||
TransientSyncException,
|
TransientSyncException,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ class GoogleWorkspaceGroupClient(
|
|||||||
connection_type_query = "group"
|
connection_type_query = "group"
|
||||||
can_discover = True
|
can_discover = True
|
||||||
|
|
||||||
def to_schema(self, obj: Group) -> dict:
|
def to_schema(self, obj: Group, creating: bool) -> dict:
|
||||||
"""Convert authentik group"""
|
"""Convert authentik group"""
|
||||||
raw_google_group = {
|
raw_google_group = {
|
||||||
"email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}"
|
"email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}"
|
||||||
@ -45,12 +45,12 @@ class GoogleWorkspaceGroupClient(
|
|||||||
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
mapping: GoogleWorkspaceProviderMapping
|
|
||||||
value = mapping.evaluate(
|
value = mapping.evaluate(
|
||||||
user=None,
|
user=None,
|
||||||
request=None,
|
request=None,
|
||||||
group=obj,
|
group=obj,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
creating=creating,
|
||||||
)
|
)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@ -79,7 +79,7 @@ class GoogleWorkspaceGroupClient(
|
|||||||
self.logger.debug("Group does not exist in Google, skipping")
|
self.logger.debug("Group does not exist in Google, skipping")
|
||||||
return None
|
return None
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if self.provider.group_delete_action == GoogleWorkspaceDeleteAction.DELETE:
|
if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE:
|
||||||
self._request(
|
self._request(
|
||||||
self.directory_service.groups().delete(groupKey=google_group.google_id)
|
self.directory_service.groups().delete(groupKey=google_group.google_id)
|
||||||
)
|
)
|
||||||
@ -87,7 +87,7 @@ class GoogleWorkspaceGroupClient(
|
|||||||
|
|
||||||
def create(self, group: Group):
|
def create(self, group: Group):
|
||||||
"""Create group from scratch and create a connection object"""
|
"""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"])
|
self.check_email_valid(google_group["email"])
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
@ -99,17 +99,17 @@ class GoogleWorkspaceGroupClient(
|
|||||||
group_data = self._request(
|
group_data = self._request(
|
||||||
self.directory_service.groups().get(groupKey=google_group["email"])
|
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"]
|
provider=self.provider, group=group, google_id=group_data["id"]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
GoogleWorkspaceProviderGroup.objects.create(
|
return GoogleWorkspaceProviderGroup.objects.create(
|
||||||
provider=self.provider, group=group, google_id=response["id"]
|
provider=self.provider, group=group, google_id=response["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
|
def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
|
||||||
"""Update existing group"""
|
"""Update existing group"""
|
||||||
google_group = self.to_schema(group)
|
google_group = self.to_schema(group, False)
|
||||||
self.check_email_valid(google_group["email"])
|
self.check_email_valid(google_group["email"])
|
||||||
try:
|
try:
|
||||||
return self._request(
|
return self._request(
|
||||||
@ -124,28 +124,16 @@ class GoogleWorkspaceGroupClient(
|
|||||||
|
|
||||||
def write(self, obj: Group):
|
def write(self, obj: Group):
|
||||||
google_group, created = super().write(obj)
|
google_group, created = super().write(obj)
|
||||||
if created:
|
self.create_sync_members(obj, google_group)
|
||||||
self.create_sync_members(obj, google_group)
|
return google_group, created
|
||||||
return google_group
|
|
||||||
|
|
||||||
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"""
|
"""Sync all members after a group was created"""
|
||||||
users = list(obj.users.order_by("id").values_list("id", flat=True))
|
users = list(obj.users.order_by("id").values_list("id", flat=True))
|
||||||
connections = GoogleWorkspaceProviderUser.objects.filter(
|
connections = GoogleWorkspaceProviderUser.objects.filter(
|
||||||
provider=self.provider, user__pk__in=users
|
provider=self.provider, user__pk__in=users
|
||||||
)
|
).values_list("google_id", flat=True)
|
||||||
for user in connections:
|
self._patch(google_group.google_id, Direction.add, connections)
|
||||||
try:
|
|
||||||
self._request(
|
|
||||||
self.directory_service.members().insert(
|
|
||||||
groupKey=google_group["id"],
|
|
||||||
body={
|
|
||||||
"email": user.google_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except TransientSyncException:
|
|
||||||
continue
|
|
||||||
|
|
||||||
def update_group(self, group: Group, action: Direction, users_set: set[int]):
|
def update_group(self, group: Group, action: Direction, users_set: set[int]):
|
||||||
"""Update a groups members"""
|
"""Update a groups members"""
|
||||||
|
@ -8,7 +8,6 @@ from authentik.core.expression.exceptions import (
|
|||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
||||||
from authentik.enterprise.providers.google_workspace.models import (
|
from authentik.enterprise.providers.google_workspace.models import (
|
||||||
GoogleWorkspaceDeleteAction,
|
|
||||||
GoogleWorkspaceProviderMapping,
|
GoogleWorkspaceProviderMapping,
|
||||||
GoogleWorkspaceProviderUser,
|
GoogleWorkspaceProviderUser,
|
||||||
)
|
)
|
||||||
@ -18,6 +17,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
StopSync,
|
StopSync,
|
||||||
TransientSyncException,
|
TransientSyncException,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
|
|
||||||
@ -29,18 +29,18 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
|||||||
connection_type_query = "user"
|
connection_type_query = "user"
|
||||||
can_discover = True
|
can_discover = True
|
||||||
|
|
||||||
def to_schema(self, obj: User) -> dict:
|
def to_schema(self, obj: User, creating: bool) -> dict:
|
||||||
"""Convert authentik user"""
|
"""Convert authentik user"""
|
||||||
raw_google_user = {}
|
raw_google_user = {}
|
||||||
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
||||||
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
mapping: GoogleWorkspaceProviderMapping
|
|
||||||
value = mapping.evaluate(
|
value = mapping.evaluate(
|
||||||
user=obj,
|
user=obj,
|
||||||
request=None,
|
request=None,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
creating=creating,
|
||||||
)
|
)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@ -71,11 +71,11 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
|||||||
return None
|
return None
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
response = None
|
response = None
|
||||||
if self.provider.user_delete_action == GoogleWorkspaceDeleteAction.DELETE:
|
if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE:
|
||||||
response = self._request(
|
response = self._request(
|
||||||
self.directory_service.users().delete(userKey=google_user.google_id)
|
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(
|
response = self._request(
|
||||||
self.directory_service.users().update(
|
self.directory_service.users().update(
|
||||||
userKey=google_user.google_id, body={"suspended": True}
|
userKey=google_user.google_id, body={"suspended": True}
|
||||||
@ -86,7 +86,7 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
|||||||
|
|
||||||
def create(self, user: User):
|
def create(self, user: User):
|
||||||
"""Create user from scratch and create a connection object"""
|
"""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(
|
self.check_email_valid(
|
||||||
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
|
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
|
||||||
)
|
)
|
||||||
@ -95,19 +95,19 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
|||||||
response = self._request(self.directory_service.users().insert(body=google_user))
|
response = self._request(self.directory_service.users().insert(body=google_user))
|
||||||
except ObjectExistsSyncException:
|
except ObjectExistsSyncException:
|
||||||
# user already exists in google workspace, so we can connect them manually
|
# 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
|
provider=self.provider, user=user, google_id=user.email
|
||||||
)
|
)
|
||||||
except TransientSyncException as exc:
|
except TransientSyncException as exc:
|
||||||
raise exc
|
raise exc
|
||||||
else:
|
else:
|
||||||
GoogleWorkspaceProviderUser.objects.create(
|
return GoogleWorkspaceProviderUser.objects.create(
|
||||||
provider=self.provider, user=user, google_id=response["primaryEmail"]
|
provider=self.provider, user=user, google_id=response["primaryEmail"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(self, user: User, connection: GoogleWorkspaceProviderUser):
|
def update(self, user: User, connection: GoogleWorkspaceProviderUser):
|
||||||
"""Update existing user"""
|
"""Update existing user"""
|
||||||
google_user = self.to_schema(user)
|
google_user = self.to_schema(user, False)
|
||||||
self.check_email_valid(
|
self.check_email_valid(
|
||||||
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
|
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
|
||||||
)
|
)
|
||||||
|
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -16,8 +16,9 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.models import SerializerModel
|
||||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
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]:
|
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):
|
class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||||
"""Sync users from authentik into Google Workspace."""
|
"""Sync users from authentik into Google Workspace."""
|
||||||
|
|
||||||
@ -48,10 +40,10 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
|||||||
default_group_email_domain = models.TextField()
|
default_group_email_domain = models.TextField()
|
||||||
exclude_users_service_account = models.BooleanField(default=False)
|
exclude_users_service_account = models.BooleanField(default=False)
|
||||||
user_delete_action = models.TextField(
|
user_delete_action = models.TextField(
|
||||||
choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE
|
choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE
|
||||||
)
|
)
|
||||||
group_delete_action = models.TextField(
|
group_delete_action = models.TextField(
|
||||||
choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE
|
choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE
|
||||||
)
|
)
|
||||||
|
|
||||||
filter_group = models.ForeignKey(
|
filter_group = models.ForeignKey(
|
||||||
@ -113,10 +105,10 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
|||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.enterprise.providers.google_workspace.api.providers import (
|
from authentik.enterprise.providers.google_workspace.api.providers import (
|
||||||
GoogleProviderSerializer,
|
GoogleWorkspaceProviderSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
return GoogleProviderSerializer
|
return GoogleWorkspaceProviderSerializer
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Google Workspace Provider {self.name}"
|
return f"Google Workspace Provider {self.name}"
|
||||||
@ -136,10 +128,10 @@ class GoogleWorkspaceProviderMapping(PropertyMapping):
|
|||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
|
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
|
||||||
GoogleProviderMappingSerializer,
|
GoogleWorkspaceProviderMappingSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
return GoogleProviderMappingSerializer
|
return GoogleWorkspaceProviderMappingSerializer
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Google Workspace Provider Mapping {self.name}"
|
return f"Google Workspace Provider Mapping {self.name}"
|
||||||
@ -149,7 +141,7 @@ class GoogleWorkspaceProviderMapping(PropertyMapping):
|
|||||||
verbose_name_plural = _("Google Workspace Provider Mappings")
|
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"""
|
"""Mapping of a user and provider to a Google user ID"""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
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)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
provider = models.ForeignKey(GoogleWorkspaceProvider, 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:
|
class Meta:
|
||||||
|
verbose_name = _("Google Workspace Provider User")
|
||||||
|
verbose_name_plural = _("Google Workspace Provider Users")
|
||||||
unique_together = (("google_id", "user", "provider"),)
|
unique_together = (("google_id", "user", "provider"),)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
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"""
|
"""Mapping of a group and provider to a Google group ID"""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
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)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
provider = models.ForeignKey(GoogleWorkspaceProvider, 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:
|
class Meta:
|
||||||
|
verbose_name = _("Google Workspace Provider Group")
|
||||||
|
verbose_name_plural = _("Google Workspace Provider Groups")
|
||||||
unique_together = (("google_id", "group", "provider"),)
|
unique_together = (("google_id", "group", "provider"),)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
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}"
|
||||||
|
@ -2,18 +2,21 @@
|
|||||||
|
|
||||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||||
from authentik.events.system_tasks import SystemTask
|
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.lib.sync.outgoing.tasks import SyncTasks
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
sync_tasks = SyncTasks(GoogleWorkspaceProvider)
|
sync_tasks = SyncTasks(GoogleWorkspaceProvider)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
|
||||||
def google_workspace_sync_objects(*args, **kwargs):
|
def google_workspace_sync_objects(*args, **kwargs):
|
||||||
return sync_tasks.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):
|
def google_workspace_sync(self, provider_pk: int, *args, **kwargs):
|
||||||
"""Run full sync for Google Workspace provider"""
|
"""Run full sync for Google Workspace provider"""
|
||||||
return sync_tasks.sync_single(self, provider_pk, google_workspace_sync_objects)
|
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)
|
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):
|
def google_workspace_sync_direct(*args, **kwargs):
|
||||||
return sync_tasks.sync_signal_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):
|
def google_workspace_sync_m2m(*args, **kwargs):
|
||||||
return sync_tasks.sync_signal_m2m(*args, **kwargs)
|
return sync_tasks.sync_signal_m2m(*args, **kwargs)
|
||||||
|
@ -9,7 +9,6 @@ from authentik.core.models import Application, Group, User
|
|||||||
from authentik.core.tests.utils import create_test_user
|
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.clients.test_http import MockHTTP
|
||||||
from authentik.enterprise.providers.google_workspace.models import (
|
from authentik.enterprise.providers.google_workspace.models import (
|
||||||
GoogleWorkspaceDeleteAction,
|
|
||||||
GoogleWorkspaceProvider,
|
GoogleWorkspaceProvider,
|
||||||
GoogleWorkspaceProviderGroup,
|
GoogleWorkspaceProviderGroup,
|
||||||
GoogleWorkspaceProviderMapping,
|
GoogleWorkspaceProviderMapping,
|
||||||
@ -17,6 +16,7 @@ from authentik.enterprise.providers.google_workspace.models import (
|
|||||||
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
|
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id
|
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.lib.tests.utils import load_fixture
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ class GoogleWorkspaceGroupTests(TestCase):
|
|||||||
|
|
||||||
def test_group_create_delete_do_nothing(self):
|
def test_group_create_delete_do_nothing(self):
|
||||||
"""Test group deletion (delete action = do nothing)"""
|
"""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()
|
self.provider.save()
|
||||||
uid = generate_id()
|
uid = generate_id()
|
||||||
http = MockHTTP()
|
http = MockHTTP()
|
||||||
|
@ -9,7 +9,6 @@ from authentik.blueprints.tests import apply_blueprint
|
|||||||
from authentik.core.models import Application, Group, User
|
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.clients.test_http import MockHTTP
|
||||||
from authentik.enterprise.providers.google_workspace.models import (
|
from authentik.enterprise.providers.google_workspace.models import (
|
||||||
GoogleWorkspaceDeleteAction,
|
|
||||||
GoogleWorkspaceProvider,
|
GoogleWorkspaceProvider,
|
||||||
GoogleWorkspaceProviderMapping,
|
GoogleWorkspaceProviderMapping,
|
||||||
GoogleWorkspaceProviderUser,
|
GoogleWorkspaceProviderUser,
|
||||||
@ -17,6 +16,7 @@ from authentik.enterprise.providers.google_workspace.models import (
|
|||||||
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
|
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id
|
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.lib.tests.utils import load_fixture
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ class GoogleWorkspaceUserTests(TestCase):
|
|||||||
|
|
||||||
def test_user_create_delete_suspend(self):
|
def test_user_create_delete_suspend(self):
|
||||||
"""Test user deletion (delete action = Suspend)"""
|
"""Test user deletion (delete action = Suspend)"""
|
||||||
self.provider.user_delete_action = GoogleWorkspaceDeleteAction.SUSPEND
|
self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND
|
||||||
self.provider.save()
|
self.provider.save()
|
||||||
uid = generate_id()
|
uid = generate_id()
|
||||||
http = MockHTTP()
|
http = MockHTTP()
|
||||||
@ -209,7 +209,7 @@ class GoogleWorkspaceUserTests(TestCase):
|
|||||||
|
|
||||||
def test_user_create_delete_do_nothing(self):
|
def test_user_create_delete_do_nothing(self):
|
||||||
"""Test user deletion (delete action = do nothing)"""
|
"""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()
|
self.provider.save()
|
||||||
uid = generate_id()
|
uid = generate_id()
|
||||||
http = MockHTTP()
|
http = MockHTTP()
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
"""google provider urls"""
|
"""google provider urls"""
|
||||||
|
|
||||||
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
|
from authentik.enterprise.providers.google_workspace.api.groups import (
|
||||||
GoogleProviderMappingViewSet,
|
GoogleWorkspaceProviderGroupViewSet,
|
||||||
|
)
|
||||||
|
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
|
||||||
|
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 = [
|
api_urlpatterns = [
|
||||||
("providers/google_workspace", GoogleProviderViewSet),
|
("providers/google_workspace", GoogleWorkspaceProviderViewSet),
|
||||||
("propertymappings/provider/google_workspace", GoogleProviderMappingViewSet),
|
("providers/google_workspace_users", GoogleWorkspaceProviderUserViewSet),
|
||||||
|
("providers/google_workspace_groups", GoogleWorkspaceProviderGroupViewSet),
|
||||||
|
("propertymappings/provider/google_workspace", GoogleWorkspaceProviderMappingViewSet),
|
||||||
]
|
]
|
||||||
|
33
authentik/enterprise/providers/microsoft_entra/api/groups.py
Normal file
33
authentik/enterprise/providers/microsoft_entra/api/groups.py
Normal file
@ -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"]
|
@ -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"]
|
@ -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
|
33
authentik/enterprise/providers/microsoft_entra/api/users.py
Normal file
33
authentik/enterprise/providers/microsoft_entra/api/users.py
Normal file
@ -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"]
|
9
authentik/enterprise/providers/microsoft_entra/apps.py
Normal file
9
authentik/enterprise/providers/microsoft_entra/apps.py
Normal file
@ -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
|
100
authentik/enterprise/providers/microsoft_entra/clients/base.py
Normal file
100
authentik/enterprise/providers/microsoft_entra/clients/base.py
Normal file
@ -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}")
|
241
authentik/enterprise/providers/microsoft_entra/clients/groups.py
Normal file
241
authentik/enterprise/providers/microsoft_entra/clients/groups.py
Normal file
@ -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,
|
||||||
|
)
|
150
authentik/enterprise/providers/microsoft_entra/clients/users.py
Normal file
150
authentik/enterprise/providers/microsoft_entra/clients/users.py
Normal file
@ -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,
|
||||||
|
)
|
@ -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")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
180
authentik/enterprise/providers/microsoft_entra/models.py
Normal file
180
authentik/enterprise/providers/microsoft_entra/models.py
Normal file
@ -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}"
|
13
authentik/enterprise/providers/microsoft_entra/settings.py
Normal file
13
authentik/enterprise/providers/microsoft_entra/settings.py
Normal file
@ -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"},
|
||||||
|
},
|
||||||
|
}
|
16
authentik/enterprise/providers/microsoft_entra/signals.py
Normal file
16
authentik/enterprise/providers/microsoft_entra/signals.py
Normal file
@ -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,
|
||||||
|
)
|
37
authentik/enterprise/providers/microsoft_entra/tasks.py
Normal file
37
authentik/enterprise/providers/microsoft_entra/tasks.py
Normal file
@ -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)
|
@ -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()
|
@ -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()
|
21
authentik/enterprise/providers/microsoft_entra/urls.py
Normal file
21
authentik/enterprise/providers/microsoft_entra/urls.py
Normal file
@ -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),
|
||||||
|
]
|
@ -15,6 +15,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
TENANT_APPS = [
|
TENANT_APPS = [
|
||||||
"authentik.enterprise.audit",
|
"authentik.enterprise.audit",
|
||||||
"authentik.enterprise.providers.google_workspace",
|
"authentik.enterprise.providers.google_workspace",
|
||||||
|
"authentik.enterprise.providers.microsoft_entra",
|
||||||
"authentik.enterprise.providers.rac",
|
"authentik.enterprise.providers.rac",
|
||||||
"authentik.enterprise.stages.source",
|
"authentik.enterprise.stages.source",
|
||||||
]
|
]
|
||||||
|
@ -101,6 +101,7 @@ def get_logger_config():
|
|||||||
"uvicorn": "WARNING",
|
"uvicorn": "WARNING",
|
||||||
"gunicorn": "INFO",
|
"gunicorn": "INFO",
|
||||||
"requests_mock": "WARNING",
|
"requests_mock": "WARNING",
|
||||||
|
"hpack": "WARNING",
|
||||||
}
|
}
|
||||||
for handler_name, level in handler_level_map.items():
|
for handler_name, level in handler_level_map.items():
|
||||||
base_config["loggers"][handler_name] = {
|
base_config["loggers"][handler_name] = {
|
||||||
|
@ -39,26 +39,25 @@ class BaseOutgoingSyncClient[
|
|||||||
"""Create object in remote destination"""
|
"""Create object in remote destination"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def update(self, obj: TModel, connection: object):
|
def update(self, obj: TModel, connection: TConnection):
|
||||||
"""Update object in remote destination"""
|
"""Update object in remote destination"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def write(self, obj: TModel) -> tuple[TConnection, bool]:
|
def write(self, obj: TModel) -> tuple[TConnection, bool]:
|
||||||
"""Write object to destination. Uses self.create and self.update, but
|
"""Write object to destination. Uses self.create and self.update, but
|
||||||
can be overwritten for further logic"""
|
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}
|
provider=self.provider, **{self.connection_type_query: obj}
|
||||||
).first()
|
).first()
|
||||||
connection: TConnection | None = None
|
|
||||||
try:
|
try:
|
||||||
if not remote_obj:
|
if not connection:
|
||||||
connection = self.create(obj)
|
connection = self.create(obj)
|
||||||
return connection, True
|
return connection, True
|
||||||
try:
|
try:
|
||||||
self.update(obj, remote_obj)
|
self.update(obj, connection)
|
||||||
return remote_obj, False
|
return connection, False
|
||||||
except NotFoundSyncException:
|
except NotFoundSyncException:
|
||||||
remote_obj.delete()
|
connection.delete()
|
||||||
connection = self.create(obj)
|
connection = self.create(obj)
|
||||||
return connection, True
|
return connection, True
|
||||||
except DatabaseError as exc:
|
except DatabaseError as exc:
|
||||||
@ -71,7 +70,7 @@ class BaseOutgoingSyncClient[
|
|||||||
"""Delete object from destination"""
|
"""Delete object from destination"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def to_schema(self, obj: TModel) -> TSchema:
|
def to_schema(self, obj: TModel, creating: bool) -> TSchema:
|
||||||
"""Convert object to destination schema"""
|
"""Convert object to destination schema"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -17,6 +17,10 @@ class ObjectExistsSyncException(BaseSyncException):
|
|||||||
"""Exception when an object already exists in the remote system"""
|
"""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):
|
class StopSync(BaseSyncException):
|
||||||
"""Exception raised when a configuration error should stop the sync process"""
|
"""Exception raised when a configuration error should stop the sync process"""
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from django.core.cache import cache
|
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 redis.lock import Lock
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
@ -9,6 +9,15 @@ from authentik.lib.sync.outgoing import PAGE_TIMEOUT
|
|||||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
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 OutgoingSyncProvider(Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -47,7 +47,7 @@ def register_signals(
|
|||||||
return
|
return
|
||||||
task_sync_direct.delay(
|
task_sync_direct.delay(
|
||||||
class_to_path(instance.__class__), instance.pk, Direction.remove.value
|
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, User, dispatch_uid=uid, weak=False)
|
||||||
pre_delete.connect(model_pre_delete, Group, dispatch_uid=uid, weak=False)
|
pre_delete.connect(model_pre_delete, Group, dispatch_uid=uid, weak=False)
|
||||||
|
@ -14,7 +14,11 @@ from authentik.events.models import TaskStatus
|
|||||||
from authentik.events.system_tasks import SystemTask
|
from authentik.events.system_tasks import SystemTask
|
||||||
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
|
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
|
||||||
from authentik.lib.sync.outgoing.base import Direction
|
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.sync.outgoing.models import OutgoingSyncProvider
|
||||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||||
|
|
||||||
@ -121,6 +125,27 @@ class SyncTasks:
|
|||||||
client.write(obj)
|
client.write(obj)
|
||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
continue
|
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:
|
except TransientSyncException as exc:
|
||||||
self.logger.warning("failed to sync object", exc=exc, user=obj)
|
self.logger.warning("failed to sync object", exc=exc, user=obj)
|
||||||
messages.append(
|
messages.append(
|
||||||
|
@ -34,7 +34,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
|||||||
connection_type = SCIMGroup
|
connection_type = SCIMGroup
|
||||||
connection_type_query = "group"
|
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"""
|
"""Convert authentik user into SCIM"""
|
||||||
raw_scim_group = {
|
raw_scim_group = {
|
||||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
|
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
|
||||||
@ -51,6 +51,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
|||||||
request=None,
|
request=None,
|
||||||
group=obj,
|
group=obj,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
creating=creating,
|
||||||
)
|
)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@ -99,7 +100,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
|||||||
|
|
||||||
def create(self, group: Group):
|
def create(self, group: Group):
|
||||||
"""Create group from scratch and create a connection object"""
|
"""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(
|
response = self._request(
|
||||||
"POST",
|
"POST",
|
||||||
"/Groups",
|
"/Groups",
|
||||||
@ -111,11 +112,11 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
|||||||
scim_id = response.get("id")
|
scim_id = response.get("id")
|
||||||
if not scim_id or scim_id == "":
|
if not scim_id or scim_id == "":
|
||||||
raise StopSync("SCIM Response with missing or invalid `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):
|
def update(self, group: Group, connection: SCIMGroup):
|
||||||
"""Update existing group"""
|
"""Update existing group"""
|
||||||
scim_group = self.to_schema(group)
|
scim_group = self.to_schema(group, False)
|
||||||
scim_group.id = connection.scim_id
|
scim_group.id = connection.scim_id
|
||||||
try:
|
try:
|
||||||
return self._request(
|
return self._request(
|
||||||
|
@ -23,7 +23,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
|||||||
connection_type = SCIMUser
|
connection_type = SCIMUser
|
||||||
connection_type_query = "user"
|
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"""
|
"""Convert authentik user into SCIM"""
|
||||||
raw_scim_user = {
|
raw_scim_user = {
|
||||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
|
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
|
||||||
@ -37,6 +37,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
|||||||
user=obj,
|
user=obj,
|
||||||
request=None,
|
request=None,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
creating=creating,
|
||||||
)
|
)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@ -73,7 +74,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
|||||||
|
|
||||||
def create(self, user: User):
|
def create(self, user: User):
|
||||||
"""Create user from scratch and create a connection object"""
|
"""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(
|
response = self._request(
|
||||||
"POST",
|
"POST",
|
||||||
"/Users",
|
"/Users",
|
||||||
@ -85,11 +86,11 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
|||||||
scim_id = response.get("id")
|
scim_id = response.get("id")
|
||||||
if not scim_id or scim_id == "":
|
if not scim_id or scim_id == "":
|
||||||
raise StopSync("SCIM Response with missing or invalid `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):
|
def update(self, user: User, connection: SCIMUser):
|
||||||
"""Update existing user"""
|
"""Update existing user"""
|
||||||
scim_user = self.to_schema(user)
|
scim_user = self.to_schema(user, False)
|
||||||
scim_user.id = connection.scim_id
|
scim_user.id = connection.scim_id
|
||||||
self._request(
|
self._request(
|
||||||
"PUT",
|
"PUT",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""SCIM Provider tasks"""
|
"""SCIM Provider tasks"""
|
||||||
|
|
||||||
from authentik.events.system_tasks import SystemTask
|
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.lib.sync.outgoing.tasks import SyncTasks
|
||||||
from authentik.providers.scim.models import SCIMProvider
|
from authentik.providers.scim.models import SCIMProvider
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
@ -8,12 +9,14 @@ from authentik.root.celery import CELERY_APP
|
|||||||
sync_tasks = SyncTasks(SCIMProvider)
|
sync_tasks = SyncTasks(SCIMProvider)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
|
||||||
def scim_sync_objects(*args, **kwargs):
|
def scim_sync_objects(*args, **kwargs):
|
||||||
return sync_tasks.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):
|
def scim_sync(self, provider_pk: int, *args, **kwargs):
|
||||||
"""Run full sync for SCIM provider"""
|
"""Run full sync for SCIM provider"""
|
||||||
return sync_tasks.sync_single(self, provider_pk, scim_sync_objects)
|
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)
|
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):
|
def scim_sync_direct(*args, **kwargs):
|
||||||
return sync_tasks.sync_signal_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):
|
def scim_sync_m2m(*args, **kwargs):
|
||||||
return sync_tasks.sync_signal_m2m(*args, **kwargs)
|
return sync_tasks.sync_signal_m2m(*args, **kwargs)
|
||||||
|
@ -127,7 +127,7 @@ class SCIMGroupTests(TestCase):
|
|||||||
"id": scim_id,
|
"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()
|
uid = generate_id()
|
||||||
group = Group.objects.create(
|
group = Group.objects.create(
|
||||||
name=uid,
|
name=uid,
|
||||||
|
@ -230,7 +230,7 @@ class SCIMUserTests(TestCase):
|
|||||||
"id": scim_id,
|
"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()
|
uid = generate_id()
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
username=uid,
|
username=uid,
|
||||||
|
@ -155,9 +155,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
"LDAPAPIAccessMode": "authentik.providers.ldap.models.APIAccessMode",
|
"LDAPAPIAccessMode": "authentik.providers.ldap.models.APIAccessMode",
|
||||||
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
|
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
|
||||||
"UserTypeEnum": "authentik.core.models.UserTypes",
|
"UserTypeEnum": "authentik.core.models.UserTypes",
|
||||||
"GoogleWorkspaceDeleteAction": (
|
"OutgoingSyncDeleteAction": "authentik.lib.sync.outgoing.models.OutgoingSyncDeleteAction",
|
||||||
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceDeleteAction"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||||
"ENUM_GENERATE_CHOICE_DESCRIPTION": False,
|
"ENUM_GENERATE_CHOICE_DESCRIPTION": False,
|
||||||
|
@ -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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -3412,6 +3486,7 @@
|
|||||||
"authentik.enterprise",
|
"authentik.enterprise",
|
||||||
"authentik.enterprise.audit",
|
"authentik.enterprise.audit",
|
||||||
"authentik.enterprise.providers.google_workspace",
|
"authentik.enterprise.providers.google_workspace",
|
||||||
|
"authentik.enterprise.providers.microsoft_entra",
|
||||||
"authentik.enterprise.providers.rac",
|
"authentik.enterprise.providers.rac",
|
||||||
"authentik.enterprise.stages.source",
|
"authentik.enterprise.stages.source",
|
||||||
"authentik.events"
|
"authentik.events"
|
||||||
@ -3495,6 +3570,8 @@
|
|||||||
"authentik_enterprise.license",
|
"authentik_enterprise.license",
|
||||||
"authentik_providers_google_workspace.googleworkspaceprovider",
|
"authentik_providers_google_workspace.googleworkspaceprovider",
|
||||||
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
|
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
|
||||||
|
"authentik_providers_microsoft_entra.microsoftentraprovider",
|
||||||
|
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
|
||||||
"authentik_providers_rac.racprovider",
|
"authentik_providers_rac.racprovider",
|
||||||
"authentik_providers_rac.endpoint",
|
"authentik_providers_rac.endpoint",
|
||||||
"authentik_providers_rac.racpropertymapping",
|
"authentik_providers_rac.racpropertymapping",
|
||||||
@ -8302,6 +8379,102 @@
|
|||||||
},
|
},
|
||||||
"required": []
|
"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": {
|
"model_authentik_providers_rac.racprovider": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2,7 +2,7 @@ version: 1
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
blueprints.goauthentik.io/system: "true"
|
blueprints.goauthentik.io/system: "true"
|
||||||
name: System - Google Provider - Mappings
|
name: System - Google Workspace Provider - Mappings
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/providers/google_workspace/user
|
managed: goauthentik.io/providers/google_workspace/user
|
||||||
|
39
blueprints/system/providers-microsoft-entra.yaml
Normal file
39
blueprints/system/providers-microsoft-entra.yaml
Normal file
@ -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),
|
||||||
|
}
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
|
"POT-Creation-Date: 2024-05-08 00:07+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||||
@ -433,6 +433,27 @@ msgstr "访问此功能需要企业版。"
|
|||||||
msgid "Feature only accessible for internal users."
|
msgid "Feature only accessible for internal users."
|
||||||
msgstr "仅内部用户能访问此功能。"
|
msgstr "仅内部用户能访问此功能。"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
|
||||||
|
msgid "Property mappings used for group creation/updating."
|
||||||
|
msgstr "用于创建/更新组的属性映射。"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider"
|
||||||
|
msgstr "Google Workspace 提供程序"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Providers"
|
||||||
|
msgstr "Google Workspace 提供程序"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider Mapping"
|
||||||
|
msgstr "Google Workspace 提供程序映射"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider Mappings"
|
||||||
|
msgstr "Google Workspace 提供程序映射"
|
||||||
|
|
||||||
#: authentik/enterprise/providers/rac/models.py
|
#: authentik/enterprise/providers/rac/models.py
|
||||||
#: authentik/stages/user_login/models.py
|
#: authentik/stages/user_login/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -778,6 +799,25 @@ msgstr "流程令牌"
|
|||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr "无效的 next URL"
|
msgstr "无效的 next URL"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
msgid "Starting full provider sync"
|
||||||
|
msgstr "开始全量提供程序同步"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-format
|
||||||
|
msgid "Syncing page %(page)d of users"
|
||||||
|
msgstr "正在同步用户页面 %(page)d"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-format
|
||||||
|
msgid "Syncing page %(page)d of groups"
|
||||||
|
msgstr "正在同步群组页面 %(page)d"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Stopping sync due to error: {error}"
|
||||||
|
msgstr "由于以下错误,同步停止:{error}"
|
||||||
|
|
||||||
#: authentik/lib/utils/time.py
|
#: authentik/lib/utils/time.py
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
|
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
|
||||||
@ -1654,10 +1694,6 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
|||||||
msgid "Authentication token"
|
msgid "Authentication token"
|
||||||
msgstr "身份验证令牌"
|
msgstr "身份验证令牌"
|
||||||
|
|
||||||
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
|
|
||||||
msgid "Property mappings used for group creation/updating."
|
|
||||||
msgstr "用于创建/更新组的属性映射。"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/models.py
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr "SCIM 提供程序"
|
msgstr "SCIM 提供程序"
|
||||||
@ -1674,35 +1710,6 @@ msgstr "SCIM 映射"
|
|||||||
msgid "SCIM Mappings"
|
msgid "SCIM Mappings"
|
||||||
msgstr "SCIM 映射"
|
msgstr "SCIM 映射"
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
msgid "Starting full SCIM sync"
|
|
||||||
msgstr "开始全量 SCIM 同步"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-format
|
|
||||||
msgid "Syncing page %(page)d of users"
|
|
||||||
msgstr "正在同步用户页面 %(page)d"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-format
|
|
||||||
msgid "Syncing page %(page)d of groups"
|
|
||||||
msgstr "正在同步群组页面 %(page)d"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to sync user {user_name} due to remote error: {error}"
|
|
||||||
msgstr "由于远端错误,同步用户 {user_name} 失败:{error}"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Stopping sync due to error: {error}"
|
|
||||||
msgstr "由于以下错误,同步停止:{error}"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to sync group {group_name} due to remote error: {error}"
|
|
||||||
msgstr "由于远端错误,同步组 {group_name} 失败:{error}"
|
|
||||||
|
|
||||||
#: authentik/rbac/models.py
|
#: authentik/rbac/models.py
|
||||||
msgid "Role"
|
msgid "Role"
|
||||||
msgstr "角色"
|
msgstr "角色"
|
||||||
|
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
|
"POT-Creation-Date: 2024-05-08 00:07+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -433,6 +433,27 @@ msgstr "访问此功能需要企业版。"
|
|||||||
msgid "Feature only accessible for internal users."
|
msgid "Feature only accessible for internal users."
|
||||||
msgstr "仅内部用户能访问此功能。"
|
msgstr "仅内部用户能访问此功能。"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
|
||||||
|
msgid "Property mappings used for group creation/updating."
|
||||||
|
msgstr "用于创建/更新组的属性映射。"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider"
|
||||||
|
msgstr "Google Workspace 提供程序"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Providers"
|
||||||
|
msgstr "Google Workspace 提供程序"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider Mapping"
|
||||||
|
msgstr "Google Workspace 提供程序映射"
|
||||||
|
|
||||||
|
#: authentik/enterprise/providers/google_workspace/models.py
|
||||||
|
msgid "Google Workspace Provider Mappings"
|
||||||
|
msgstr "Google Workspace 提供程序映射"
|
||||||
|
|
||||||
#: authentik/enterprise/providers/rac/models.py
|
#: authentik/enterprise/providers/rac/models.py
|
||||||
#: authentik/stages/user_login/models.py
|
#: authentik/stages/user_login/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -778,6 +799,25 @@ msgstr "流程令牌"
|
|||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr "无效的 next URL"
|
msgstr "无效的 next URL"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
msgid "Starting full provider sync"
|
||||||
|
msgstr "开始全量提供程序同步"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-format
|
||||||
|
msgid "Syncing page %(page)d of users"
|
||||||
|
msgstr "正在同步用户页面 %(page)d"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-format
|
||||||
|
msgid "Syncing page %(page)d of groups"
|
||||||
|
msgstr "正在同步群组页面 %(page)d"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Stopping sync due to error: {error}"
|
||||||
|
msgstr "由于以下错误,同步停止:{error}"
|
||||||
|
|
||||||
#: authentik/lib/utils/time.py
|
#: authentik/lib/utils/time.py
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
|
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
|
||||||
@ -1654,10 +1694,6 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
|||||||
msgid "Authentication token"
|
msgid "Authentication token"
|
||||||
msgstr "身份验证令牌"
|
msgstr "身份验证令牌"
|
||||||
|
|
||||||
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
|
|
||||||
msgid "Property mappings used for group creation/updating."
|
|
||||||
msgstr "用于创建/更新组的属性映射。"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/models.py
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr "SCIM 提供程序"
|
msgstr "SCIM 提供程序"
|
||||||
@ -1674,35 +1710,6 @@ msgstr "SCIM 映射"
|
|||||||
msgid "SCIM Mappings"
|
msgid "SCIM Mappings"
|
||||||
msgstr "SCIM 映射"
|
msgstr "SCIM 映射"
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
msgid "Starting full SCIM sync"
|
|
||||||
msgstr "开始全量 SCIM 同步"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-format
|
|
||||||
msgid "Syncing page %(page)d of users"
|
|
||||||
msgstr "正在同步用户页面 %(page)d"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-format
|
|
||||||
msgid "Syncing page %(page)d of groups"
|
|
||||||
msgstr "正在同步群组页面 %(page)d"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to sync user {user_name} due to remote error: {error}"
|
|
||||||
msgstr "由于远端错误,同步用户 {user_name} 失败:{error}"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Stopping sync due to error: {error}"
|
|
||||||
msgstr "由于以下错误,同步停止:{error}"
|
|
||||||
|
|
||||||
#: authentik/providers/scim/tasks.py
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to sync group {group_name} due to remote error: {error}"
|
|
||||||
msgstr "由于远端错误,同步组 {group_name} 失败:{error}"
|
|
||||||
|
|
||||||
#: authentik/rbac/models.py
|
#: authentik/rbac/models.py
|
||||||
msgid "Role"
|
msgid "Role"
|
||||||
msgstr "角色"
|
msgstr "角色"
|
||||||
|
594
poetry.lock
generated
594
poetry.lock
generated
@ -315,6 +315,42 @@ six = "*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"]
|
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]]
|
[[package]]
|
||||||
name = "bandit"
|
name = "bandit"
|
||||||
version = "1.7.8"
|
version = "1.7.8"
|
||||||
@ -1121,6 +1157,23 @@ files = [
|
|||||||
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
{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]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.0.6"
|
version = "5.0.6"
|
||||||
@ -1716,6 +1769,53 @@ files = [
|
|||||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
{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]]
|
[[package]]
|
||||||
name = "httplib2"
|
name = "httplib2"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@ -1778,6 +1878,31 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["Cython (>=0.29.24,<0.30.0)"]
|
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]]
|
[[package]]
|
||||||
name = "humanize"
|
name = "humanize"
|
||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
@ -1792,6 +1917,17 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["freezegun", "pytest", "pytest-cov"]
|
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]]
|
[[package]]
|
||||||
name = "hyperlink"
|
name = "hyperlink"
|
||||||
version = "21.0.0"
|
version = "21.0.0"
|
||||||
@ -1819,22 +1955,22 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importlib-metadata"
|
name = "importlib-metadata"
|
||||||
version = "7.1.0"
|
version = "7.0.0"
|
||||||
description = "Read metadata from Python packages"
|
description = "Read metadata from Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
|
{file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"},
|
||||||
{file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
|
{file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
zipp = ">=0.5"
|
zipp = ">=0.5"
|
||||||
|
|
||||||
[package.extras]
|
[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"]
|
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]]
|
[[package]]
|
||||||
name = "incremental"
|
name = "incremental"
|
||||||
@ -2385,6 +2521,154 @@ files = [
|
|||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
{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]]
|
[[package]]
|
||||||
name = "msgpack"
|
name = "msgpack"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
@ -2450,6 +2734,51 @@ files = [
|
|||||||
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
|
{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]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.0.5"
|
version = "6.0.5"
|
||||||
@ -2600,6 +2929,48 @@ files = [
|
|||||||
{file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"},
|
{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]]
|
[[package]]
|
||||||
name = "outcome"
|
name = "outcome"
|
||||||
version = "1.3.0.post0"
|
version = "1.3.0.post0"
|
||||||
@ -2687,6 +3058,105 @@ pygments = ">=2.12.0"
|
|||||||
[package.extras]
|
[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"]
|
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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.2.1"
|
version = "4.2.1"
|
||||||
@ -2718,6 +3188,25 @@ files = [
|
|||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
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]]
|
[[package]]
|
||||||
name = "prometheus-client"
|
name = "prometheus-client"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@ -3007,6 +3496,9 @@ files = [
|
|||||||
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
|
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
crypto = ["cryptography (>=3.4.0)"]
|
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"]
|
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"]
|
dev = ["build", "hatch"]
|
||||||
doc = ["sphinx"]
|
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]]
|
[[package]]
|
||||||
name = "stevedore"
|
name = "stevedore"
|
||||||
version = "5.2.0"
|
version = "5.2.0"
|
||||||
@ -4464,6 +4967,85 @@ files = [
|
|||||||
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
|
{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]]
|
[[package]]
|
||||||
name = "wsproto"
|
name = "wsproto"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -4732,4 +5314,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "~3.12"
|
python-versions = "~3.12"
|
||||||
content-hash = "1ef87ed82de9c403cc569f4858d874e85da73924a610190379f7db3a76458d5b"
|
content-hash = "6399c90a2adca3e7119277bf4d0649fe0826d5fb4454a23b1b1fad3e64a1fe90"
|
||||||
|
@ -76,7 +76,7 @@ show_missing = true
|
|||||||
DJANGO_SETTINGS_MODULE = "authentik.root.settings"
|
DJANGO_SETTINGS_MODULE = "authentik.root.settings"
|
||||||
python_files = ["tests.py", "test_*.py", "*_tests.py"]
|
python_files = ["tests.py", "test_*.py", "*_tests.py"]
|
||||||
junit_family = "xunit2"
|
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 = [
|
filterwarnings = [
|
||||||
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
|
"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",
|
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
|
||||||
@ -118,6 +118,7 @@ jsonpatch = "*"
|
|||||||
kubernetes = "*"
|
kubernetes = "*"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
|
msgraph-sdk = "*"
|
||||||
opencontainers = { extras = ["reggie"], version = "*" }
|
opencontainers = { extras = ["reggie"], version = "*" }
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
|
2275
schema.yml
2275
schema.yml
File diff suppressed because it is too large
Load Diff
8
tests/wdio/package-lock.json
generated
8
tests/wdio/package-lock.json
generated
@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@goauthentik/web-tests",
|
"name": "@goauthentik/web-tests",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromedriver": "^124.0.1"
|
"chromedriver": "^124.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
@ -2084,9 +2084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "124.0.1",
|
"version": "124.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.2.tgz",
|
||||||
"integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==",
|
"integrity": "sha512-TIcIH7tp81qGCWNprQMnmUm15zGUvX/39IiBy3JJMH/UFiimf2lqiWhdQ6LtbnW8BsPFFuEhEfnI733YXg8mxg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testim/chrome-version": "^1.1.4",
|
"@testim/chrome-version": "^1.1.4",
|
||||||
|
@ -32,6 +32,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromedriver": "^124.0.1"
|
"chromedriver": "^124.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
153
web/package-lock.json
generated
153
web/package-lock.json
generated
@ -15,10 +15,9 @@
|
|||||||
"@codemirror/lang-xml": "^6.1.0",
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
"@codemirror/legacy-modes": "^6.4.0",
|
"@codemirror/legacy-modes": "^6.4.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@esbuild/linux-arm64": "^0.21.1",
|
|
||||||
"@formatjs/intl-listformat": "^7.5.5",
|
"@formatjs/intl-listformat": "^7.5.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@goauthentik/api": "^2024.4.2-1715104360",
|
"@goauthentik/api": "^2024.4.2-1715262102",
|
||||||
"@lit-labs/task": "^3.1.0",
|
"@lit-labs/task": "^3.1.0",
|
||||||
"@lit/context": "^1.1.1",
|
"@lit/context": "^1.1.1",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
@ -26,7 +25,7 @@
|
|||||||
"@open-wc/lit-helpers": "^0.7.0",
|
"@open-wc/lit-helpers": "^0.7.0",
|
||||||
"@patternfly/elements": "^3.0.1",
|
"@patternfly/elements": "^3.0.1",
|
||||||
"@patternfly/patternfly": "^4.224.2",
|
"@patternfly/patternfly": "^4.224.2",
|
||||||
"@sentry/browser": "^7.113.0",
|
"@sentry/browser": "^7.114.0",
|
||||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
@ -2984,9 +2983,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2024.4.2-1715104360",
|
"version": "2024.4.2-1715262102",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.4.2-1715104360.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.4.2-1715262102.tgz",
|
||||||
"integrity": "sha512-keGAopWhVKVmWboNw5TIC9SQ5oizrvwX8GryBcFPq+QAsazGxtNrYA6KifHk7ETaXJSY5wtP2837qwTxOqen0Q=="
|
"integrity": "sha512-+BDRla0OnBm0+U5nxb+MGjKAvoI90i2aBe4/WbPB5CXoH6p44A6xX6ECU16DxhTZyteWwcpMuMC8V5PW1Et+SQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@hcaptcha/types": {
|
"node_modules/@hcaptcha/types": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@ -4524,83 +4523,83 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/feedback": {
|
"node_modules/@sentry-internal/feedback": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.114.0.tgz",
|
||||||
"integrity": "sha512-eEmL8QXauUnM3FXGv0GT29RpL0Jo0pkn/uMu3aqjhQo7JKNqUGVYIUxJxiGWbVMbDXqPQ7L66bjjMS3FR1GM2g==",
|
"integrity": "sha512-kUiLRUDZuh10QE9JbSVVLgqxFoD9eDPOzT0MmzlPuas8JlTmJuV4FtSANNcqctd5mBuLt2ebNXH0MhRMwyae4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/replay-canvas": {
|
"node_modules/@sentry-internal/replay-canvas": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.114.0.tgz",
|
||||||
"integrity": "sha512-K8uA42aobNF/BAXf14el15iSAi9fonLBUrjZi6nPDq7zaA8rPvfcTL797hwCbqkETz2zDf52Jz7I3WFCshDoUw==",
|
"integrity": "sha512-6rTiqmKi/FYtesdM2TM2U+rh6BytdPjLP65KTUodtxohJ+r/3m+termj2o4BhIYPE1YYOZNmbZfwebkuQPmWeg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/replay": "7.113.0",
|
"@sentry/replay": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/tracing": {
|
"node_modules/@sentry-internal/tracing": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.114.0.tgz",
|
||||||
"integrity": "sha512-8MDnYENRMnEfQjvN4gkFYFaaBSiMFSU/6SQZfY9pLI3V105z6JQ4D0PGMAUVowXilwNZVpKNYohE7XByuhEC7Q==",
|
"integrity": "sha512-dOuvfJN7G+3YqLlUY4HIjyWHaRP8vbOgF+OsE5w2l7ZEn1rMAaUbPntAR8AF9GBA6j2zWNoSo8e7GjbJxVofSg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/browser": {
|
"node_modules/@sentry/browser": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.114.0.tgz",
|
||||||
"integrity": "sha512-PdyVHPOprwoxGfKGsP2dXDWO0MBDW1eyP7EZlfZvM1A4hjk6ZRNfCv30g+TrqX4hiZDKzyqN3+AdP7N/J2IX0Q==",
|
"integrity": "sha512-ijJ0vOEY6U9JJADVYGkUbLrAbpGSQgA4zV+KW3tcsBLX9M1jaWq4BV1PWHdzDPPDhy4OgfOjIfaMb5BSPn1U+g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/feedback": "7.113.0",
|
"@sentry-internal/feedback": "7.114.0",
|
||||||
"@sentry-internal/replay-canvas": "7.113.0",
|
"@sentry-internal/replay-canvas": "7.114.0",
|
||||||
"@sentry-internal/tracing": "7.113.0",
|
"@sentry-internal/tracing": "7.114.0",
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/integrations": "7.113.0",
|
"@sentry/integrations": "7.114.0",
|
||||||
"@sentry/replay": "7.113.0",
|
"@sentry/replay": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/core": {
|
"node_modules/@sentry/core": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.114.0.tgz",
|
||||||
"integrity": "sha512-pg75y3C5PG2+ur27A0Re37YTCEnX0liiEU7EOxWDGutH17x3ySwlYqLQmZsFZTSnvzv7t3MGsNZ8nT5O0746YA==",
|
"integrity": "sha512-YnanVlmulkjgZiVZ9BfY9k6I082n+C+LbZo52MTvx3FY6RE5iyiPMpaOh67oXEZRWcYQEGm+bKruRxLVP6RlbA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/integrations": {
|
"node_modules/@sentry/integrations": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.114.0.tgz",
|
||||||
"integrity": "sha512-w0sspGBQ+6+V/9bgCkpuM3CGwTYoQEVeTW6iNebFKbtN7MrM3XsGAM9I2cW1jVxFZROqCBPFtd2cs5n0j14aAg==",
|
"integrity": "sha512-BJIBWXGKeIH0ifd7goxOS29fBA8BkEgVVCahs6xIOXBjX1IRS6PmX0zYx/GP23nQTfhJiubv2XPzoYOlZZmDxg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0",
|
"@sentry/utils": "7.114.0",
|
||||||
"localforage": "^1.8.1"
|
"localforage": "^1.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -4608,33 +4607,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/replay": {
|
"node_modules/@sentry/replay": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.114.0.tgz",
|
||||||
"integrity": "sha512-UD2IaphOWKFdeGR+ZiaNAQ+wFsnwbJK6PNwcW6cHmWKv9COlKufpFt06lviaqFZ8jmNrM4H+r+R8YVTrqCuxgg==",
|
"integrity": "sha512-UvEajoLIX9n2poeW3R4Ybz7D0FgCGXoFr/x/33rdUEMIdTypknxjJWxg6fJngIduzwrlrvWpvP8QiZXczYQy2Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/tracing": "7.113.0",
|
"@sentry-internal/tracing": "7.114.0",
|
||||||
"@sentry/core": "7.113.0",
|
"@sentry/core": "7.114.0",
|
||||||
"@sentry/types": "7.113.0",
|
"@sentry/types": "7.114.0",
|
||||||
"@sentry/utils": "7.113.0"
|
"@sentry/utils": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/types": {
|
"node_modules/@sentry/types": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.114.0.tgz",
|
||||||
"integrity": "sha512-PJbTbvkcPu/LuRwwXB1He8m+GjDDLKBtu3lWg5xOZaF5IRdXQU2xwtdXXsjge4PZR00tF7MO7X8ZynTgWbYaew==",
|
"integrity": "sha512-tsqkkyL3eJtptmPtT0m9W/bPLkU7ILY7nvwpi1hahA5jrM7ppoU0IMaQWAgTD+U3rzFH40IdXNBFb8Gnqcva4w==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/utils": {
|
"node_modules/@sentry/utils": {
|
||||||
"version": "7.113.0",
|
"version": "7.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.113.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.114.0.tgz",
|
||||||
"integrity": "sha512-nzKsErwmze1mmEsbW2AwL2oB+I5v6cDEJY4sdfLekA4qZbYZ8pV5iWza6IRl4XfzGTE1qpkZmEjPU9eyo0yvYw==",
|
"integrity": "sha512-319N90McVpupQ6vws4+tfCy/03AdtsU0MurIE4+W5cubHME08HtiEWlfacvAxX+yuKFhvdsO4K4BB/dj54ideg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/types": "7.113.0"
|
"@sentry/types": "7.114.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -12072,38 +12071,6 @@
|
|||||||
"esbuild": ">=0.12 <1"
|
"esbuild": ">=0.12 <1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild/node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.0.tgz",
|
|
||||||
"integrity": "sha512-XMcLA6siz67AoEOl8WOot2Y3TOSClT15AqJdQz/sx98Dpv3oTbcv0BoqvHAhpBPgC8iyIKM98vVj6th7lA4DFg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild/node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.0.tgz",
|
|
||||||
"integrity": "sha512-mgOuJBbV8Uexb3BmeVl1q2preJMu0aDiwiFxIfsQhE2+rqxVAEcIrllb7SulkH9G244O/ZN1VVILdZb2NPSvpw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.5",
|
"@formatjs/intl-listformat": "^7.5.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@goauthentik/api": "^2024.4.2-1715104360",
|
"@goauthentik/api": "^2024.4.2-1715262102",
|
||||||
"@lit-labs/task": "^3.1.0",
|
"@lit-labs/task": "^3.1.0",
|
||||||
"@lit/context": "^1.1.1",
|
"@lit/context": "^1.1.1",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"@open-wc/lit-helpers": "^0.7.0",
|
"@open-wc/lit-helpers": "^0.7.0",
|
||||||
"@patternfly/elements": "^3.0.1",
|
"@patternfly/elements": "^3.0.1",
|
||||||
"@patternfly/patternfly": "^4.224.2",
|
"@patternfly/patternfly": "^4.224.2",
|
||||||
"@sentry/browser": "^7.113.0",
|
"@sentry/browser": "^7.114.0",
|
||||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
|
@ -10,11 +10,11 @@ import { TemplateResult, html } from "lit";
|
|||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.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")
|
@customElement("ak-property-mapping-google-workspace-form")
|
||||||
export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<GoogleProviderMapping> {
|
export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<GoogleWorkspaceProviderMapping> {
|
||||||
loadInstance(pk: string): Promise<GoogleProviderMapping> {
|
loadInstance(pk: string): Promise<GoogleWorkspaceProviderMapping> {
|
||||||
return new PropertymappingsApi(
|
return new PropertymappingsApi(
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
).propertymappingsProviderGoogleWorkspaceRetrieve({
|
).propertymappingsProviderGoogleWorkspaceRetrieve({
|
||||||
@ -22,19 +22,19 @@ export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(data: GoogleProviderMapping): Promise<GoogleProviderMapping> {
|
async send(data: GoogleWorkspaceProviderMapping): Promise<GoogleWorkspaceProviderMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(
|
return new PropertymappingsApi(
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
).propertymappingsProviderGoogleWorkspaceUpdate({
|
).propertymappingsProviderGoogleWorkspaceUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
googleProviderMappingRequest: data,
|
googleWorkspaceProviderMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new PropertymappingsApi(
|
return new PropertymappingsApi(
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
).propertymappingsProviderGoogleWorkspaceCreate({
|
).propertymappingsProviderGoogleWorkspaceCreate({
|
||||||
googleProviderMappingRequest: data,
|
googleWorkspaceProviderMappingRequest: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm<LDAPPropert
|
|||||||
async send(data: LDAPPropertyMapping): Promise<LDAPPropertyMapping> {
|
async send(data: LDAPPropertyMapping): Promise<LDAPPropertyMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
lDAPPropertyMappingRequest: data,
|
lDAPPropertyMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "@goauthentik/admin/property-mappings/PropertyMappingGoogleWorkspaceForm";
|
import "@goauthentik/admin/property-mappings/PropertyMappingGoogleWorkspaceForm";
|
||||||
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
|
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
|
||||||
|
import "@goauthentik/admin/property-mappings/PropertyMappingMicrosoftEntraForm";
|
||||||
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
||||||
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||||
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
||||||
|
@ -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<MicrosoftEntraProviderMapping> {
|
||||||
|
loadInstance(pk: string): Promise<MicrosoftEntraProviderMapping> {
|
||||||
|
return new PropertymappingsApi(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
).propertymappingsProviderGoogleWorkspaceRetrieve({
|
||||||
|
pmUuid: pk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(data: MicrosoftEntraProviderMapping): Promise<MicrosoftEntraProviderMapping> {
|
||||||
|
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` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${ifDefined(this.instance?.name)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Expression")}
|
||||||
|
?required=${true}
|
||||||
|
name="expression"
|
||||||
|
>
|
||||||
|
<ak-codemirror
|
||||||
|
mode=${CodeMirrorMode.Python}
|
||||||
|
value="${ifDefined(this.instance?.expression)}"
|
||||||
|
>
|
||||||
|
</ak-codemirror>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Expression using Python.")}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
|
>
|
||||||
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ export class PropertyMappingNotification extends ModelForm<NotificationWebhookMa
|
|||||||
async send(data: NotificationWebhookMapping): Promise<NotificationWebhookMapping> {
|
async send(data: NotificationWebhookMapping): Promise<NotificationWebhookMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
notificationWebhookMappingRequest: data,
|
notificationWebhookMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,7 +51,7 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
|
|||||||
async send(data: RACPropertyMapping): Promise<RACPropertyMapping> {
|
async send(data: RACPropertyMapping): Promise<RACPropertyMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
rACPropertyMappingRequest: data,
|
rACPropertyMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,7 +23,7 @@ export class PropertyMappingSAMLForm extends BasePropertyMappingForm<SAMLPropert
|
|||||||
async send(data: SAMLPropertyMapping): Promise<SAMLPropertyMapping> {
|
async send(data: SAMLPropertyMapping): Promise<SAMLPropertyMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
sAMLPropertyMappingRequest: data,
|
sAMLPropertyMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,7 +23,7 @@ export class PropertyMappingSCIMForm extends BasePropertyMappingForm<SCIMMapping
|
|||||||
async send(data: SCIMMapping): Promise<SCIMMapping> {
|
async send(data: SCIMMapping): Promise<SCIMMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScimUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
sCIMMappingRequest: data,
|
sCIMMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,7 +23,7 @@ export class PropertyMappingScopeForm extends BasePropertyMappingForm<ScopeMappi
|
|||||||
async send(data: ScopeMapping): Promise<ScopeMapping> {
|
async send(data: ScopeMapping): Promise<ScopeMapping> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeUpdate({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeUpdate({
|
||||||
pmUuid: this.instance.pk || "",
|
pmUuid: this.instance.pk,
|
||||||
scopeMappingRequest: data,
|
scopeMappingRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import "@goauthentik/admin/applications/ApplicationWizardHint";
|
import "@goauthentik/admin/applications/ApplicationWizardHint";
|
||||||
import "@goauthentik/admin/providers/ProviderWizard";
|
import "@goauthentik/admin/providers/ProviderWizard";
|
||||||
|
import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm";
|
||||||
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
|
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
|
||||||
|
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||||
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
|
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
|
||||||
import "@goauthentik/admin/providers/rac/RACProviderForm";
|
import "@goauthentik/admin/providers/rac/RACProviderForm";
|
||||||
import "@goauthentik/admin/providers/radius/RadiusProviderForm";
|
import "@goauthentik/admin/providers/radius/RadiusProviderForm";
|
||||||
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
|
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
|
||||||
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
|
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
|
||||||
import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm";
|
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage";
|
import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage";
|
||||||
|
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage";
|
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage";
|
import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/rac/RACProviderViewPage";
|
import "@goauthentik/admin/providers/rac/RACProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/radius/RadiusProviderViewPage";
|
import "@goauthentik/admin/providers/radius/RadiusProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/saml/SAMLProviderViewPage";
|
import "@goauthentik/admin/providers/saml/SAMLProviderViewPage";
|
||||||
import "@goauthentik/admin/providers/scim/SCIMProviderViewPage";
|
import "@goauthentik/admin/providers/scim/SCIMProviderViewPage";
|
||||||
import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage";
|
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
@ -75,6 +76,10 @@ export class ProviderViewPage extends AKElement {
|
|||||||
return html`<ak-provider-google-workspace-view
|
return html`<ak-provider-google-workspace-view
|
||||||
providerID=${ifDefined(this.provider.pk)}
|
providerID=${ifDefined(this.provider.pk)}
|
||||||
></ak-provider-google-workspace-view>`;
|
></ak-provider-google-workspace-view>`;
|
||||||
|
case "ak-provider-microsoft-entra-form":
|
||||||
|
return html`<ak-provider-microsoft-entra-view
|
||||||
|
providerID=${ifDefined(this.provider.pk)}
|
||||||
|
></ak-provider-microsoft-entra-view>`;
|
||||||
default:
|
default:
|
||||||
return html`<p>Invalid provider type ${this.provider?.component}</p>`;
|
return html`<p>Invalid provider type ${this.provider?.component}</p>`;
|
||||||
}
|
}
|
||||||
|
@ -16,17 +16,17 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
import {
|
import {
|
||||||
CoreApi,
|
CoreApi,
|
||||||
CoreGroupsListRequest,
|
CoreGroupsListRequest,
|
||||||
GoogleProvider,
|
GoogleWorkspaceProvider,
|
||||||
GoogleWorkspaceDeleteAction,
|
|
||||||
Group,
|
Group,
|
||||||
PaginatedGoogleProviderMappingList,
|
OutgoingSyncDeleteAction,
|
||||||
|
PaginatedGoogleWorkspaceProviderMappingList,
|
||||||
PropertymappingsApi,
|
PropertymappingsApi,
|
||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-provider-google-workspace-form")
|
@customElement("ak-provider-google-workspace-form")
|
||||||
export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProvider> {
|
export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWorkspaceProvider> {
|
||||||
loadInstance(pk: number): Promise<GoogleProvider> {
|
loadInstance(pk: number): Promise<GoogleWorkspaceProvider> {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceRetrieve({
|
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceRetrieve({
|
||||||
id: pk,
|
id: pk,
|
||||||
});
|
});
|
||||||
@ -40,17 +40,17 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProv
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
propertyMappings?: PaginatedGoogleProviderMappingList;
|
propertyMappings?: PaginatedGoogleWorkspaceProviderMappingList;
|
||||||
|
|
||||||
async send(data: GoogleProvider): Promise<GoogleProvider> {
|
async send(data: GoogleWorkspaceProvider): Promise<GoogleWorkspaceProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
googleProviderRequest: data,
|
googleWorkspaceProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceCreate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceCreate({
|
||||||
googleProviderRequest: data,
|
googleWorkspaceProviderRequest: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +76,9 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProv
|
|||||||
mode=${CodeMirrorMode.JavaScript}
|
mode=${CodeMirrorMode.JavaScript}
|
||||||
.value="${first(this.instance?.credentials, {})}"
|
.value="${first(this.instance?.credentials, {})}"
|
||||||
></ak-codemirror>
|
></ak-codemirror>
|
||||||
<p class="pf-c-form__helper-text">${msg("TODO")}</p>
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Google Cloud credentials file.")}
|
||||||
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Delegated Subject")}
|
label=${msg("Delegated Subject")}
|
||||||
@ -84,12 +86,16 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProv
|
|||||||
name="delegatedSubject"
|
name="delegatedSubject"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
value="${first(this.instance?.delegatedSubject, "")}"
|
value="${first(this.instance?.delegatedSubject, "")}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">${msg("TODO")}</p>
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"Email address of the user the actions of authentik will be delegated to.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Default group email domain")}
|
label=${msg("Default group email domain")}
|
||||||
@ -115,20 +121,20 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProv
|
|||||||
.options=${[
|
.options=${[
|
||||||
{
|
{
|
||||||
label: msg("Delete"),
|
label: msg("Delete"),
|
||||||
value: GoogleWorkspaceDeleteAction.Delete,
|
value: OutgoingSyncDeleteAction.Delete,
|
||||||
default: true,
|
default: true,
|
||||||
description: html`${msg("User is deleted")}`,
|
description: html`${msg("User is deleted")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: msg("Suspend"),
|
label: msg("Suspend"),
|
||||||
value: GoogleWorkspaceDeleteAction.Suspend,
|
value: OutgoingSyncDeleteAction.Suspend,
|
||||||
description: html`${msg(
|
description: html`${msg(
|
||||||
"User is suspended, and connection to user in authentik is removed.",
|
"User is suspended, and connection to user in authentik is removed.",
|
||||||
)}`,
|
)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: msg("Do Nothing"),
|
label: msg("Do Nothing"),
|
||||||
value: GoogleWorkspaceDeleteAction.DoNothing,
|
value: OutgoingSyncDeleteAction.DoNothing,
|
||||||
description: html`${msg(
|
description: html`${msg(
|
||||||
"The connection is removed but the user is not modified",
|
"The connection is removed but the user is not modified",
|
||||||
)}`,
|
)}`,
|
||||||
@ -145,13 +151,13 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProv
|
|||||||
.options=${[
|
.options=${[
|
||||||
{
|
{
|
||||||
label: msg("Delete"),
|
label: msg("Delete"),
|
||||||
value: GoogleWorkspaceDeleteAction.Delete,
|
value: OutgoingSyncDeleteAction.Delete,
|
||||||
default: true,
|
default: true,
|
||||||
description: html`${msg("Group is deleted")}`,
|
description: html`${msg("Group is deleted")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: msg("Do Nothing"),
|
label: msg("Do Nothing"),
|
||||||
value: GoogleWorkspaceDeleteAction.DoNothing,
|
value: OutgoingSyncDeleteAction.DoNothing,
|
||||||
description: html`${msg(
|
description: html`${msg(
|
||||||
"The connection is removed but the group is not modified",
|
"The connection is removed but the group is not modified",
|
||||||
)}`,
|
)}`,
|
||||||
|
@ -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 { GoogleWorkspaceProviderGroup, ProvidersApi } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-provider-google-workspace-groups-list")
|
||||||
|
export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProviderGroup> {
|
||||||
|
@property({ type: Number })
|
||||||
|
providerId?: number;
|
||||||
|
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderGroup>> {
|
||||||
|
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`<a href="#/identity/groups/${item.groupObj.pk}">
|
||||||
|
<div>${item.groupObj.name}</div>
|
||||||
|
</a>`,
|
||||||
|
html`${item.id}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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<GoogleWorkspaceProviderUser> {
|
||||||
|
@property({ type: Number })
|
||||||
|
providerId?: number;
|
||||||
|
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderUser>> {
|
||||||
|
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`<a href="#/identity/users/${item.userObj.pk}">
|
||||||
|
<div>${item.userObj.username}</div>
|
||||||
|
<small>${item.userObj.name}</small>
|
||||||
|
</a>`,
|
||||||
|
html`${item.id}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Markdown";
|
import "@goauthentik/elements/Markdown";
|
||||||
|
import "@goauthentik/elements/SyncStatusCard";
|
||||||
import "@goauthentik/elements/Tabs";
|
import "@goauthentik/elements/Tabs";
|
||||||
import "@goauthentik/elements/buttons/ActionButton";
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
import "@goauthentik/elements/events/LogViewer";
|
|
||||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GoogleProvider,
|
GoogleWorkspaceProvider,
|
||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
RbacPermissionsAssignedByUsersListModelEnum,
|
RbacPermissionsAssignedByUsersListModelEnum,
|
||||||
SyncStatus,
|
SyncStatus,
|
||||||
SystemTaskStatusEnum,
|
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-provider-google-workspace-view")
|
@customElement("ak-provider-google-workspace-view")
|
||||||
@ -41,7 +42,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
|||||||
providerID?: number;
|
providerID?: number;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
provider?: GoogleProvider;
|
provider?: GoogleWorkspaceProvider;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
syncState?: SyncStatus;
|
syncState?: SyncStatus;
|
||||||
@ -121,6 +122,28 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-users"
|
||||||
|
data-tab-title="${msg("Provisioned Users")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
|
<ak-provider-google-workspace-users-list
|
||||||
|
providerId=${this.provider.pk}
|
||||||
|
></ak-provider-google-workspace-users-list>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-groups"
|
||||||
|
data-tab-title="${msg("Provisioned Groups")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
|
<ak-provider-google-workspace-groups-list
|
||||||
|
providerId=${this.provider.pk}
|
||||||
|
></ak-provider-google-workspace-groups-list>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<ak-rbac-object-permission-page
|
<ak-rbac-object-permission-page
|
||||||
slot="page-permissions"
|
slot="page-permissions"
|
||||||
data-tab-title="${msg("Permissions")}"
|
data-tab-title="${msg("Permissions")}"
|
||||||
@ -130,39 +153,6 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
|||||||
</ak-tabs>`;
|
</ak-tabs>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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`
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
${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`<li>
|
|
||||||
<p>${task.name}</p>
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
<li>${header}</li>
|
|
||||||
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
|
||||||
</ul>
|
|
||||||
</li> `;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTabOverview(): TemplateResult {
|
renderTabOverview(): TemplateResult {
|
||||||
if (!this.provider) {
|
if (!this.provider) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -197,7 +187,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
|||||||
<div class="pf-c-card__footer">
|
<div class="pf-c-card__footer">
|
||||||
<ak-forms-modal>
|
<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg("Update Google Provider")} </span>
|
<span slot="header"> ${msg("Update Google Workspace Provider")} </span>
|
||||||
<ak-provider-google-workspace-form
|
<ak-provider-google-workspace-form
|
||||||
slot="form"
|
slot="form"
|
||||||
.instancePk=${this.provider.pk}
|
.instancePk=${this.provider.pk}
|
||||||
@ -209,33 +199,24 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
||||||
<div class="pf-c-card__title">
|
<ak-sync-status-card
|
||||||
<p>${msg("Sync status")}</p>
|
.fetch=${() => {
|
||||||
</div>
|
return new ProvidersApi(
|
||||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
DEFAULT_CONFIG,
|
||||||
<div class="pf-c-card__footer">
|
).providersGoogleWorkspaceSyncStatusRetrieve({
|
||||||
<ak-action-button
|
id: this.provider?.pk || 0,
|
||||||
class="pf-m-secondary"
|
});
|
||||||
.apiRequest=${() => {
|
}}
|
||||||
return new ProvidersApi(DEFAULT_CONFIG)
|
.triggerSync=${() => {
|
||||||
.providersGoogleWorkspacePartialUpdate({
|
return new ProvidersApi(
|
||||||
id: this.provider?.pk || 0,
|
DEFAULT_CONFIG,
|
||||||
patchedGoogleProviderRequest: this.provider,
|
).providersGoogleWorkspacePartialUpdate({
|
||||||
})
|
id: this.provider?.pk || 0,
|
||||||
.then(() => {
|
patchedGoogleWorkspaceProviderRequest: {},
|
||||||
this.dispatchEvent(
|
});
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
}}
|
||||||
bubbles: true,
|
></ak-sync-status-card>
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Run sync again")}
|
|
||||||
</ak-action-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
|
|||||||
async send(data: LDAPProvider): Promise<LDAPProvider> {
|
async send(data: LDAPProvider): Promise<LDAPProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
lDAPProviderRequest: data,
|
lDAPProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -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<MicrosoftEntraProvider> {
|
||||||
|
loadInstance(pk: number): Promise<MicrosoftEntraProvider> {
|
||||||
|
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraRetrieve({
|
||||||
|
id: pk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<void> {
|
||||||
|
this.propertyMappings = await new PropertymappingsApi(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
).propertymappingsProviderMicrosoftEntraList({
|
||||||
|
ordering: "managed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyMappings?: PaginatedMicrosoftEntraProviderMappingList;
|
||||||
|
|
||||||
|
async send(data: MicrosoftEntraProvider): Promise<MicrosoftEntraProvider> {
|
||||||
|
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` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${ifDefined(this.instance?.name)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-group .expanded=${true}>
|
||||||
|
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Client ID")}
|
||||||
|
?required=${true}
|
||||||
|
name="clientId"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.clientId, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Client ID for the app registration.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Client Secret")}
|
||||||
|
?required=${true}
|
||||||
|
name="clientSecret"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.clientSecret, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Client secret for the app registration.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Tenant ID")}
|
||||||
|
?required=${true}
|
||||||
|
name="tenantId"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.tenantId, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("ID of the tenant accounts will be synced into.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-radio-input
|
||||||
|
name="userDeleteAction"
|
||||||
|
label=${msg("User deletion action")}
|
||||||
|
required
|
||||||
|
.options=${[
|
||||||
|
{
|
||||||
|
label: msg("Delete"),
|
||||||
|
value: OutgoingSyncDeleteAction.Delete,
|
||||||
|
default: true,
|
||||||
|
description: html`${msg("User is deleted")}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Do Nothing"),
|
||||||
|
value: OutgoingSyncDeleteAction.DoNothing,
|
||||||
|
description: html`${msg(
|
||||||
|
"The connection is removed but the user is not modified",
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
.value=${this.instance?.userDeleteAction}
|
||||||
|
help=${msg("Determines what authentik will do when a User is deleted.")}
|
||||||
|
>
|
||||||
|
</ak-radio-input>
|
||||||
|
<ak-radio-input
|
||||||
|
name="groupDeleteAction"
|
||||||
|
label=${msg("Group deletion action")}
|
||||||
|
required
|
||||||
|
.options=${[
|
||||||
|
{
|
||||||
|
label: msg("Delete"),
|
||||||
|
value: OutgoingSyncDeleteAction.Delete,
|
||||||
|
default: true,
|
||||||
|
description: html`${msg("Group is deleted")}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Do Nothing"),
|
||||||
|
value: OutgoingSyncDeleteAction.DoNothing,
|
||||||
|
description: html`${msg(
|
||||||
|
"The connection is removed but the group is not modified",
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
.value=${this.instance?.groupDeleteAction}
|
||||||
|
help=${msg("Determines what authentik will do when a Group is deleted.")}
|
||||||
|
>
|
||||||
|
</ak-radio-input>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
<ak-form-group ?expanded=${true}>
|
||||||
|
<span slot="header">${msg("User filtering")}</span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal name="excludeUsersServiceAccount">
|
||||||
|
<label class="pf-c-switch">
|
||||||
|
<input
|
||||||
|
class="pf-c-switch__input"
|
||||||
|
type="checkbox"
|
||||||
|
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
|
||||||
|
/>
|
||||||
|
<span class="pf-c-switch__toggle">
|
||||||
|
<span class="pf-c-switch__toggle-icon">
|
||||||
|
<i class="fas fa-check" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="pf-c-switch__label"
|
||||||
|
>${msg("Exclude service accounts")}</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||||
|
<ak-search-select
|
||||||
|
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
</ak-search-select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Only sync users within the selected group.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
<ak-form-group ?expanded=${true}>
|
||||||
|
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("User Property Mappings")}
|
||||||
|
name="propertyMappings"
|
||||||
|
>
|
||||||
|
<select class="pf-c-form-control" multiple>
|
||||||
|
${this.propertyMappings?.results.map((mapping) => {
|
||||||
|
let selected = false;
|
||||||
|
if (!this.instance?.propertyMappings) {
|
||||||
|
selected =
|
||||||
|
mapping.managed ===
|
||||||
|
"goauthentik.io/providers/microsoft_entra/user" ||
|
||||||
|
false;
|
||||||
|
} else {
|
||||||
|
selected = Array.from(this.instance?.propertyMappings).some(
|
||||||
|
(su) => {
|
||||||
|
return su == mapping.pk;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return html`<option
|
||||||
|
value=${ifDefined(mapping.pk)}
|
||||||
|
?selected=${selected}
|
||||||
|
>
|
||||||
|
${mapping.name}
|
||||||
|
</option>`;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Property mappings used to user mapping.")}
|
||||||
|
</p>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Hold control/command to select multiple items.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Group Property Mappings")}
|
||||||
|
name="propertyMappingsGroup"
|
||||||
|
>
|
||||||
|
<select class="pf-c-form-control" multiple>
|
||||||
|
${this.propertyMappings?.results.map((mapping) => {
|
||||||
|
let selected = false;
|
||||||
|
if (!this.instance?.propertyMappingsGroup) {
|
||||||
|
selected =
|
||||||
|
mapping.managed ===
|
||||||
|
"goauthentik.io/providers/microsoft_entra/group";
|
||||||
|
} else {
|
||||||
|
selected = Array.from(
|
||||||
|
this.instance?.propertyMappingsGroup,
|
||||||
|
).some((su) => {
|
||||||
|
return su == mapping.pk;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return html`<option
|
||||||
|
value=${ifDefined(mapping.pk)}
|
||||||
|
?selected=${selected}
|
||||||
|
>
|
||||||
|
${mapping.name}
|
||||||
|
</option>`;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Property mappings used to group creation.")}
|
||||||
|
</p>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Hold control/command to select multiple items.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>`;
|
||||||
|
}
|
||||||
|
}
|
@ -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<MicrosoftEntraProviderGroup> {
|
||||||
|
@property({ type: Number })
|
||||||
|
providerId?: number;
|
||||||
|
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<MicrosoftEntraProviderGroup>> {
|
||||||
|
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`<a href="#/identity/groups/${item.groupObj.pk}">
|
||||||
|
<div>${item.groupObj.name}</div>
|
||||||
|
</a>`,
|
||||||
|
html`${item.id}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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<MicrosoftEntraProviderUser> {
|
||||||
|
@property({ type: Number })
|
||||||
|
providerId?: number;
|
||||||
|
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<MicrosoftEntraProviderUser>> {
|
||||||
|
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`<a href="#/identity/users/${item.userObj.pk}">
|
||||||
|
<div>${item.userObj.username}</div>
|
||||||
|
<small>${item.userObj.name}</small>
|
||||||
|
</a>`,
|
||||||
|
html`${item.id}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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<this>) {
|
||||||
|
if (changedProperties.has("providerID") && this.providerID) {
|
||||||
|
this.fetchProvider(this.providerID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
if (!this.provider) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html` <ak-tabs>
|
||||||
|
<section
|
||||||
|
slot="page-overview"
|
||||||
|
data-tab-title="${msg("Overview")}"
|
||||||
|
@activate=${() => {
|
||||||
|
new ProvidersApi(DEFAULT_CONFIG)
|
||||||
|
.providersMicrosoftEntraSyncStatusRetrieve({
|
||||||
|
id: this.provider?.pk || 0,
|
||||||
|
})
|
||||||
|
.then((state) => {
|
||||||
|
this.syncState = state;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.syncState = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${this.renderTabOverview()}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-changelog"
|
||||||
|
data-tab-title="${msg("Changelog")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-object-changelog
|
||||||
|
targetModelPk=${this.provider?.pk || ""}
|
||||||
|
targetModelName=${this.provider?.metaModelName || ""}
|
||||||
|
>
|
||||||
|
</ak-object-changelog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-users"
|
||||||
|
data-tab-title="${msg("Provisioned Users")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
|
<ak-provider-microsoft-entra-users-list
|
||||||
|
providerId=${this.provider.pk}
|
||||||
|
></ak-provider-microsoft-entra-users-list>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-groups"
|
||||||
|
data-tab-title="${msg("Provisioned Groups")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
|
<ak-provider-microsoft-entra-groups-list
|
||||||
|
providerId=${this.provider.pk}
|
||||||
|
></ak-provider-microsoft-entra-groups-list>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<ak-rbac-object-permission-page
|
||||||
|
slot="page-permissions"
|
||||||
|
data-tab-title="${msg("Permissions")}"
|
||||||
|
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersMicrosoftEntraMicrosoftentraprovider}
|
||||||
|
objectPk=${this.provider.pk}
|
||||||
|
></ak-rbac-object-permission-page>
|
||||||
|
</ak-tabs>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTabOverview(): TemplateResult {
|
||||||
|
if (!this.provider) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`<div slot="header" class="pf-c-banner pf-m-info">
|
||||||
|
${msg("Microsoft Entra Provider is in preview.")}
|
||||||
|
<a href="mailto:hello+feature/mse@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||||
|
</div>
|
||||||
|
${!this.provider?.assignedBackchannelApplicationName
|
||||||
|
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||||
|
${msg(
|
||||||
|
"Warning: Provider is not assigned to an application as backchannel provider.",
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
|
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
|
||||||
|
<div class="pf-c-card pf-m-12-col pf-l-stack__item">
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<dl class="pf-c-description-list pf-m-3-col-on-lg">
|
||||||
|
<div class="pf-c-description-list__group">
|
||||||
|
<dt class="pf-c-description-list__term">
|
||||||
|
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="pf-c-description-list__description">
|
||||||
|
<div class="pf-c-description-list__text">
|
||||||
|
${this.provider.name}
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card__footer">
|
||||||
|
<ak-forms-modal>
|
||||||
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
|
<span slot="header"> ${msg("Update Microsoft Entra Provider")} </span>
|
||||||
|
<ak-provider-microsoft-entra-form
|
||||||
|
slot="form"
|
||||||
|
.instancePk=${this.provider.pk}
|
||||||
|
>
|
||||||
|
</ak-provider-microsoft-entra-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${msg("Edit")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
||||||
|
<ak-sync-status-card
|
||||||
|
.fetch=${() => {
|
||||||
|
return new ProvidersApi(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
).providersMicrosoftEntraSyncStatusRetrieve({
|
||||||
|
id: this.provider?.pk || 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
.triggerSync=${() => {
|
||||||
|
return new ProvidersApi(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
).providersMicrosoftEntraPartialUpdate({
|
||||||
|
id: this.provider?.pk || 0,
|
||||||
|
patchedMicrosoftEntraProviderRequest: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></ak-sync-status-card>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
@ -146,7 +146,7 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
async send(data: OAuth2Provider): Promise<OAuth2Provider> {
|
async send(data: OAuth2Provider): Promise<OAuth2Provider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
|
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
oAuth2ProviderRequest: data,
|
oAuth2ProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -72,7 +72,7 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
|
|||||||
}
|
}
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
proxyProviderRequest: data,
|
proxyProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -54,7 +54,7 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
|
|||||||
async send(data: RACProvider): Promise<RACProvider> {
|
async send(data: RACProvider): Promise<RACProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersRacUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersRacUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
rACProviderRequest: data,
|
rACProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -24,7 +24,7 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<Rad
|
|||||||
async send(data: RadiusProvider): Promise<RadiusProvider> {
|
async send(data: RadiusProvider): Promise<RadiusProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersRadiusUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersRadiusUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
radiusProviderRequest: data,
|
radiusProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +47,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
|||||||
async send(data: SAMLProvider): Promise<SAMLProvider> {
|
async send(data: SAMLProvider): Promise<SAMLProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersSamlUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersSamlUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
sAMLProviderRequest: data,
|
sAMLProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,7 +42,7 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
|
|||||||
async send(data: SCIMProvider): Promise<SCIMProvider> {
|
async send(data: SCIMProvider): Promise<SCIMProvider> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersScimUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk,
|
||||||
sCIMProviderRequest: data,
|
sCIMProviderRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,13 +5,13 @@ import "@goauthentik/components/events/ObjectChangelog";
|
|||||||
import MDSCIMProvider from "@goauthentik/docs/providers/scim/index.md";
|
import MDSCIMProvider from "@goauthentik/docs/providers/scim/index.md";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Markdown";
|
import "@goauthentik/elements/Markdown";
|
||||||
|
import "@goauthentik/elements/SyncStatusCard";
|
||||||
import "@goauthentik/elements/Tabs";
|
import "@goauthentik/elements/Tabs";
|
||||||
import "@goauthentik/elements/buttons/ActionButton";
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
import "@goauthentik/elements/events/LogViewer";
|
|
||||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
@ -32,8 +32,6 @@ import {
|
|||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
RbacPermissionsAssignedByUsersListModelEnum,
|
RbacPermissionsAssignedByUsersListModelEnum,
|
||||||
SCIMProvider,
|
SCIMProvider,
|
||||||
SyncStatus,
|
|
||||||
SystemTaskStatusEnum,
|
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-provider-scim-view")
|
@customElement("ak-provider-scim-view")
|
||||||
@ -44,9 +42,6 @@ export class SCIMProviderViewPage extends AKElement {
|
|||||||
@state()
|
@state()
|
||||||
provider?: SCIMProvider;
|
provider?: SCIMProvider;
|
||||||
|
|
||||||
@state()
|
|
||||||
syncState?: SyncStatus;
|
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
PFBase,
|
PFBase,
|
||||||
@ -89,22 +84,7 @@ export class SCIMProviderViewPage extends AKElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html` <ak-tabs>
|
return html` <ak-tabs>
|
||||||
<section
|
<section slot="page-overview" data-tab-title="${msg("Overview")}">
|
||||||
slot="page-overview"
|
|
||||||
data-tab-title="${msg("Overview")}"
|
|
||||||
@activate=${() => {
|
|
||||||
new ProvidersApi(DEFAULT_CONFIG)
|
|
||||||
.providersScimSyncStatusRetrieve({
|
|
||||||
id: this.provider?.pk || 0,
|
|
||||||
})
|
|
||||||
.then((state) => {
|
|
||||||
this.syncState = state;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.syncState = undefined;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${this.renderTabOverview()}
|
${this.renderTabOverview()}
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
@ -131,39 +111,6 @@ export class SCIMProviderViewPage extends AKElement {
|
|||||||
</ak-tabs>`;
|
</ak-tabs>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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`
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
${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`<li>
|
|
||||||
<p>${task.name}</p>
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
<li>${header}</li>
|
|
||||||
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
|
||||||
</ul>
|
|
||||||
</li> `;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTabOverview(): TemplateResult {
|
renderTabOverview(): TemplateResult {
|
||||||
if (!this.provider) {
|
if (!this.provider) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -218,33 +165,22 @@ export class SCIMProviderViewPage extends AKElement {
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
||||||
<div class="pf-c-card__title">
|
<ak-sync-status-card
|
||||||
<p>${msg("Sync status")}</p>
|
.fetch=${() => {
|
||||||
</div>
|
return new ProvidersApi(
|
||||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
DEFAULT_CONFIG,
|
||||||
<div class="pf-c-card__footer">
|
).providersScimSyncStatusRetrieve({
|
||||||
<ak-action-button
|
id: this.provider?.pk || 0,
|
||||||
class="pf-m-secondary"
|
});
|
||||||
.apiRequest=${() => {
|
}}
|
||||||
return new ProvidersApi(DEFAULT_CONFIG)
|
.triggerSync=${() => {
|
||||||
.providersScimPartialUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersScimPartialUpdate({
|
||||||
id: this.provider?.pk || 0,
|
id: this.provider?.pk || 0,
|
||||||
patchedSCIMProviderRequest: this.provider,
|
patchedSCIMProviderRequest: {},
|
||||||
})
|
});
|
||||||
.then(() => {
|
}}
|
||||||
this.dispatchEvent(
|
></ak-sync-status-card>
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Run sync again")}
|
|
||||||
</ak-action-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
|
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
|
||||||
|
@ -5,13 +5,14 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
|||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
|
import "@goauthentik/elements/SyncStatusCard";
|
||||||
import "@goauthentik/elements/Tabs";
|
import "@goauthentik/elements/Tabs";
|
||||||
import "@goauthentik/elements/buttons/ActionButton";
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { CSSResult, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
@ -29,7 +30,6 @@ import {
|
|||||||
RbacPermissionsAssignedByUsersListModelEnum,
|
RbacPermissionsAssignedByUsersListModelEnum,
|
||||||
SourcesApi,
|
SourcesApi,
|
||||||
SyncStatus,
|
SyncStatus,
|
||||||
SystemTaskStatusEnum,
|
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-source-ldap-view")
|
@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`
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
${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`<li>
|
|
||||||
<p>${task.name}</p>
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
<li>${header}</li>
|
|
||||||
${task.messages.map((m) => {
|
|
||||||
return html`<li>${m}</li>`;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</li> `;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
load(): void {
|
load(): void {
|
||||||
new SourcesApi(DEFAULT_CONFIG)
|
new SourcesApi(DEFAULT_CONFIG)
|
||||||
.sourcesLdapSyncStatusRetrieve({
|
.sourcesLdapSyncStatusRetrieve({
|
||||||
@ -187,35 +152,22 @@ export class LDAPSourceViewPage extends AKElement {
|
|||||||
></ak-source-ldap-connectivity>
|
></ak-source-ldap-connectivity>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-10-col">
|
<div class="pf-l-grid__item pf-m-10-col">
|
||||||
<div class="pf-c-card__title">
|
<ak-sync-status-card
|
||||||
<p>${msg("Sync status")}</p>
|
.fetch=${() => {
|
||||||
</div>
|
return new SourcesApi(DEFAULT_CONFIG).sourcesLdapSyncStatusRetrieve(
|
||||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
{
|
||||||
<div class="pf-c-card__footer">
|
slug: this.source?.slug,
|
||||||
<ak-action-button
|
},
|
||||||
class="pf-m-secondary"
|
);
|
||||||
?disabled=${this.syncState?.isRunning}
|
}}
|
||||||
.apiRequest=${() => {
|
.triggerSync=${() => {
|
||||||
return new SourcesApi(DEFAULT_CONFIG)
|
return new SourcesApi(DEFAULT_CONFIG).sourcesLdapPartialUpdate({
|
||||||
.sourcesLdapPartialUpdate({
|
slug: this.source?.slug || "",
|
||||||
slug: this.source?.slug || "",
|
patchedLDAPSourceRequest: {},
|
||||||
patchedLDAPSourceRequest: this.source,
|
});
|
||||||
})
|
}}
|
||||||
.then(() => {
|
></ak-sync-status-card>
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.load();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Run sync again")}
|
|
||||||
</ak-action-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
119
web/src/elements/SyncStatusCard.ts
Normal file
119
web/src/elements/SyncStatusCard.ts
Normal file
@ -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<SyncStatus>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
triggerSync!: () => Promise<unknown>;
|
||||||
|
|
||||||
|
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`<li>
|
||||||
|
${(this.syncState?.tasks || []).length > 1 ? html`<span>${task.name}</span>` : nothing}
|
||||||
|
<span
|
||||||
|
><ak-status-label
|
||||||
|
?good=${task.status === SystemTaskStatusEnum.Successful}
|
||||||
|
good-label=${msg("Finished successfully")}
|
||||||
|
bad-label=${msg("Finished with errors")}
|
||||||
|
></ak-status-label
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
>${msg(
|
||||||
|
str`Finished ${getRelativeTime(task.finishTimestamp)} (${task.finishTimestamp.toLocaleString()})`,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
||||||
|
</li> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSyncStatus(): TemplateResult {
|
||||||
|
if (this.loading) {
|
||||||
|
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
|
||||||
|
}
|
||||||
|
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`
|
||||||
|
<ul class="pf-c-list">
|
||||||
|
${this.syncState.tasks.map((task) => {
|
||||||
|
return this.renderSyncTask(task);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__title">${msg("Sync status")}</div>
|
||||||
|
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||||
|
<div class="pf-c-card__footer">
|
||||||
|
<ak-action-button
|
||||||
|
class="pf-m-secondary"
|
||||||
|
?disabled=${this.syncState?.isRunning}
|
||||||
|
.apiRequest=${() => {
|
||||||
|
if (this.syncState) {
|
||||||
|
// This is a bit of a UX cheat, we set isRunning to true to disable the button
|
||||||
|
// and change the text even before we hear back from the backend
|
||||||
|
this.syncState = {
|
||||||
|
...this.syncState,
|
||||||
|
isRunning: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.triggerSync().then(() => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.firstUpdated();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${this.syncState?.isRunning
|
||||||
|
? msg("Sync currently running")
|
||||||
|
: msg("Run sync again")}
|
||||||
|
</ak-action-button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
@ -8645,60 +8645,79 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s8cfd29891b2c93f0">
|
<trans-unit id="s8cfd29891b2c93f0">
|
||||||
<source>Google Workspace Provider</source>
|
<source>Google Workspace Provider</source>
|
||||||
|
<target>Google Workspace 提供程序</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa817aa9f6f88a991">
|
<trans-unit id="sa817aa9f6f88a991">
|
||||||
<source>Credentials</source>
|
<source>Credentials</source>
|
||||||
|
<target>证书</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s44b0d2fa9c97719d">
|
<trans-unit id="s44b0d2fa9c97719d">
|
||||||
<source>TODO</source>
|
<source>TODO</source>
|
||||||
|
<target>待定</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc668815044e218c2">
|
<trans-unit id="sc668815044e218c2">
|
||||||
<source>Delegated Subject</source>
|
<source>Delegated Subject</source>
|
||||||
|
<target>委托主题</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="se89a9ecf275e4343">
|
<trans-unit id="se89a9ecf275e4343">
|
||||||
<source>Default group email domain</source>
|
<source>Default group email domain</source>
|
||||||
|
<target>默认组电子邮件域</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s64a0a6da6ed6d680">
|
<trans-unit id="s64a0a6da6ed6d680">
|
||||||
<source>Default domain that is used to generate a group's email address. Can be customized using property mappings.</source>
|
<source>Default domain that is used to generate a group's email address. Can be customized using property mappings.</source>
|
||||||
|
<target>用于生成组内电子邮件的默认域。可以通过属性映射进行自定义。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sd061e65955e036ca">
|
<trans-unit id="sd061e65955e036ca">
|
||||||
<source>User deletion action</source>
|
<source>User deletion action</source>
|
||||||
|
<target>用户删除动作</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s87f86f65b5e19ea7">
|
<trans-unit id="s87f86f65b5e19ea7">
|
||||||
<source>User is deleted</source>
|
<source>User is deleted</source>
|
||||||
|
<target>用户被删除</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s3906cd4a102867a3">
|
<trans-unit id="s3906cd4a102867a3">
|
||||||
<source>Suspend</source>
|
<source>Suspend</source>
|
||||||
|
<target>停用</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s0bf5b364e2c79392">
|
<trans-unit id="s0bf5b364e2c79392">
|
||||||
<source>User is suspended, and connection to user in authentik is removed.</source>
|
<source>User is suspended, and connection to user in authentik is removed.</source>
|
||||||
|
<target>用户被停用,authentik 与此用户的连接被删除。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4acb0250d735c2a9">
|
<trans-unit id="s4acb0250d735c2a9">
|
||||||
<source>Do Nothing</source>
|
<source>Do Nothing</source>
|
||||||
|
<target>什么也不做</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s5fe525d19499ed47">
|
<trans-unit id="s5fe525d19499ed47">
|
||||||
<source>The connection is removed but the user is not modified</source>
|
<source>The connection is removed but the user is not modified</source>
|
||||||
|
<target>连接被删除,但用户未被修改</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s676a0127c35d240a">
|
<trans-unit id="s676a0127c35d240a">
|
||||||
<source>Determines what authentik will do when a User is deleted.</source>
|
<source>Determines what authentik will do when a User is deleted.</source>
|
||||||
|
<target>决定当用户被删除时,authentik 应该做什么。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sd1aa04cc32caf2b0">
|
<trans-unit id="sd1aa04cc32caf2b0">
|
||||||
<source>Group deletion action</source>
|
<source>Group deletion action</source>
|
||||||
|
<target>组删除动作</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s874c788479481131">
|
<trans-unit id="s874c788479481131">
|
||||||
<source>Group is deleted</source>
|
<source>Group is deleted</source>
|
||||||
|
<target>组被删除</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s0983fa9b84e4a9f7">
|
<trans-unit id="s0983fa9b84e4a9f7">
|
||||||
<source>The connection is removed but the group is not modified</source>
|
<source>The connection is removed but the group is not modified</source>
|
||||||
|
<target>连接被删除,但组未被修改</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sdb872a9f425937fa">
|
<trans-unit id="sdb872a9f425937fa">
|
||||||
<source>Determines what authentik will do when a Group is deleted.</source>
|
<source>Determines what authentik will do when a Group is deleted.</source>
|
||||||
|
<target>决定当组被删除时,authentik 应该做什么。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa5f28f1ad0bee45b">
|
<trans-unit id="sa5f28f1ad0bee45b">
|
||||||
<source>Google Workspace Provider is in preview.</source>
|
<source>Google Workspace Provider is in preview.</source>
|
||||||
|
<target>Google Workspace 提供程序处于预览状态。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sfcf7bc2ef234d938">
|
<trans-unit id="sfcf7bc2ef234d938">
|
||||||
<source>Update Google Provider</source>
|
<source>Update Google Provider</source>
|
||||||
|
<target>更新 Google 提供程序</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
@ -8642,6 +8642,82 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s3a20e5d7dd70dcd3">
|
<trans-unit id="s3a20e5d7dd70dcd3">
|
||||||
<source>Cleared</source>
|
<source>Cleared</source>
|
||||||
<target>已清除</target>
|
<target>已清除</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s8cfd29891b2c93f0">
|
||||||
|
<source>Google Workspace Provider</source>
|
||||||
|
<target>Google Workspace 提供程序</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sa817aa9f6f88a991">
|
||||||
|
<source>Credentials</source>
|
||||||
|
<target>证书</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s44b0d2fa9c97719d">
|
||||||
|
<source>TODO</source>
|
||||||
|
<target>待定</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sc668815044e218c2">
|
||||||
|
<source>Delegated Subject</source>
|
||||||
|
<target>委托主题</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="se89a9ecf275e4343">
|
||||||
|
<source>Default group email domain</source>
|
||||||
|
<target>默认组电子邮件域</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s64a0a6da6ed6d680">
|
||||||
|
<source>Default domain that is used to generate a group's email address. Can be customized using property mappings.</source>
|
||||||
|
<target>用于生成组内电子邮件的默认域。可以通过属性映射进行自定义。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sd061e65955e036ca">
|
||||||
|
<source>User deletion action</source>
|
||||||
|
<target>用户删除动作</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s87f86f65b5e19ea7">
|
||||||
|
<source>User is deleted</source>
|
||||||
|
<target>用户被删除</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s3906cd4a102867a3">
|
||||||
|
<source>Suspend</source>
|
||||||
|
<target>停用</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0bf5b364e2c79392">
|
||||||
|
<source>User is suspended, and connection to user in authentik is removed.</source>
|
||||||
|
<target>用户被停用,authentik 与此用户的连接被删除。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s4acb0250d735c2a9">
|
||||||
|
<source>Do Nothing</source>
|
||||||
|
<target>什么也不做</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s5fe525d19499ed47">
|
||||||
|
<source>The connection is removed but the user is not modified</source>
|
||||||
|
<target>连接被删除,但用户未被修改</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s676a0127c35d240a">
|
||||||
|
<source>Determines what authentik will do when a User is deleted.</source>
|
||||||
|
<target>决定当用户被删除时,authentik 应该做什么。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sd1aa04cc32caf2b0">
|
||||||
|
<source>Group deletion action</source>
|
||||||
|
<target>组删除动作</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s874c788479481131">
|
||||||
|
<source>Group is deleted</source>
|
||||||
|
<target>组被删除</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0983fa9b84e4a9f7">
|
||||||
|
<source>The connection is removed but the group is not modified</source>
|
||||||
|
<target>连接被删除,但组未被修改</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sdb872a9f425937fa">
|
||||||
|
<source>Determines what authentik will do when a Group is deleted.</source>
|
||||||
|
<target>决定当组被删除时,authentik 应该做什么。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sa5f28f1ad0bee45b">
|
||||||
|
<source>Google Workspace Provider is in preview.</source>
|
||||||
|
<target>Google Workspace 提供程序处于预览状态。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="sfcf7bc2ef234d938">
|
||||||
|
<source>Update Google Provider</source>
|
||||||
|
<target>更新 Google 提供程序</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
Reference in New Issue
Block a user