diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py index cf403d1f59..0a2f03caac 100644 --- a/authentik/providers/scim/clients/groups.py +++ b/authentik/providers/scim/clients/groups.py @@ -47,15 +47,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: """Convert authentik user into SCIM""" - raw_scim_group = super().to_schema( - obj, - connection, - schemas=(SCIM_GROUP_SCHEMA,), - ) + raw_scim_group = super().to_schema(obj, connection) try: scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) except ValidationError as exc: raise StopSync(exc, obj) from exc + if SCIM_GROUP_SCHEMA not in scim_group.schemas: + scim_group.schemas.insert(0, SCIM_GROUP_SCHEMA) + # As this might be unset, we need to tell pydantic it's set so ensure the schemas + # are included, even if its just the defaults + scim_group.schemas = list(scim_group.schemas) if not scim_group.externalId: scim_group.externalId = str(obj.pk) diff --git a/authentik/providers/scim/clients/users.py b/authentik/providers/scim/clients/users.py index 579d4c8381..2334f26ec1 100644 --- a/authentik/providers/scim/clients/users.py +++ b/authentik/providers/scim/clients/users.py @@ -31,15 +31,16 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]): def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: """Convert authentik user into SCIM""" - raw_scim_user = super().to_schema( - obj, - connection, - schemas=(SCIM_USER_SCHEMA,), - ) + raw_scim_user = super().to_schema(obj, connection) try: scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) except ValidationError as exc: raise StopSync(exc, obj) from exc + if SCIM_USER_SCHEMA not in scim_user.schemas: + scim_user.schemas.insert(0, SCIM_USER_SCHEMA) + # As this might be unset, we need to tell pydantic it's set so ensure the schemas + # are included, even if its just the defaults + scim_user.schemas = list(scim_user.schemas) if not scim_user.externalId: scim_user.externalId = str(obj.uid) return scim_user diff --git a/authentik/providers/scim/tests/test_user.py b/authentik/providers/scim/tests/test_user.py index 582ba03814..2f32b68857 100644 --- a/authentik/providers/scim/tests/test_user.py +++ b/authentik/providers/scim/tests/test_user.py @@ -91,6 +91,57 @@ class SCIMUserTests(TestCase): }, ) + @Mocker() + def test_user_create_custom_schema(self, mock: Mocker): + """Test user creation with custom schema""" + schema = SCIMMapping.objects.create( + name="custom_schema", + expression="""return {"schemas": ["foo"]}""", + ) + self.provider.property_mappings.add(schema) + scim_id = generate_id() + mock.get( + "https://localhost/ServiceProviderConfig", + json={}, + ) + mock.post( + "https://localhost/Users", + json={ + "id": scim_id, + }, + ) + uid = generate_id() + user = User.objects.create( + username=uid, + name=f"{uid} {uid}", + email=f"{uid}@goauthentik.io", + ) + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[1].method, "POST") + self.assertJSONEqual( + mock.request_history[1].body, + { + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "foo"], + "active": True, + "emails": [ + { + "primary": True, + "type": "other", + "value": f"{uid}@goauthentik.io", + } + ], + "externalId": user.uid, + "name": { + "familyName": uid, + "formatted": f"{uid} {uid}", + "givenName": uid, + }, + "displayName": f"{uid} {uid}", + "userName": uid, + }, + ) + @Mocker() def test_user_create_different_provider_same_id(self, mock: Mocker): """Test user creation with multiple providers that happen