Skip to content

Commit

Permalink
providers/sync: improve v3 (goauthentik#9966)
Browse files Browse the repository at this point in the history
* make external id field externally visible

Signed-off-by: Jens Langhammer <[email protected]>

* catch up scim provider

Signed-off-by: Jens Langhammer <[email protected]>

* add missing views to scim provider

Signed-off-by: Jens Langhammer <[email protected]>

* make neither user nor group required for mapping testing

Signed-off-by: Jens Langhammer <[email protected]>

* improve SkipObject handling

Signed-off-by: Jens Langhammer <[email protected]>

* allow deletion of connection objects

Signed-off-by: Jens Langhammer <[email protected]>

* make entra logs less noisy

Signed-off-by: Jens Langhammer <[email protected]>

* make event_matcher less noisy

Signed-off-by: Jens Langhammer <[email protected]>

---------

Signed-off-by: Jens Langhammer <[email protected]>
  • Loading branch information
BeryJu authored Jun 6, 2024
1 parent 0c652a2 commit 88e9c9b
Show file tree
Hide file tree
Showing 28 changed files with 963 additions and 51 deletions.
6 changes: 3 additions & 3 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant
Expand Down Expand Up @@ -97,8 +97,8 @@ def excluded_models() -> list[type[Model]]:
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken,
LicenseUsage,
SCIMGroup,
SCIMUser,
SCIMProviderGroup,
SCIMProviderUser,
Tenant,
SystemTask,
ConnectionToken,
Expand Down
6 changes: 4 additions & 2 deletions authentik/core/api/property_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ class PropertyMappingViewSet(
class PropertyMappingTestSerializer(PolicyTestSerializer):
"""Test property mapping execution for a user/group with context"""

user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
group = PrimaryKeyRelatedField(queryset=Group.objects.all(), required=False)
user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, allow_null=True)
group = PrimaryKeyRelatedField(
queryset=Group.objects.all(), required=False, allow_null=True
)

queryset = PropertyMapping.objects.select_subclasses()
serializer_class = PropertyMappingSerializer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Meta:
model = GoogleWorkspaceProviderGroup
fields = [
"id",
"google_id",
"group",
"group_obj",
"provider",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Meta:
model = GoogleWorkspaceProviderUser
fields = [
"id",
"google_id",
"user",
"user_obj",
"provider",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Meta:
model = MicrosoftEntraProviderGroup
fields = [
"id",
"microsoft_id",
"group",
"group_obj",
"provider",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Meta:
model = MicrosoftEntraProviderUser
fields = [
"id",
"microsoft_id",
"user",
"user_obj",
"provider",
Expand Down
2 changes: 2 additions & 0 deletions authentik/lib/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def get_logger_config():
"gunicorn": "INFO",
"requests_mock": "WARNING",
"hpack": "WARNING",
"httpx": "WARNING",
"azure": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {
Expand Down
2 changes: 2 additions & 0 deletions authentik/lib/sync/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def iter_eval(
mapping.set_context(user, request, **kwargs)
try:
value = mapping.evaluate(mapping.model.expression)
except PropertyMappingExpressionException as exc:
raise exc from exc
except Exception as exc:
raise PropertyMappingExpressionException(exc, mapping.model) from exc
if value is None:
Expand Down
9 changes: 4 additions & 5 deletions authentik/lib/sync/outgoing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,9 @@ def to_schema(self, obj: TModel, connection: TConnection | None, **defaults) ->
}
eval_kwargs.setdefault("user", None)
for value in self.mapper.iter_eval(**eval_kwargs):
try:
always_merger.merge(raw_final_object, value)
except SkipObjectException as exc:
raise exc from exc
always_merger.merge(raw_final_object, value)
except SkipObjectException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
Expand All @@ -104,7 +103,7 @@ def to_schema(self, obj: TModel, connection: TConnection | None, **defaults) ->
).save()
raise StopSync(exc, obj, exc.mapping) from exc
if not raw_final_object:
raise StopSync(ValueError("No user mappings configured"), obj)
raise StopSync(ValueError("No mappings configured"), obj)
for key, value in defaults.items():
raw_final_object.setdefault(key, value)
return raw_final_object
Expand Down
1 change: 1 addition & 0 deletions authentik/lib/sync/outgoing/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def sync_objects(self, object_type: str, page: int, provider_pk: int):
try:
client.write(obj)
except SkipObjectException:
self.logger.debug("skipping object due to SkipObject", obj=obj)
continue
except BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj)
Expand Down
2 changes: 1 addition & 1 deletion authentik/policies/event_matcher/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def passes(self, request: PolicyRequest) -> PolicyResult:
result = checker(request, event)
if result is None:
continue
LOGGER.info(
LOGGER.debug(
"Event matcher check result",
checker=checker.__name__,
result=result,
Expand Down
43 changes: 43 additions & 0 deletions authentik/providers/scim/api/groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""SCIMProviderGroup API Views"""

from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet

from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.providers.scim.models import SCIMProviderGroup


class SCIMProviderGroupSerializer(ModelSerializer):
"""SCIMProviderGroup Serializer"""

group_obj = UserGroupSerializer(source="group", read_only=True)

class Meta:

model = SCIMProviderGroup
fields = [
"id",
"scim_id",
"group",
"group_obj",
"provider",
]


class SCIMProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""SCIMProviderGroup Viewset"""

queryset = SCIMProviderGroup.objects.all().select_related("group")
serializer_class = SCIMProviderGroupSerializer
filterset_fields = ["provider__id", "group__name", "group__group_uuid"]
search_fields = ["provider__name", "group__name"]
ordering = ["group__name"]
43 changes: 43 additions & 0 deletions authentik/providers/scim/api/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""SCIMProviderUser API Views"""

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.used_by import UsedByMixin
from authentik.providers.scim.models import SCIMProviderUser


class SCIMProviderUserSerializer(ModelSerializer):
"""SCIMProviderUser Serializer"""

user_obj = GroupMemberSerializer(source="user", read_only=True)

class Meta:

model = SCIMProviderUser
fields = [
"id",
"scim_id",
"user",
"user_obj",
"provider",
]


class SCIMProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""SCIMProviderUser Viewset"""

queryset = SCIMProviderUser.objects.all().select_related("user")
serializer_class = SCIMProviderUserSerializer
filterset_fields = ["provider__id", "user__username", "user__id"]
search_fields = ["provider__name", "user__username"]
ordering = ["user__username"]
39 changes: 23 additions & 16 deletions authentik/providers/scim/clients/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@
)
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchRequest
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMProvider, SCIMUser
from authentik.providers.scim.models import (
SCIMMapping,
SCIMProvider,
SCIMProviderGroup,
SCIMProviderUser,
)


class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
"""SCIM client for groups"""

connection_type = SCIMGroup
connection_type = SCIMProviderGroup
connection_type_query = "group"
mapper: PropertyMappingManager

Expand All @@ -37,7 +42,7 @@ def __init__(self, provider: SCIMProvider):
["group", "provider", "connection"],
)

def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(
obj,
Expand All @@ -52,7 +57,7 @@ def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:
scim_group.externalId = str(obj.pk)

users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = SCIMUser.objects.filter(provider=self.provider, user__pk__in=users)
connections = SCIMProviderUser.objects.filter(provider=self.provider, user__pk__in=users)
members = []
for user in connections:
members.append(
Expand All @@ -66,7 +71,7 @@ def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:

def delete(self, obj: Group):
"""Delete group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
Expand All @@ -88,9 +93,11 @@ def create(self, group: Group):
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
return SCIMProviderGroup.objects.create(
provider=self.provider, group=group, scim_id=scim_id
)

def update(self, group: Group, connection: SCIMGroup):
def update(self, group: Group, connection: SCIMProviderGroup):
"""Update existing group"""
scim_group = self.to_schema(group, connection)
scim_group.id = connection.scim_id
Expand Down Expand Up @@ -158,16 +165,16 @@ def _patch_add_users(self, group: Group, users_set: set[int]):
"""Add users in users_set to group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True
)
SCIMProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("scim_id", flat=True)
)
if len(user_ids) < 1:
return
Expand All @@ -184,16 +191,16 @@ def _patch_remove_users(self, group: Group, users_set: set[int]):
"""Remove users in users_set from group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True
)
SCIMProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("scim_id", flat=True)
)
if len(user_ids) < 1:
return
Expand Down
14 changes: 7 additions & 7 deletions authentik/providers/scim/clients/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMUser
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser


class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
"""SCIM client for users"""

connection_type = SCIMUser
connection_type = SCIMProviderUser
connection_type_query = "user"
mapper: PropertyMappingManager

Expand All @@ -27,7 +27,7 @@ def __init__(self, provider: SCIMProvider):
["provider", "connection"],
)

def to_schema(self, obj: User, connection: SCIMUser) -> SCIMUserSchema:
def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = super().to_schema(
obj,
Expand All @@ -44,7 +44,7 @@ def to_schema(self, obj: User, connection: SCIMUser) -> SCIMUserSchema:

def delete(self, obj: User):
"""Delete user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
scim_user = SCIMProviderUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
Expand All @@ -66,9 +66,9 @@ def create(self, user: User):
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)

def update(self, user: User, connection: SCIMUser):
def update(self, user: User, connection: SCIMProviderUser):
"""Update existing user"""
scim_user = self.to_schema(user, connection)
scim_user.id = connection.scim_id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.0.6 on 2024-06-04 07:45

from django.conf import settings
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
("authentik_providers_scim", "0007_scimgroup_scim_id_scimuser_scim_id_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RenameModel(
old_name="SCIMGroup",
new_name="SCIMProviderGroup",
),
migrations.RenameModel(
old_name="SCIMUser",
new_name="SCIMProviderUser",
),
]
Loading

0 comments on commit 88e9c9b

Please sign in to comment.