authentik.enterprise.providers.microsoft_entra.clients.users
1from deepmerge import always_merger 2from django.db import transaction 3from msgraph.generated.models.user import User as MSUser 4from msgraph.generated.users.users_request_builder import UsersRequestBuilder 5 6from authentik.core.models import User 7from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient 8from authentik.enterprise.providers.microsoft_entra.models import ( 9 MicrosoftEntraProvider, 10 MicrosoftEntraProviderMapping, 11 MicrosoftEntraProviderUser, 12) 13from authentik.lib.sync.mapper import PropertyMappingManager 14from authentik.lib.sync.outgoing.exceptions import ( 15 ObjectExistsSyncException, 16 StopSync, 17 TransientSyncException, 18) 19from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction 20from authentik.policies.utils import delete_none_values 21 22 23class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]): 24 """Sync authentik users into microsoft entra""" 25 26 connection_type = MicrosoftEntraProviderUser 27 connection_type_query = "user" 28 can_discover = True 29 30 def __init__(self, provider: MicrosoftEntraProvider) -> None: 31 super().__init__(provider) 32 self.mapper = PropertyMappingManager( 33 self.provider.property_mappings.all().order_by("name").select_subclasses(), 34 MicrosoftEntraProviderMapping, 35 ["provider", "connection"], 36 ) 37 38 def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser: 39 """Convert authentik user""" 40 raw_microsoft_user = super().to_schema(obj, connection) 41 try: 42 return MSUser(**delete_none_values(raw_microsoft_user)) 43 except TypeError as exc: 44 raise StopSync(exc, obj) from exc 45 46 def delete(self, identifier: str): 47 """Delete user""" 48 MicrosoftEntraProviderUser.objects.filter( 49 provider=self.provider, microsoft_id=identifier 50 ).delete() 51 if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE: 52 return self._request(self.client.users.by_user_id(identifier).delete()) 53 if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND: 54 return self._request( 55 self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False)) 56 ) 57 58 def get_select_fields(self) -> list[str]: 59 """All fields that should be selected when we fetch user data.""" 60 # TODO: Make this customizable in the future 61 return [ 62 # Default fields 63 "businessPhones", 64 "displayName", 65 "givenName", 66 "jobTitle", 67 "mail", 68 "mobilePhone", 69 "officeLocation", 70 "preferredLanguage", 71 "surname", 72 "userPrincipalName", 73 "id", 74 # Required for logging into M365 using authentik 75 "onPremisesImmutableId", 76 ] 77 78 def create(self, user: User): 79 """Create user from scratch and create a connection object""" 80 microsoft_user = self.to_schema(user, None) 81 if microsoft_user.user_principal_name: 82 self.check_email_valid(microsoft_user.user_principal_name) 83 with transaction.atomic(): 84 try: 85 response = self._request(self.client.users.post(microsoft_user)) 86 except ObjectExistsSyncException: 87 # user already exists in microsoft entra, so we can connect them manually 88 request_configuration = ( 89 UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 90 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 91 filter=f"mail eq '{microsoft_user.mail}'", 92 select=self.get_select_fields(), 93 ), 94 ) 95 ) 96 user_data = self._request(self.client.users.get(request_configuration)) 97 if user_data.odata_count < 1 or len(user_data.value) < 1: 98 self.logger.warning( 99 "User which could not be created also does not exist", user=user 100 ) 101 return 102 ms_user = user_data.value[0] 103 return MicrosoftEntraProviderUser.objects.create( 104 provider=self.provider, 105 user=user, 106 microsoft_id=ms_user.id, 107 attributes=self.entity_as_dict(ms_user), 108 ) 109 except TransientSyncException as exc: 110 raise exc 111 else: 112 return MicrosoftEntraProviderUser.objects.create( 113 provider=self.provider, 114 user=user, 115 microsoft_id=response.id, 116 attributes=self.entity_as_dict(response), 117 ) 118 119 def update(self, user: User, connection: MicrosoftEntraProviderUser): 120 """Update existing user""" 121 microsoft_user = self.to_schema(user, connection) 122 if microsoft_user.user_principal_name: 123 self.check_email_valid(microsoft_user.user_principal_name) 124 response = self._request( 125 self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user) 126 ) 127 if response: 128 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 129 connection.save() 130 131 def discover(self): 132 """Iterate through all users and connect them with authentik users if possible""" 133 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 134 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 135 select=self.get_select_fields(), 136 ), 137 ) 138 users = self._request(self.client.users.get(request_configuration)) 139 next_link = True 140 while next_link: 141 for user in users.value: 142 self._discover_single_user(user) 143 next_link = users.odata_next_link 144 if not next_link: 145 break 146 users = self._request(self.client.users.with_url(next_link).get()) 147 148 def _discover_single_user(self, user: MSUser): 149 """handle discovery of a single user""" 150 matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first() 151 if not matching_authentik_user: 152 return 153 MicrosoftEntraProviderUser.objects.update_or_create( 154 provider=self.provider, 155 user=matching_authentik_user, 156 microsoft_id=user.id, 157 defaults={"attributes": self.entity_as_dict(user)}, 158 ) 159 160 def update_single_attribute(self, connection: MicrosoftEntraProviderUser): 161 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 162 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 163 select=self.get_select_fields(), 164 ), 165 ) 166 data = self._request( 167 self.client.users.by_user_id(connection.microsoft_id).get(request_configuration) 168 ) 169 connection.attributes = self.entity_as_dict(data)
class
MicrosoftEntraUserClient(authentik.enterprise.providers.microsoft_entra.clients.base.MicrosoftEntraSyncClient[authentik.core.models.User, authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser, msgraph.generated.models.user.User]):
24class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]): 25 """Sync authentik users into microsoft entra""" 26 27 connection_type = MicrosoftEntraProviderUser 28 connection_type_query = "user" 29 can_discover = True 30 31 def __init__(self, provider: MicrosoftEntraProvider) -> None: 32 super().__init__(provider) 33 self.mapper = PropertyMappingManager( 34 self.provider.property_mappings.all().order_by("name").select_subclasses(), 35 MicrosoftEntraProviderMapping, 36 ["provider", "connection"], 37 ) 38 39 def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser: 40 """Convert authentik user""" 41 raw_microsoft_user = super().to_schema(obj, connection) 42 try: 43 return MSUser(**delete_none_values(raw_microsoft_user)) 44 except TypeError as exc: 45 raise StopSync(exc, obj) from exc 46 47 def delete(self, identifier: str): 48 """Delete user""" 49 MicrosoftEntraProviderUser.objects.filter( 50 provider=self.provider, microsoft_id=identifier 51 ).delete() 52 if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE: 53 return self._request(self.client.users.by_user_id(identifier).delete()) 54 if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND: 55 return self._request( 56 self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False)) 57 ) 58 59 def get_select_fields(self) -> list[str]: 60 """All fields that should be selected when we fetch user data.""" 61 # TODO: Make this customizable in the future 62 return [ 63 # Default fields 64 "businessPhones", 65 "displayName", 66 "givenName", 67 "jobTitle", 68 "mail", 69 "mobilePhone", 70 "officeLocation", 71 "preferredLanguage", 72 "surname", 73 "userPrincipalName", 74 "id", 75 # Required for logging into M365 using authentik 76 "onPremisesImmutableId", 77 ] 78 79 def create(self, user: User): 80 """Create user from scratch and create a connection object""" 81 microsoft_user = self.to_schema(user, None) 82 if microsoft_user.user_principal_name: 83 self.check_email_valid(microsoft_user.user_principal_name) 84 with transaction.atomic(): 85 try: 86 response = self._request(self.client.users.post(microsoft_user)) 87 except ObjectExistsSyncException: 88 # user already exists in microsoft entra, so we can connect them manually 89 request_configuration = ( 90 UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 91 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 92 filter=f"mail eq '{microsoft_user.mail}'", 93 select=self.get_select_fields(), 94 ), 95 ) 96 ) 97 user_data = self._request(self.client.users.get(request_configuration)) 98 if user_data.odata_count < 1 or len(user_data.value) < 1: 99 self.logger.warning( 100 "User which could not be created also does not exist", user=user 101 ) 102 return 103 ms_user = user_data.value[0] 104 return MicrosoftEntraProviderUser.objects.create( 105 provider=self.provider, 106 user=user, 107 microsoft_id=ms_user.id, 108 attributes=self.entity_as_dict(ms_user), 109 ) 110 except TransientSyncException as exc: 111 raise exc 112 else: 113 return MicrosoftEntraProviderUser.objects.create( 114 provider=self.provider, 115 user=user, 116 microsoft_id=response.id, 117 attributes=self.entity_as_dict(response), 118 ) 119 120 def update(self, user: User, connection: MicrosoftEntraProviderUser): 121 """Update existing user""" 122 microsoft_user = self.to_schema(user, connection) 123 if microsoft_user.user_principal_name: 124 self.check_email_valid(microsoft_user.user_principal_name) 125 response = self._request( 126 self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user) 127 ) 128 if response: 129 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 130 connection.save() 131 132 def discover(self): 133 """Iterate through all users and connect them with authentik users if possible""" 134 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 135 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 136 select=self.get_select_fields(), 137 ), 138 ) 139 users = self._request(self.client.users.get(request_configuration)) 140 next_link = True 141 while next_link: 142 for user in users.value: 143 self._discover_single_user(user) 144 next_link = users.odata_next_link 145 if not next_link: 146 break 147 users = self._request(self.client.users.with_url(next_link).get()) 148 149 def _discover_single_user(self, user: MSUser): 150 """handle discovery of a single user""" 151 matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first() 152 if not matching_authentik_user: 153 return 154 MicrosoftEntraProviderUser.objects.update_or_create( 155 provider=self.provider, 156 user=matching_authentik_user, 157 microsoft_id=user.id, 158 defaults={"attributes": self.entity_as_dict(user)}, 159 ) 160 161 def update_single_attribute(self, connection: MicrosoftEntraProviderUser): 162 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 163 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 164 select=self.get_select_fields(), 165 ), 166 ) 167 data = self._request( 168 self.client.users.by_user_id(connection.microsoft_id).get(request_configuration) 169 ) 170 connection.attributes = self.entity_as_dict(data)
Sync authentik users into microsoft entra
MicrosoftEntraUserClient( provider: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider)
connection_type =
<class 'authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser'>
def
to_schema( self, obj: authentik.core.models.User, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser) -> msgraph.generated.models.user.User:
39 def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser: 40 """Convert authentik user""" 41 raw_microsoft_user = super().to_schema(obj, connection) 42 try: 43 return MSUser(**delete_none_values(raw_microsoft_user)) 44 except TypeError as exc: 45 raise StopSync(exc, obj) from exc
Convert authentik user
def
delete(self, identifier: str):
47 def delete(self, identifier: str): 48 """Delete user""" 49 MicrosoftEntraProviderUser.objects.filter( 50 provider=self.provider, microsoft_id=identifier 51 ).delete() 52 if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE: 53 return self._request(self.client.users.by_user_id(identifier).delete()) 54 if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND: 55 return self._request( 56 self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False)) 57 )
Delete user
def
get_select_fields(self) -> list[str]:
59 def get_select_fields(self) -> list[str]: 60 """All fields that should be selected when we fetch user data.""" 61 # TODO: Make this customizable in the future 62 return [ 63 # Default fields 64 "businessPhones", 65 "displayName", 66 "givenName", 67 "jobTitle", 68 "mail", 69 "mobilePhone", 70 "officeLocation", 71 "preferredLanguage", 72 "surname", 73 "userPrincipalName", 74 "id", 75 # Required for logging into M365 using authentik 76 "onPremisesImmutableId", 77 ]
All fields that should be selected when we fetch user data.
79 def create(self, user: User): 80 """Create user from scratch and create a connection object""" 81 microsoft_user = self.to_schema(user, None) 82 if microsoft_user.user_principal_name: 83 self.check_email_valid(microsoft_user.user_principal_name) 84 with transaction.atomic(): 85 try: 86 response = self._request(self.client.users.post(microsoft_user)) 87 except ObjectExistsSyncException: 88 # user already exists in microsoft entra, so we can connect them manually 89 request_configuration = ( 90 UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 91 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 92 filter=f"mail eq '{microsoft_user.mail}'", 93 select=self.get_select_fields(), 94 ), 95 ) 96 ) 97 user_data = self._request(self.client.users.get(request_configuration)) 98 if user_data.odata_count < 1 or len(user_data.value) < 1: 99 self.logger.warning( 100 "User which could not be created also does not exist", user=user 101 ) 102 return 103 ms_user = user_data.value[0] 104 return MicrosoftEntraProviderUser.objects.create( 105 provider=self.provider, 106 user=user, 107 microsoft_id=ms_user.id, 108 attributes=self.entity_as_dict(ms_user), 109 ) 110 except TransientSyncException as exc: 111 raise exc 112 else: 113 return MicrosoftEntraProviderUser.objects.create( 114 provider=self.provider, 115 user=user, 116 microsoft_id=response.id, 117 attributes=self.entity_as_dict(response), 118 )
Create user from scratch and create a connection object
def
update( self, user: authentik.core.models.User, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser):
120 def update(self, user: User, connection: MicrosoftEntraProviderUser): 121 """Update existing user""" 122 microsoft_user = self.to_schema(user, connection) 123 if microsoft_user.user_principal_name: 124 self.check_email_valid(microsoft_user.user_principal_name) 125 response = self._request( 126 self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user) 127 ) 128 if response: 129 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 130 connection.save()
Update existing user
def
discover(self):
132 def discover(self): 133 """Iterate through all users and connect them with authentik users if possible""" 134 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 135 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 136 select=self.get_select_fields(), 137 ), 138 ) 139 users = self._request(self.client.users.get(request_configuration)) 140 next_link = True 141 while next_link: 142 for user in users.value: 143 self._discover_single_user(user) 144 next_link = users.odata_next_link 145 if not next_link: 146 break 147 users = self._request(self.client.users.with_url(next_link).get())
Iterate through all users and connect them with authentik users if possible
def
update_single_attribute( self, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser):
161 def update_single_attribute(self, connection: MicrosoftEntraProviderUser): 162 request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( 163 query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( 164 select=self.get_select_fields(), 165 ), 166 ) 167 data = self._request( 168 self.client.users.by_user_id(connection.microsoft_id).get(request_configuration) 169 ) 170 connection.attributes = self.entity_as_dict(data)
Update connection attributes on a connection object, when the connection is manually created