authentik.providers.scim.clients.users
User client
1"""User client""" 2 3from typing import Any 4 5from django.db import transaction 6from django.db.models import Q 7from django.utils.http import urlencode 8from orjson import dumps 9from pydantic import ValidationError 10 11from authentik.core.models import User 12from authentik.lib.merge import MERGE_LIST_UNIQUE 13from authentik.lib.sync.mapper import PropertyMappingManager 14from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync 15from authentik.policies.utils import delete_none_values 16from authentik.providers.scim.clients.base import SCIMClient 17from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA 18from authentik.providers.scim.clients.schema import User as SCIMUserSchema 19from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser 20 21 22class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]): 23 """SCIM client for users""" 24 25 connection_type = SCIMProviderUser 26 connection_type_query = "user" 27 mapper: PropertyMappingManager 28 29 def __init__(self, provider: SCIMProvider): 30 super().__init__(provider) 31 self.mapper = PropertyMappingManager( 32 self.provider.property_mappings.all().order_by("name").select_subclasses(), 33 SCIMMapping, 34 ["provider", "connection"], 35 ) 36 37 def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: 38 """Convert authentik user into SCIM""" 39 raw_scim_user = super().to_schema(obj, connection) 40 try: 41 scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) 42 except ValidationError as exc: 43 raise StopSync(exc, obj) from exc 44 if SCIM_USER_SCHEMA not in scim_user.schemas: 45 scim_user.schemas.insert(0, SCIM_USER_SCHEMA) 46 # As this might be unset, we need to tell pydantic it's set so ensure the schemas 47 # are included, even if its just the defaults 48 scim_user.schemas = list(scim_user.schemas) 49 if not scim_user.externalId: 50 scim_user.externalId = str(obj.uid) 51 return scim_user 52 53 def delete(self, identifier: str): 54 """Delete user""" 55 SCIMProviderUser.objects.filter(provider=self.provider, scim_id=identifier).delete() 56 return self._request("DELETE", f"/Users/{identifier}") 57 58 def create(self, user: User): 59 """Create user from scratch and create a connection object""" 60 scim_user = self.to_schema(user, None) 61 with transaction.atomic(): 62 try: 63 response = self._request( 64 "POST", 65 "/Users", 66 json=scim_user.model_dump( 67 mode="json", 68 exclude_unset=True, 69 ), 70 ) 71 except ObjectExistsSyncException as exc: 72 if not self._config.filter.supported: 73 raise exc 74 users = self._request( 75 "GET", 76 f"/Users?{urlencode({'filter': f'userName eq "{scim_user.userName}"'})}", 77 ) 78 users_res = users.get("Resources", []) 79 if len(users_res) < 1: 80 raise exc 81 return SCIMProviderUser.objects.create( 82 provider=self.provider, 83 user=user, 84 scim_id=users_res[0]["id"], 85 attributes=users_res[0], 86 ) 87 else: 88 scim_id = response.get("id") 89 if not scim_id or scim_id == "": 90 raise StopSync("SCIM Response with missing or invalid `id`") 91 return SCIMProviderUser.objects.create( 92 provider=self.provider, user=user, scim_id=scim_id, attributes=response 93 ) 94 95 def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser): 96 """Check if a user is different than what we last wrote to the remote system. 97 Returns true if there is a difference in data.""" 98 local_known = connection.attributes 99 local_updated = {} 100 MERGE_LIST_UNIQUE.merge(local_updated, local_known) 101 MERGE_LIST_UNIQUE.merge(local_updated, local_created) 102 return dumps(local_updated) != dumps(local_known) 103 104 def update(self, user: User, connection: SCIMProviderUser): 105 """Update existing user""" 106 scim_user = self.to_schema(user, connection) 107 scim_user.id = connection.scim_id 108 payload = scim_user.model_dump( 109 mode="json", 110 exclude_unset=True, 111 ) 112 if not self.diff(payload, connection): 113 self.logger.debug("Skipping user write as data has not changed") 114 return 115 response = self._request( 116 "PUT", 117 f"/Users/{connection.scim_id}", 118 json=payload, 119 ) 120 connection.attributes = response 121 connection.save() 122 123 def discover(self): 124 res = self._request("GET", "/Users") 125 seen_items = 0 126 expected_items = int(res["totalResults"]) 127 while True: 128 for user in res["Resources"]: 129 self._discover_user_single(user) 130 seen_items += 1 131 if seen_items >= expected_items: 132 break 133 res = self._request("GET", f"/Users?startIndex={seen_items+1}") 134 135 def _discover_user_single(self, user: dict): 136 scim_user = SCIMUserSchema.model_validate(user) 137 if SCIMProviderUser.objects.filter(scim_id=scim_user.id, provider=self.provider).exists(): 138 return 139 user_query = Q(username=scim_user.userName) 140 for email in scim_user.emails: 141 user_query |= Q(username=email.value) | Q(email=email.value) 142 ak_user = User.objects.filter(user_query).first() 143 if not ak_user: 144 return 145 SCIMProviderUser.objects.create( 146 provider=self.provider, 147 user=ak_user, 148 scim_id=scim_user.id, 149 attributes=user, 150 )
class
SCIMUserClient(authentik.providers.scim.clients.base.SCIMClient[authentik.core.models.User, authentik.providers.scim.models.SCIMProviderUser, authentik.providers.scim.clients.schema.User]):
23class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]): 24 """SCIM client for users""" 25 26 connection_type = SCIMProviderUser 27 connection_type_query = "user" 28 mapper: PropertyMappingManager 29 30 def __init__(self, provider: SCIMProvider): 31 super().__init__(provider) 32 self.mapper = PropertyMappingManager( 33 self.provider.property_mappings.all().order_by("name").select_subclasses(), 34 SCIMMapping, 35 ["provider", "connection"], 36 ) 37 38 def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: 39 """Convert authentik user into SCIM""" 40 raw_scim_user = super().to_schema(obj, connection) 41 try: 42 scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) 43 except ValidationError as exc: 44 raise StopSync(exc, obj) from exc 45 if SCIM_USER_SCHEMA not in scim_user.schemas: 46 scim_user.schemas.insert(0, SCIM_USER_SCHEMA) 47 # As this might be unset, we need to tell pydantic it's set so ensure the schemas 48 # are included, even if its just the defaults 49 scim_user.schemas = list(scim_user.schemas) 50 if not scim_user.externalId: 51 scim_user.externalId = str(obj.uid) 52 return scim_user 53 54 def delete(self, identifier: str): 55 """Delete user""" 56 SCIMProviderUser.objects.filter(provider=self.provider, scim_id=identifier).delete() 57 return self._request("DELETE", f"/Users/{identifier}") 58 59 def create(self, user: User): 60 """Create user from scratch and create a connection object""" 61 scim_user = self.to_schema(user, None) 62 with transaction.atomic(): 63 try: 64 response = self._request( 65 "POST", 66 "/Users", 67 json=scim_user.model_dump( 68 mode="json", 69 exclude_unset=True, 70 ), 71 ) 72 except ObjectExistsSyncException as exc: 73 if not self._config.filter.supported: 74 raise exc 75 users = self._request( 76 "GET", 77 f"/Users?{urlencode({'filter': f'userName eq "{scim_user.userName}"'})}", 78 ) 79 users_res = users.get("Resources", []) 80 if len(users_res) < 1: 81 raise exc 82 return SCIMProviderUser.objects.create( 83 provider=self.provider, 84 user=user, 85 scim_id=users_res[0]["id"], 86 attributes=users_res[0], 87 ) 88 else: 89 scim_id = response.get("id") 90 if not scim_id or scim_id == "": 91 raise StopSync("SCIM Response with missing or invalid `id`") 92 return SCIMProviderUser.objects.create( 93 provider=self.provider, user=user, scim_id=scim_id, attributes=response 94 ) 95 96 def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser): 97 """Check if a user is different than what we last wrote to the remote system. 98 Returns true if there is a difference in data.""" 99 local_known = connection.attributes 100 local_updated = {} 101 MERGE_LIST_UNIQUE.merge(local_updated, local_known) 102 MERGE_LIST_UNIQUE.merge(local_updated, local_created) 103 return dumps(local_updated) != dumps(local_known) 104 105 def update(self, user: User, connection: SCIMProviderUser): 106 """Update existing user""" 107 scim_user = self.to_schema(user, connection) 108 scim_user.id = connection.scim_id 109 payload = scim_user.model_dump( 110 mode="json", 111 exclude_unset=True, 112 ) 113 if not self.diff(payload, connection): 114 self.logger.debug("Skipping user write as data has not changed") 115 return 116 response = self._request( 117 "PUT", 118 f"/Users/{connection.scim_id}", 119 json=payload, 120 ) 121 connection.attributes = response 122 connection.save() 123 124 def discover(self): 125 res = self._request("GET", "/Users") 126 seen_items = 0 127 expected_items = int(res["totalResults"]) 128 while True: 129 for user in res["Resources"]: 130 self._discover_user_single(user) 131 seen_items += 1 132 if seen_items >= expected_items: 133 break 134 res = self._request("GET", f"/Users?startIndex={seen_items+1}") 135 136 def _discover_user_single(self, user: dict): 137 scim_user = SCIMUserSchema.model_validate(user) 138 if SCIMProviderUser.objects.filter(scim_id=scim_user.id, provider=self.provider).exists(): 139 return 140 user_query = Q(username=scim_user.userName) 141 for email in scim_user.emails: 142 user_query |= Q(username=email.value) | Q(email=email.value) 143 ak_user = User.objects.filter(user_query).first() 144 if not ak_user: 145 return 146 SCIMProviderUser.objects.create( 147 provider=self.provider, 148 user=ak_user, 149 scim_id=scim_user.id, 150 attributes=user, 151 )
SCIM client for users
SCIMUserClient(provider: authentik.providers.scim.models.SCIMProvider)
connection_type =
<class 'authentik.providers.scim.models.SCIMProviderUser'>
def
to_schema( self, obj: authentik.core.models.User, connection: authentik.providers.scim.models.SCIMProviderUser) -> authentik.providers.scim.clients.schema.User:
38 def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: 39 """Convert authentik user into SCIM""" 40 raw_scim_user = super().to_schema(obj, connection) 41 try: 42 scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) 43 except ValidationError as exc: 44 raise StopSync(exc, obj) from exc 45 if SCIM_USER_SCHEMA not in scim_user.schemas: 46 scim_user.schemas.insert(0, SCIM_USER_SCHEMA) 47 # As this might be unset, we need to tell pydantic it's set so ensure the schemas 48 # are included, even if its just the defaults 49 scim_user.schemas = list(scim_user.schemas) 50 if not scim_user.externalId: 51 scim_user.externalId = str(obj.uid) 52 return scim_user
Convert authentik user into SCIM
def
delete(self, identifier: str):
54 def delete(self, identifier: str): 55 """Delete user""" 56 SCIMProviderUser.objects.filter(provider=self.provider, scim_id=identifier).delete() 57 return self._request("DELETE", f"/Users/{identifier}")
Delete user
59 def create(self, user: User): 60 """Create user from scratch and create a connection object""" 61 scim_user = self.to_schema(user, None) 62 with transaction.atomic(): 63 try: 64 response = self._request( 65 "POST", 66 "/Users", 67 json=scim_user.model_dump( 68 mode="json", 69 exclude_unset=True, 70 ), 71 ) 72 except ObjectExistsSyncException as exc: 73 if not self._config.filter.supported: 74 raise exc 75 users = self._request( 76 "GET", 77 f"/Users?{urlencode({'filter': f'userName eq "{scim_user.userName}"'})}", 78 ) 79 users_res = users.get("Resources", []) 80 if len(users_res) < 1: 81 raise exc 82 return SCIMProviderUser.objects.create( 83 provider=self.provider, 84 user=user, 85 scim_id=users_res[0]["id"], 86 attributes=users_res[0], 87 ) 88 else: 89 scim_id = response.get("id") 90 if not scim_id or scim_id == "": 91 raise StopSync("SCIM Response with missing or invalid `id`") 92 return SCIMProviderUser.objects.create( 93 provider=self.provider, user=user, scim_id=scim_id, attributes=response 94 )
Create user from scratch and create a connection object
def
diff( self, local_created: dict[str, typing.Any], connection: authentik.providers.scim.models.SCIMProviderUser):
96 def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser): 97 """Check if a user is different than what we last wrote to the remote system. 98 Returns true if there is a difference in data.""" 99 local_known = connection.attributes 100 local_updated = {} 101 MERGE_LIST_UNIQUE.merge(local_updated, local_known) 102 MERGE_LIST_UNIQUE.merge(local_updated, local_created) 103 return dumps(local_updated) != dumps(local_known)
Check if a user is different than what we last wrote to the remote system. Returns true if there is a difference in data.
def
update( self, user: authentik.core.models.User, connection: authentik.providers.scim.models.SCIMProviderUser):
105 def update(self, user: User, connection: SCIMProviderUser): 106 """Update existing user""" 107 scim_user = self.to_schema(user, connection) 108 scim_user.id = connection.scim_id 109 payload = scim_user.model_dump( 110 mode="json", 111 exclude_unset=True, 112 ) 113 if not self.diff(payload, connection): 114 self.logger.debug("Skipping user write as data has not changed") 115 return 116 response = self._request( 117 "PUT", 118 f"/Users/{connection.scim_id}", 119 json=payload, 120 ) 121 connection.attributes = response 122 connection.save()
Update existing user
def
discover(self):
124 def discover(self): 125 res = self._request("GET", "/Users") 126 seen_items = 0 127 expected_items = int(res["totalResults"]) 128 while True: 129 for user in res["Resources"]: 130 self._discover_user_single(user) 131 seen_items += 1 132 if seen_items >= expected_items: 133 break 134 res = self._request("GET", f"/Users?startIndex={seen_items+1}")
Optional method. Can be used to implement a "discovery" where upon creation of this provider, this function will be called and can pre-link any users/groups in the remote system with the respective object in authentik based on a common identifier