lib/providers/sync: improve outgoing sync (#9835)

* make connection objects not updatable but allow creating with provider

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

* save data returned from google/entra and show it in UI

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

* pass connection object

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

* set immutable id on user automatically

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

* better define transient error codes

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

* format

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

* fix entra

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-05-30 10:40:10 +09:00
committed by GitHub
parent dae4bf0d6b
commit 50fffa72cc
25 changed files with 292 additions and 408 deletions

View File

@ -1,14 +1,15 @@
"""GoogleWorkspaceProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
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):
class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
"""GoogleWorkspaceProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
@ -20,10 +21,20 @@ class GoogleWorkspaceProviderGroupSerializer(SourceSerializer):
"id",
"group",
"group_obj",
"provider",
"attributes",
]
extra_kwargs = {"attributes": {"read_only": True}}
class GoogleWorkspaceProviderGroupViewSet(UsedByMixin, ModelViewSet):
class GoogleWorkspaceProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""GoogleWorkspaceProviderGroup Viewset"""
queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group")

View File

@ -1,14 +1,15 @@
"""GoogleWorkspaceProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
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):
class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
"""GoogleWorkspaceProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
@ -20,10 +21,20 @@ class GoogleWorkspaceProviderUserSerializer(SourceSerializer):
"id",
"user",
"user_obj",
"provider",
"attributes",
]
extra_kwargs = {"attributes": {"read_only": True}}
class GoogleWorkspaceProviderUserViewSet(UsedByMixin, ModelViewSet):
class GoogleWorkspaceProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""GoogleWorkspaceProviderUser Viewset"""
queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user")

View File

@ -33,14 +33,14 @@ class GoogleWorkspaceGroupClient(
self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
GoogleWorkspaceProviderMapping,
["group", "provider", "creating"],
["group", "provider", "connection"],
)
def to_schema(self, obj: Group, creating: bool) -> dict:
def to_schema(self, obj: Group, connection: GoogleWorkspaceProviderGroup) -> dict:
"""Convert authentik group"""
return super().to_schema(
obj,
creating,
connection=connection,
email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}",
)
@ -61,7 +61,7 @@ class GoogleWorkspaceGroupClient(
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
google_group = self.to_schema(group, True)
google_group = self.to_schema(group, None)
self.check_email_valid(google_group["email"])
with transaction.atomic():
try:
@ -74,16 +74,22 @@ class GoogleWorkspaceGroupClient(
self.directory_service.groups().get(groupKey=google_group["email"])
)
return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=group_data["id"]
provider=self.provider,
group=group,
google_id=group_data["id"],
attributes=group_data,
)
else:
return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=response["id"]
provider=self.provider,
group=group,
google_id=response["id"],
attributes=response,
)
def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
"""Update existing group"""
google_group = self.to_schema(group, False)
google_group = self.to_schema(group, connection)
self.check_email_valid(google_group["email"])
try:
return self._request(
@ -204,4 +210,5 @@ class GoogleWorkspaceGroupClient(
provider=self.provider,
group=matching_authentik_group,
google_id=google_id,
attributes=group,
)

View File

@ -28,15 +28,12 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(),
GoogleWorkspaceProviderMapping,
["provider", "creating"],
["provider", "connection"],
)
def to_schema(self, obj: User, creating: bool) -> dict:
def to_schema(self, obj: User, connection: GoogleWorkspaceProviderUser) -> dict:
"""Convert authentik user"""
raw_google_user = super().to_schema(obj, creating)
if "primaryEmail" not in raw_google_user:
raw_google_user["primaryEmail"] = str(obj.email)
return delete_none_values(raw_google_user)
return delete_none_values(super().to_schema(obj, connection, primaryEmail=obj.email))
def delete(self, obj: User):
"""Delete user"""
@ -63,7 +60,7 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
def create(self, user: User):
"""Create user from scratch and create a connection object"""
google_user = self.to_schema(user, True)
google_user = self.to_schema(user, None)
self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
)
@ -73,18 +70,21 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
except ObjectExistsSyncException:
# user already exists in google workspace, so we can connect them manually
return GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=user.email
provider=self.provider, user=user, google_id=user.email, attributes={}
)
except TransientSyncException as exc:
raise exc
else:
return GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=response["primaryEmail"]
provider=self.provider,
user=user,
google_id=response["primaryEmail"],
attributes=response,
)
def update(self, user: User, connection: GoogleWorkspaceProviderUser):
"""Update existing user"""
google_user = self.to_schema(user, False)
google_user = self.to_schema(user, connection)
self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
)
@ -115,4 +115,5 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
provider=self.provider,
user=matching_authentik_user,
google_id=email,
attributes=user,
)

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.6 on 2024-05-23 20:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_google_workspace",
"0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more",
),
]
operations = [
migrations.AddField(
model_name="googleworkspaceprovidergroup",
name="attributes",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="googleworkspaceprovideruser",
name="attributes",
field=models.JSONField(default=dict),
),
]

View File

@ -153,6 +153,7 @@ class GoogleWorkspaceProviderUser(SerializerModel):
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
@ -178,6 +179,7 @@ class GoogleWorkspaceProviderGroup(SerializerModel):
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:

View File

@ -1,14 +1,15 @@
"""MicrosoftEntraProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
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):
class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
"""MicrosoftEntraProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
@ -20,10 +21,20 @@ class MicrosoftEntraProviderGroupSerializer(SourceSerializer):
"id",
"group",
"group_obj",
"provider",
"attributes",
]
extra_kwargs = {"attributes": {"read_only": True}}
class MicrosoftEntraProviderGroupViewSet(UsedByMixin, ModelViewSet):
class MicrosoftEntraProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""MicrosoftEntraProviderGroup Viewset"""
queryset = MicrosoftEntraProviderGroup.objects.all().select_related("group")

View File

@ -1,14 +1,15 @@
"""MicrosoftEntraProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
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):
class MicrosoftEntraProviderUserSerializer(ModelSerializer):
"""MicrosoftEntraProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
@ -20,10 +21,20 @@ class MicrosoftEntraProviderUserSerializer(SourceSerializer):
"id",
"user",
"user_obj",
"provider",
"attributes",
]
extra_kwargs = {"attributes": {"read_only": True}}
class MicrosoftEntraProviderUserViewSet(UsedByMixin, ModelViewSet):
class MicrosoftEntraProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""MicrosoftEntraProviderUser Viewset"""
queryset = MicrosoftEntraProviderUser.objects.all().select_related("user")

View File

@ -1,5 +1,6 @@
from asyncio import run
from collections.abc import Coroutine
from dataclasses import asdict
from typing import Any
from azure.core.exceptions import (
@ -15,6 +16,7 @@ from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
from kiota_http.kiota_client_factory import KiotaClientFactory
from msgraph.generated.models.entity import Entity
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
@ -98,3 +100,10 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
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}")
def entity_as_dict(self, entity: Entity) -> dict:
"""Create a dictionary of a model instance, making sure to remove (known) things
we can't JSON serialize"""
raw_data = asdict(entity)
raw_data.pop("backing_store", None)
return raw_data

View File

@ -36,12 +36,12 @@ class MicrosoftEntraGroupClient(
self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
MicrosoftEntraProviderMapping,
["group", "provider", "creating"],
["group", "provider", "connection"],
)
def to_schema(self, obj: Group, creating: bool) -> MSGroup:
def to_schema(self, obj: Group, connection: MicrosoftEntraProviderGroup) -> MSGroup:
"""Convert authentik group"""
raw_microsoft_group = super().to_schema(obj, creating)
raw_microsoft_group = super().to_schema(obj, connection)
try:
return MSGroup(**raw_microsoft_group)
except TypeError as exc:
@ -62,7 +62,7 @@ class MicrosoftEntraGroupClient(
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
microsoft_group = self.to_schema(group, True)
microsoft_group = self.to_schema(group, None)
with transaction.atomic():
try:
response = self._request(self.client.groups.post(microsoft_group))
@ -79,22 +79,29 @@ class MicrosoftEntraGroupClient(
)
)
group_data = self._request(self.client.groups.get(request_configuration))
if group_data.odata_count < 1:
if group_data.odata_count < 1 or len(group_data.value) < 1:
self.logger.warning(
"Group which could not be created also does not exist", group=group
)
return
ms_group = group_data.value[0]
return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=group_data.value[0].id
provider=self.provider,
group=group,
microsoft_id=ms_group.id,
attributes=self.entity_as_dict(ms_group),
)
else:
return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=response.id
provider=self.provider,
group=group,
microsoft_id=response.id,
attributes=self.entity_as_dict(response),
)
def update(self, group: Group, connection: MicrosoftEntraProviderGroup):
"""Update existing group"""
microsoft_group = self.to_schema(group, False)
microsoft_group = self.to_schema(group, connection)
microsoft_group.id = connection.microsoft_id
try:
return self._request(
@ -213,4 +220,5 @@ class MicrosoftEntraGroupClient(
provider=self.provider,
group=matching_authentik_group,
microsoft_id=group.id,
attributes=self.entity_as_dict(group),
)

View File

@ -31,12 +31,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(),
MicrosoftEntraProviderMapping,
["provider", "creating"],
["provider", "connection"],
)
def to_schema(self, obj: User, creating: bool) -> MSUser:
def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser:
"""Convert authentik user"""
raw_microsoft_user = super().to_schema(obj, creating)
raw_microsoft_user = super().to_schema(obj, connection)
try:
return MSUser(**delete_none_values(raw_microsoft_user))
except TypeError as exc:
@ -67,7 +67,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def create(self, user: User):
"""Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, True)
microsoft_user = self.to_schema(user, None)
self.check_email_valid(microsoft_user.user_principal_name)
with transaction.atomic():
try:
@ -83,24 +83,32 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
)
)
user_data = self._request(self.client.users.get(request_configuration))
if user_data.odata_count < 1:
if user_data.odata_count < 1 or len(user_data.value) < 1:
self.logger.warning(
"User which could not be created also does not exist", user=user
)
return
ms_user = user_data.value[0]
return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=user_data.value[0].id
provider=self.provider,
user=user,
microsoft_id=ms_user.id,
attributes=self.entity_as_dict(ms_user),
)
except TransientSyncException as exc:
raise exc
else:
print(self.entity_as_dict(response))
return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=response.id
provider=self.provider,
user=user,
microsoft_id=response.id,
attributes=self.entity_as_dict(response),
)
def update(self, user: User, connection: MicrosoftEntraProviderUser):
"""Update existing user"""
microsoft_user = self.to_schema(user, False)
microsoft_user = self.to_schema(user, connection)
self.check_email_valid(microsoft_user.user_principal_name)
self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user))
@ -125,4 +133,5 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
provider=self.provider,
user=matching_authentik_user,
microsoft_id=user.id,
attributes=self.entity_as_dict(user),
)

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-05-23 20:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_microsoft_entra", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="microsoftentraprovidergroup",
name="attributes",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="microsoftentraprovideruser",
name="attributes",
field=models.JSONField(default=dict),
),
]

View File

@ -142,6 +142,7 @@ class MicrosoftEntraProviderUser(SerializerModel):
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
@ -167,6 +168,7 @@ class MicrosoftEntraProviderGroup(SerializerModel):
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:

View File

@ -3,3 +3,6 @@
PAGE_SIZE = 100
PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour
HTTP_CONFLICT = 409
HTTP_NO_CONTENT = 204
HTTP_SERVICE_UNAVAILABLE = 503
HTTP_TOO_MANY_REQUESTS = 429

View File

@ -79,14 +79,14 @@ class BaseOutgoingSyncClient[
"""Delete object from destination"""
raise NotImplementedError()
def to_schema(self, obj: TModel, creating: bool, **defaults) -> TSchema:
def to_schema(self, obj: TModel, connection: TConnection | None, **defaults) -> TSchema:
"""Convert object to destination schema"""
raw_final_object = {}
try:
eval_kwargs = {
"request": None,
"provider": self.provider,
"creating": creating,
"connection": connection,
obj._meta.model_name: obj,
}
eval_kwargs.setdefault("user", None)

View File

@ -124,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
self.update(current, reference)
self.logger.debug("Updating")
except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == 422: # noqa: PLR2004
self.logger.debug("Failed to update current, triggering re-create")
self._recreate(current=current, reference=reference)

View File

@ -6,9 +6,18 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
from pydantic import ValidationError
from requests import RequestException, Session
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing import (
HTTP_CONFLICT,
HTTP_NO_CONTENT,
HTTP_SERVICE_UNAVAILABLE,
HTTP_TOO_MANY_REQUESTS,
)
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, ObjectExistsSyncException
from authentik.lib.sync.outgoing.exceptions import (
NotFoundSyncException,
ObjectExistsSyncException,
TransientSyncException,
)
from authentik.lib.utils.http import get_http_session
from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
@ -61,13 +70,15 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
if response.status_code >= HttpResponseBadRequest.status_code:
if response.status_code == HttpResponseNotFound.status_code:
raise NotFoundSyncException(response)
if response.status_code in [HTTP_TOO_MANY_REQUESTS, HTTP_SERVICE_UNAVAILABLE]:
raise TransientSyncException()
if response.status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException(response)
self.logger.warning(
"Failed to send SCIM request", path=path, method=method, response=response.text
)
raise SCIMRequestException(response)
if response.status_code == 204: # noqa: PLR2004
if response.status_code == HTTP_NO_CONTENT:
return {}
return response.json()

View File

@ -34,14 +34,14 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
SCIMMapping,
["group", "provider", "creating"],
["group", "provider", "connection"],
)
def to_schema(self, obj: Group, creating: bool) -> SCIMGroupSchema:
def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(
obj,
creating,
connection,
schemas=(SCIM_GROUP_SCHEMA,),
)
try:
@ -76,7 +76,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
scim_group = self.to_schema(group, True)
scim_group = self.to_schema(group, None)
response = self._request(
"POST",
"/Groups",
@ -92,7 +92,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
def update(self, group: Group, connection: SCIMGroup):
"""Update existing group"""
scim_group = self.to_schema(group, False)
scim_group = self.to_schema(group, connection)
scim_group.id = connection.scim_id
try:
return self._request(

View File

@ -24,14 +24,14 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(),
SCIMMapping,
["provider", "creating"],
["provider", "connection"],
)
def to_schema(self, obj: User, creating: bool) -> SCIMUserSchema:
def to_schema(self, obj: User, connection: SCIMUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = super().to_schema(
obj,
creating,
connection,
schemas=(SCIM_USER_SCHEMA,),
)
try:
@ -54,7 +54,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
def create(self, user: User):
"""Create user from scratch and create a connection object"""
scim_user = self.to_schema(user, True)
scim_user = self.to_schema(user, None)
response = self._request(
"POST",
"/Users",
@ -70,7 +70,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
def update(self, user: User, connection: SCIMUser):
"""Update existing user"""
scim_user = self.to_schema(user, False)
scim_user = self.to_schema(user, connection)
scim_user.id = connection.scim_id
self._request(
"PUT",

View File

@ -19,10 +19,16 @@ entries:
"mail_nickname": request.user.username,
"user_principal_name": request.user.email,
}
if creating:
if connection:
# If there is a connection already made (discover or update), we can use
# that connection's immutable_id...
user["on_premises_immutable_id"] = connection.attributes.get("on_premises_immutable_id")
else:
user["password_profile"] = PasswordProfile(
password=request.user.password
)
# ...otherwise we set an immutable ID based on the user's UID
user["on_premises_immutable_id"] = request.user.uid,
return user
- identifiers:
managed: goauthentik.io/providers/microsoft_entra/group

View File

@ -16369,85 +16369,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: providers_google_workspace_groups_update
description: GoogleWorkspaceProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroupRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_google_workspace_groups_partial_update
description: GoogleWorkspaceProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGoogleWorkspaceProviderGroupRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_google_workspace_groups_destroy
description: GoogleWorkspaceProviderGroup Viewset
@ -16646,85 +16567,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: providers_google_workspace_users_update
description: GoogleWorkspaceProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUserRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_google_workspace_users_partial_update
description: GoogleWorkspaceProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGoogleWorkspaceProviderUserRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_google_workspace_users_destroy
description: GoogleWorkspaceProviderUser Viewset
@ -17539,85 +17381,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: providers_microsoft_entra_groups_update
description: MicrosoftEntraProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroupRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_microsoft_entra_groups_partial_update
description: MicrosoftEntraProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedMicrosoftEntraProviderGroupRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_microsoft_entra_groups_destroy
description: MicrosoftEntraProviderGroup Viewset
@ -17816,85 +17579,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: providers_microsoft_entra_users_update
description: MicrosoftEntraProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUserRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_microsoft_entra_users_partial_update
description: MicrosoftEntraProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedMicrosoftEntraProviderUserRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_microsoft_entra_users_destroy
description: MicrosoftEntraProviderUser Viewset
@ -36634,10 +36318,16 @@ components:
allOf:
- $ref: '#/components/schemas/UserGroup'
readOnly: true
provider:
type: integer
attributes:
readOnly: true
required:
- attributes
- group
- group_obj
- id
- provider
GoogleWorkspaceProviderGroupRequest:
type: object
description: GoogleWorkspaceProviderGroup Serializer
@ -36645,8 +36335,11 @@ components:
group:
type: string
format: uuid
provider:
type: integer
required:
- group
- provider
GoogleWorkspaceProviderMapping:
type: object
description: GoogleWorkspaceProviderMapping Serializer
@ -36773,8 +36466,14 @@ components:
allOf:
- $ref: '#/components/schemas/GroupMember'
readOnly: true
provider:
type: integer
attributes:
readOnly: true
required:
- attributes
- id
- provider
- user
- user_obj
GoogleWorkspaceProviderUserRequest:
@ -36783,7 +36482,10 @@ components:
properties:
user:
type: integer
provider:
type: integer
required:
- provider
- user
Group:
type: object
@ -38284,10 +37986,16 @@ components:
allOf:
- $ref: '#/components/schemas/UserGroup'
readOnly: true
provider:
type: integer
attributes:
readOnly: true
required:
- attributes
- group
- group_obj
- id
- provider
MicrosoftEntraProviderGroupRequest:
type: object
description: MicrosoftEntraProviderGroup Serializer
@ -38295,8 +38003,11 @@ components:
group:
type: string
format: uuid
provider:
type: integer
required:
- group
- provider
MicrosoftEntraProviderMapping:
type: object
description: MicrosoftEntraProviderMapping Serializer
@ -38420,8 +38131,14 @@ components:
allOf:
- $ref: '#/components/schemas/GroupMember'
readOnly: true
provider:
type: integer
attributes:
readOnly: true
required:
- attributes
- id
- provider
- user
- user_obj
MicrosoftEntraProviderUserRequest:
@ -38430,7 +38147,10 @@ components:
properties:
user:
type: integer
provider:
type: integer
required:
- provider
- user
ModelEnum:
enum:
@ -41826,13 +41546,6 @@ components:
to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context.
PatchedGoogleWorkspaceProviderGroupRequest:
type: object
description: GoogleWorkspaceProviderGroup Serializer
properties:
group:
type: string
format: uuid
PatchedGoogleWorkspaceProviderMappingRequest:
type: object
description: GoogleWorkspaceProviderMapping Serializer
@ -41892,12 +41605,6 @@ components:
default_group_email_domain:
type: string
minLength: 1
PatchedGoogleWorkspaceProviderUserRequest:
type: object
description: GoogleWorkspaceProviderUser Serializer
properties:
user:
type: integer
PatchedGroupRequest:
type: object
description: Group Serializer
@ -42253,13 +41960,6 @@ components:
key:
type: string
minLength: 1
PatchedMicrosoftEntraProviderGroupRequest:
type: object
description: MicrosoftEntraProviderGroup Serializer
properties:
group:
type: string
format: uuid
PatchedMicrosoftEntraProviderMappingRequest:
type: object
description: MicrosoftEntraProviderMapping Serializer
@ -42316,12 +42016,6 @@ components:
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
group_delete_action:
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
PatchedMicrosoftEntraProviderUserRequest:
type: object
description: MicrosoftEntraProviderUser Serializer
properties:
user:
type: integer
PatchedNotificationRequest:
type: object
description: Notification Serializer

View File

@ -13,6 +13,8 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
@property({ type: Number })
providerId?: number;
expandable = true;
searchEnabled(): boolean {
return true;
}
@ -39,4 +41,12 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
html`${item.id}`,
];
}
renderExpanded(item: GoogleWorkspaceProviderGroup): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
}

View File

@ -17,6 +17,8 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
return true;
}
expandable = true;
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderUser>> {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersList({
page: page,
@ -40,4 +42,12 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
html`${item.id}`,
];
}
renderExpanded(item: GoogleWorkspaceProviderUser): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
}

View File

@ -13,6 +13,8 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
@property({ type: Number })
providerId?: number;
expandable = true;
searchEnabled(): boolean {
return true;
}
@ -39,4 +41,12 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
html`${item.id}`,
];
}
renderExpanded(item: MicrosoftEntraProviderGroup): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
}

View File

@ -13,6 +13,8 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
@property({ type: Number })
providerId?: number;
expandable = true;
searchEnabled(): boolean {
return true;
}
@ -40,4 +42,12 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
html`${item.id}`,
];
}
renderExpanded(item: MicrosoftEntraProviderUser): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
}