providers/scim: fix scim changed detection for nested attributes (#24332)

fix scim changed detection for nested attributes

Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
Ryan Pesek
2026-07-25 07:07:13 -05:00
committed by GitHub
parent 658ee6062b
commit 328ccafacf
4 changed files with 50 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
"""Group client"""
from copy import deepcopy
from itertools import batched
from typing import Any
@@ -135,12 +136,11 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
self._patch_add_users(connection, users)
return connection
def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser):
def diff(self, local_created: dict[str, Any], connection: SCIMProviderGroup):
"""Check if a group is different than what we last wrote to the remote system.
Returns true if there is a difference in data."""
local_known = connection.attributes
local_updated = {}
MERGE_LIST_UNIQUE.merge(local_updated, local_known)
local_updated = deepcopy(local_known)
MERGE_LIST_UNIQUE.merge(local_updated, local_created)
return self._json_encoder.encode(local_updated) != self._json_encoder.encode(local_known)

View File

@@ -1,5 +1,6 @@
"""User client"""
from copy import deepcopy
from typing import Any
from django.db import transaction
@@ -95,8 +96,7 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
"""Check if a user is different than what we last wrote to the remote system.
Returns true if there is a difference in data."""
local_known = connection.attributes
local_updated = {}
MERGE_LIST_UNIQUE.merge(local_updated, local_known)
local_updated = deepcopy(local_known)
MERGE_LIST_UNIQUE.merge(local_updated, local_created)
return self._json_encoder.encode(local_updated) != self._json_encoder.encode(local_known)

View File

@@ -10,6 +10,7 @@ from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.groups import SCIMGroupClient
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderGroup
from authentik.providers.scim.tasks import scim_sync
@@ -209,6 +210,20 @@ class SCIMGroupTests(TestCase):
self.assertEqual(mock.request_history[2].method, "GET")
self.assertNotIn("PUT", [req.method for req in mock.request_history])
@Mocker()
def test_group_diff_nested_attribute(self, mock: Mocker):
"""Test nested attribute changes are detected without mutating cached data"""
mock.get("https://localhost/ServiceProviderConfig", json={})
connection = SCIMProviderGroup(attributes={"custom": {"value": "old"}})
self.assertTrue(
SCIMGroupClient(self.provider).diff(
{"custom": {"value": "new"}},
connection,
)
)
self.assertEqual(connection.attributes["custom"]["value"], "old")
@Mocker()
def test_discover(self, mock: Mocker):
group = Group.objects.create(name="acl_admins")

View File

@@ -12,6 +12,7 @@ from authentik.core.models import Application, Group, User, UserTypes
from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.base import SAFE_METHODS
from authentik.lib.sync.outgoing.exceptions import TransientSyncException
from authentik.providers.scim.clients.users import SCIMUserClient
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects, sync_tasks
from authentik.tasks.models import Task
@@ -540,6 +541,35 @@ class SCIMUserTests(TestCase):
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
@Mocker()
def test_user_diff_nested_attribute(self, mock: Mocker):
"""Test nested attribute changes are detected without mutating cached data"""
mock.get("https://localhost/ServiceProviderConfig", json={})
connection = SCIMProviderUser(
attributes={
"urn:ietf:params:scim:schemas:extension:example:2.0:User": {
"birthDate": "1990-01-31"
}
}
)
self.assertTrue(
SCIMUserClient(self.provider).diff(
{
"urn:ietf:params:scim:schemas:extension:example:2.0:User": {
"birthDate": "1991-02-01"
}
},
connection,
)
)
self.assertEqual(
connection.attributes["urn:ietf:params:scim:schemas:extension:example:2.0:User"][
"birthDate"
],
"1990-01-31",
)
@Mocker()
def test_discover(self, mock: Mocker):
user = User.objects.create(username="admin@goauthentik.io")