From 74d29e237457404bd1f51a9b4f31e5b1fa91222c Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 6 May 2024 14:55:10 +0200 Subject: [PATCH] sources/scim: fix duplicate groups and invalid schema (#9466) * sources/scim: fix duplicate groups Signed-off-by: Jens Langhammer * fix missing schema in response Signed-off-by: Jens Langhammer * fix members missing in returned group Signed-off-by: Jens Langhammer * optimise queries Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/providers/scim/clients/schema.py | 11 ++++----- authentik/sources/scim/views/v2/groups.py | 27 +++++++++++----------- authentik/sources/scim/views/v2/users.py | 9 ++++---- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/authentik/providers/scim/clients/schema.py b/authentik/providers/scim/clients/schema.py index befe784b54..f56d6b0e46 100644 --- a/authentik/providers/scim/clients/schema.py +++ b/authentik/providers/scim/clients/schema.py @@ -9,13 +9,14 @@ from pydanticscim.service_provider import ( ) from pydanticscim.user import User as BaseUser +SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User" +SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group" + class User(BaseUser): """Modified User schema with added externalId field""" - schemas: list[str] = [ - "urn:ietf:params:scim:schemas:core:2.0:User", - ] + schemas: list[str] = [SCIM_USER_SCHEMA] externalId: str | None = None meta: dict | None = None @@ -23,9 +24,7 @@ class User(BaseUser): class Group(BaseGroup): """Modified Group schema with added externalId field""" - schemas: list[str] = [ - "urn:ietf:params:scim:schemas:core:2.0:Group", - ] + schemas: list[str] = [SCIM_GROUP_SCHEMA] externalId: str | None = None meta: dict | None = None diff --git a/authentik/sources/scim/views/v2/groups.py b/authentik/sources/scim/views/v2/groups.py index ef33da20d7..ff27efc162 100644 --- a/authentik/sources/scim/views/v2/groups.py +++ b/authentik/sources/scim/views/v2/groups.py @@ -13,6 +13,7 @@ from rest_framework.request import Request from rest_framework.response import Response from authentik.core.models import Group, User +from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA from authentik.providers.scim.clients.schema import Group as SCIMGroupModel from authentik.sources.scim.models import SCIMSourceGroup from authentik.sources.scim.views.v2.base import SCIMView @@ -26,9 +27,11 @@ class GroupsView(SCIMView): def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: """Convert Group to SCIM data""" payload = SCIMGroupModel( + schemas=[SCIM_USER_SCHEMA], id=str(scim_group.group.pk), externalId=scim_group.id, displayName=scim_group.group.name, + members=[], meta={ "resourceType": "Group", "location": self.request.build_absolute_uri( @@ -42,28 +45,24 @@ class GroupsView(SCIMView): ), }, ) - return payload.model_dump( - mode="json", - exclude_unset=True, - ) + for member in scim_group.group.users.order_by("pk"): + member: User + payload.members.append(GroupMember(value=str(member.uuid))) + return payload.model_dump(mode="json", exclude_unset=True) def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response: """List Group handler""" + base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related( + "group__users" + ) if group_id: - connection = ( - SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id) - .select_related("group") - .first() - ) + connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() if not connection: raise Http404 return Response(self.group_to_scim(connection)) connections = ( - SCIMSourceGroup.objects.filter(source=self.source) - .select_related("group") - .order_by("pk") + base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) ) - connections = connections.filter(self.filter_parse(request)) page = self.paginate_query(connections) return Response( { @@ -79,6 +78,8 @@ class GroupsView(SCIMView): def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict): """Partial update a group""" group = connection.group if connection else Group() + if _group := Group.objects.filter(name=data.get("displayName")).first(): + group = _group if "displayName" in data: group.name = data.get("displayName") if group.name == "": diff --git a/authentik/sources/scim/views/v2/users.py b/authentik/sources/scim/views/v2/users.py index e53fe5f8ca..84dd555f29 100644 --- a/authentik/sources/scim/views/v2/users.py +++ b/authentik/sources/scim/views/v2/users.py @@ -11,6 +11,7 @@ from rest_framework.request import Request from rest_framework.response import Response from authentik.core.models import User +from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA from authentik.providers.scim.clients.schema import User as SCIMUserModel from authentik.sources.scim.models import SCIMSourceUser from authentik.sources.scim.views.v2.base import SCIMView @@ -33,6 +34,7 @@ class UsersView(SCIMView): def user_to_scim(self, scim_user: SCIMSourceUser) -> dict: """Convert User to SCIM data""" payload = SCIMUserModel( + schemas=[SCIM_USER_SCHEMA], id=str(scim_user.user.uuid), externalId=scim_user.id, userName=scim_user.user.username, @@ -62,10 +64,7 @@ class UsersView(SCIMView): ), }, ) - final_payload = payload.model_dump( - mode="json", - exclude_unset=True, - ) + final_payload = payload.model_dump(mode="json", exclude_unset=True) final_payload.update(scim_user.attributes) return final_payload @@ -99,6 +98,8 @@ class UsersView(SCIMView): def update_user(self, connection: SCIMSourceUser | None, data: QueryDict): """Partial update a user""" user = connection.user if connection else User() + if _user := User.objects.filter(username=data.get("userName")).first(): + user = _user user.path = self.source.get_user_path() if "userName" in data: user.username = data.get("userName")