authentik.enterprise.providers.microsoft_entra.clients.groups
1from deepmerge import always_merger 2from django.db import transaction 3from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder 4from msgraph.generated.models.group import Group as MSGroup 5from msgraph.generated.models.reference_create import ReferenceCreate 6 7from authentik.core.models import Group 8from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient 9from authentik.enterprise.providers.microsoft_entra.models import ( 10 MicrosoftEntraProvider, 11 MicrosoftEntraProviderGroup, 12 MicrosoftEntraProviderMapping, 13 MicrosoftEntraProviderUser, 14) 15from authentik.lib.sync.mapper import PropertyMappingManager 16from authentik.lib.sync.outgoing.base import Direction 17from authentik.lib.sync.outgoing.exceptions import ( 18 NotFoundSyncException, 19 ObjectExistsSyncException, 20 StopSync, 21 TransientSyncException, 22) 23from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction 24 25 26class MicrosoftEntraGroupClient( 27 MicrosoftEntraSyncClient[Group, MicrosoftEntraProviderGroup, MSGroup] 28): 29 """Microsoft client for groups""" 30 31 connection_type = MicrosoftEntraProviderGroup 32 connection_type_query = "group" 33 can_discover = True 34 35 def __init__(self, provider: MicrosoftEntraProvider) -> None: 36 super().__init__(provider) 37 self.mapper = PropertyMappingManager( 38 self.provider.property_mappings_group.all().order_by("name").select_subclasses(), 39 MicrosoftEntraProviderMapping, 40 ["group", "provider", "connection"], 41 ) 42 43 def to_schema(self, obj: Group, connection: MicrosoftEntraProviderGroup) -> MSGroup: 44 """Convert authentik group""" 45 raw_microsoft_group = super().to_schema(obj, connection) 46 try: 47 return MSGroup(**raw_microsoft_group) 48 except TypeError as exc: 49 raise StopSync(exc, obj) from exc 50 51 def delete(self, identifier: str): 52 """Delete group""" 53 MicrosoftEntraProviderGroup.objects.filter( 54 provider=self.provider, microsoft_id=identifier 55 ).delete() 56 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 57 return self._request(self.client.groups.by_group_id(identifier).delete()) 58 59 def create(self, group: Group): 60 """Create group from scratch and create a connection object""" 61 microsoft_group = self.to_schema(group, None) 62 with transaction.atomic(): 63 try: 64 response = self._request(self.client.groups.post(microsoft_group)) 65 except ObjectExistsSyncException: 66 # group already exists in microsoft entra, so we can connect them manually 67 # for groups we need to fetch the group from microsoft as we connect on 68 # ID and not group email 69 query_params = GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( 70 filter=f"displayName eq '{microsoft_group.display_name}'", 71 ) 72 request_configuration = ( 73 GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( 74 query_parameters=query_params, 75 ) 76 ) 77 group_data = self._request(self.client.groups.get(request_configuration)) 78 if group_data.odata_count < 1 or len(group_data.value) < 1: 79 self.logger.warning( 80 "Group which could not be created also does not exist", group=group 81 ) 82 return 83 ms_group = group_data.value[0] 84 return MicrosoftEntraProviderGroup.objects.create( 85 provider=self.provider, 86 group=group, 87 microsoft_id=ms_group.id, 88 attributes=self.entity_as_dict(ms_group), 89 ) 90 else: 91 return MicrosoftEntraProviderGroup.objects.create( 92 provider=self.provider, 93 group=group, 94 microsoft_id=response.id, 95 attributes=self.entity_as_dict(response), 96 ) 97 98 def update(self, group: Group, connection: MicrosoftEntraProviderGroup): 99 """Update existing group""" 100 microsoft_group = self.to_schema(group, connection) 101 microsoft_group.id = connection.microsoft_id 102 try: 103 response = self._request( 104 self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group) 105 ) 106 if response: 107 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 108 connection.save() 109 except NotFoundSyncException: 110 # Resource missing is handled by self.write, which will re-create the group 111 raise 112 113 def write(self, obj: Group): 114 microsoft_group, created = super().write(obj) 115 self.create_sync_members(obj, microsoft_group) 116 return microsoft_group, created 117 118 def create_sync_members(self, obj: Group, microsoft_group: MicrosoftEntraProviderGroup): 119 """Sync all members after a group was created""" 120 users = list(obj.users.order_by("id").values_list("id", flat=True)) 121 connections = MicrosoftEntraProviderUser.objects.filter( 122 provider=self.provider, user__pk__in=users 123 ).values_list("microsoft_id", flat=True) 124 self._patch(microsoft_group.microsoft_id, Direction.add, connections) 125 126 def update_group(self, group: Group, action: Direction, users_set: set[int]): 127 """Update a groups members""" 128 if action == Direction.add: 129 return self._patch_add_users(group, users_set) 130 if action == Direction.remove: 131 return self._patch_remove_users(group, users_set) 132 133 def _patch(self, microsoft_group_id: str, direction: Direction, members: list[str]): 134 for user in members: 135 try: 136 if direction == Direction.add: 137 request_body = ReferenceCreate( 138 odata_id=f"https://graph.microsoft.com/v1.0/directoryObjects/{user}", 139 ) 140 self._request( 141 self.client.groups.by_group_id(microsoft_group_id).members.ref.post( 142 request_body 143 ) 144 ) 145 if direction == Direction.remove: 146 self._request( 147 self.client.groups.by_group_id(microsoft_group_id) 148 .members.by_directory_object_id(user) 149 .ref.delete() 150 ) 151 except ObjectExistsSyncException: 152 pass 153 except TransientSyncException: 154 raise 155 156 def _patch_add_users(self, group: Group, users_set: set[int]): 157 """Add users in users_set to group""" 158 if len(users_set) < 1: 159 return 160 microsoft_group = MicrosoftEntraProviderGroup.objects.filter( 161 provider=self.provider, group=group 162 ).first() 163 if not microsoft_group: 164 self.logger.warning( 165 "could not sync group membership, group does not exist", group=group 166 ) 167 return 168 user_ids = list( 169 MicrosoftEntraProviderUser.objects.filter( 170 user__pk__in=users_set, provider=self.provider 171 ).values_list("microsoft_id", flat=True) 172 ) 173 if len(user_ids) < 1: 174 return 175 self._patch(microsoft_group.microsoft_id, Direction.add, user_ids) 176 177 def _patch_remove_users(self, group: Group, users_set: set[int]): 178 """Remove users in users_set from group""" 179 if len(users_set) < 1: 180 return 181 microsoft_group = MicrosoftEntraProviderGroup.objects.filter( 182 provider=self.provider, group=group 183 ).first() 184 if not microsoft_group: 185 self.logger.warning( 186 "could not sync group membership, group does not exist", group=group 187 ) 188 return 189 user_ids = list( 190 MicrosoftEntraProviderUser.objects.filter( 191 user__pk__in=users_set, provider=self.provider 192 ).values_list("microsoft_id", flat=True) 193 ) 194 if len(user_ids) < 1: 195 return 196 self._patch(microsoft_group.microsoft_id, Direction.remove, user_ids) 197 198 def discover(self): 199 """Iterate through all groups and connect them with authentik groups if possible""" 200 groups = self._request(self.client.groups.get()) 201 next_link = True 202 while next_link: 203 for group in groups.value: 204 self._discover_single_group(group) 205 next_link = groups.odata_next_link 206 if not next_link: 207 break 208 groups = self._request(self.client.groups.with_url(next_link).get()) 209 210 def _discover_single_group(self, group: MSGroup): 211 """handle discovery of a single group""" 212 microsoft_name = group.unique_name 213 matching_authentik_group = ( 214 self.provider.get_object_qs(Group).filter(name=microsoft_name).first() 215 ) 216 if not matching_authentik_group: 217 return 218 MicrosoftEntraProviderGroup.objects.update_or_create( 219 provider=self.provider, 220 group=matching_authentik_group, 221 microsoft_id=group.id, 222 defaults={"attributes": self.entity_as_dict(group)}, 223 ) 224 225 def update_single_attribute(self, connection: MicrosoftEntraProviderGroup): 226 data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get()) 227 connection.attributes = self.entity_as_dict(data)
class
MicrosoftEntraGroupClient(authentik.enterprise.providers.microsoft_entra.clients.base.MicrosoftEntraSyncClient[authentik.core.models.Group, authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup, msgraph.generated.models.group.Group]):
27class MicrosoftEntraGroupClient( 28 MicrosoftEntraSyncClient[Group, MicrosoftEntraProviderGroup, MSGroup] 29): 30 """Microsoft client for groups""" 31 32 connection_type = MicrosoftEntraProviderGroup 33 connection_type_query = "group" 34 can_discover = True 35 36 def __init__(self, provider: MicrosoftEntraProvider) -> None: 37 super().__init__(provider) 38 self.mapper = PropertyMappingManager( 39 self.provider.property_mappings_group.all().order_by("name").select_subclasses(), 40 MicrosoftEntraProviderMapping, 41 ["group", "provider", "connection"], 42 ) 43 44 def to_schema(self, obj: Group, connection: MicrosoftEntraProviderGroup) -> MSGroup: 45 """Convert authentik group""" 46 raw_microsoft_group = super().to_schema(obj, connection) 47 try: 48 return MSGroup(**raw_microsoft_group) 49 except TypeError as exc: 50 raise StopSync(exc, obj) from exc 51 52 def delete(self, identifier: str): 53 """Delete group""" 54 MicrosoftEntraProviderGroup.objects.filter( 55 provider=self.provider, microsoft_id=identifier 56 ).delete() 57 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 58 return self._request(self.client.groups.by_group_id(identifier).delete()) 59 60 def create(self, group: Group): 61 """Create group from scratch and create a connection object""" 62 microsoft_group = self.to_schema(group, None) 63 with transaction.atomic(): 64 try: 65 response = self._request(self.client.groups.post(microsoft_group)) 66 except ObjectExistsSyncException: 67 # group already exists in microsoft entra, so we can connect them manually 68 # for groups we need to fetch the group from microsoft as we connect on 69 # ID and not group email 70 query_params = GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( 71 filter=f"displayName eq '{microsoft_group.display_name}'", 72 ) 73 request_configuration = ( 74 GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( 75 query_parameters=query_params, 76 ) 77 ) 78 group_data = self._request(self.client.groups.get(request_configuration)) 79 if group_data.odata_count < 1 or len(group_data.value) < 1: 80 self.logger.warning( 81 "Group which could not be created also does not exist", group=group 82 ) 83 return 84 ms_group = group_data.value[0] 85 return MicrosoftEntraProviderGroup.objects.create( 86 provider=self.provider, 87 group=group, 88 microsoft_id=ms_group.id, 89 attributes=self.entity_as_dict(ms_group), 90 ) 91 else: 92 return MicrosoftEntraProviderGroup.objects.create( 93 provider=self.provider, 94 group=group, 95 microsoft_id=response.id, 96 attributes=self.entity_as_dict(response), 97 ) 98 99 def update(self, group: Group, connection: MicrosoftEntraProviderGroup): 100 """Update existing group""" 101 microsoft_group = self.to_schema(group, connection) 102 microsoft_group.id = connection.microsoft_id 103 try: 104 response = self._request( 105 self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group) 106 ) 107 if response: 108 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 109 connection.save() 110 except NotFoundSyncException: 111 # Resource missing is handled by self.write, which will re-create the group 112 raise 113 114 def write(self, obj: Group): 115 microsoft_group, created = super().write(obj) 116 self.create_sync_members(obj, microsoft_group) 117 return microsoft_group, created 118 119 def create_sync_members(self, obj: Group, microsoft_group: MicrosoftEntraProviderGroup): 120 """Sync all members after a group was created""" 121 users = list(obj.users.order_by("id").values_list("id", flat=True)) 122 connections = MicrosoftEntraProviderUser.objects.filter( 123 provider=self.provider, user__pk__in=users 124 ).values_list("microsoft_id", flat=True) 125 self._patch(microsoft_group.microsoft_id, Direction.add, connections) 126 127 def update_group(self, group: Group, action: Direction, users_set: set[int]): 128 """Update a groups members""" 129 if action == Direction.add: 130 return self._patch_add_users(group, users_set) 131 if action == Direction.remove: 132 return self._patch_remove_users(group, users_set) 133 134 def _patch(self, microsoft_group_id: str, direction: Direction, members: list[str]): 135 for user in members: 136 try: 137 if direction == Direction.add: 138 request_body = ReferenceCreate( 139 odata_id=f"https://graph.microsoft.com/v1.0/directoryObjects/{user}", 140 ) 141 self._request( 142 self.client.groups.by_group_id(microsoft_group_id).members.ref.post( 143 request_body 144 ) 145 ) 146 if direction == Direction.remove: 147 self._request( 148 self.client.groups.by_group_id(microsoft_group_id) 149 .members.by_directory_object_id(user) 150 .ref.delete() 151 ) 152 except ObjectExistsSyncException: 153 pass 154 except TransientSyncException: 155 raise 156 157 def _patch_add_users(self, group: Group, users_set: set[int]): 158 """Add users in users_set to group""" 159 if len(users_set) < 1: 160 return 161 microsoft_group = MicrosoftEntraProviderGroup.objects.filter( 162 provider=self.provider, group=group 163 ).first() 164 if not microsoft_group: 165 self.logger.warning( 166 "could not sync group membership, group does not exist", group=group 167 ) 168 return 169 user_ids = list( 170 MicrosoftEntraProviderUser.objects.filter( 171 user__pk__in=users_set, provider=self.provider 172 ).values_list("microsoft_id", flat=True) 173 ) 174 if len(user_ids) < 1: 175 return 176 self._patch(microsoft_group.microsoft_id, Direction.add, user_ids) 177 178 def _patch_remove_users(self, group: Group, users_set: set[int]): 179 """Remove users in users_set from group""" 180 if len(users_set) < 1: 181 return 182 microsoft_group = MicrosoftEntraProviderGroup.objects.filter( 183 provider=self.provider, group=group 184 ).first() 185 if not microsoft_group: 186 self.logger.warning( 187 "could not sync group membership, group does not exist", group=group 188 ) 189 return 190 user_ids = list( 191 MicrosoftEntraProviderUser.objects.filter( 192 user__pk__in=users_set, provider=self.provider 193 ).values_list("microsoft_id", flat=True) 194 ) 195 if len(user_ids) < 1: 196 return 197 self._patch(microsoft_group.microsoft_id, Direction.remove, user_ids) 198 199 def discover(self): 200 """Iterate through all groups and connect them with authentik groups if possible""" 201 groups = self._request(self.client.groups.get()) 202 next_link = True 203 while next_link: 204 for group in groups.value: 205 self._discover_single_group(group) 206 next_link = groups.odata_next_link 207 if not next_link: 208 break 209 groups = self._request(self.client.groups.with_url(next_link).get()) 210 211 def _discover_single_group(self, group: MSGroup): 212 """handle discovery of a single group""" 213 microsoft_name = group.unique_name 214 matching_authentik_group = ( 215 self.provider.get_object_qs(Group).filter(name=microsoft_name).first() 216 ) 217 if not matching_authentik_group: 218 return 219 MicrosoftEntraProviderGroup.objects.update_or_create( 220 provider=self.provider, 221 group=matching_authentik_group, 222 microsoft_id=group.id, 223 defaults={"attributes": self.entity_as_dict(group)}, 224 ) 225 226 def update_single_attribute(self, connection: MicrosoftEntraProviderGroup): 227 data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get()) 228 connection.attributes = self.entity_as_dict(data)
Microsoft client for groups
MicrosoftEntraGroupClient( provider: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider)
connection_type =
<class 'authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup'>
def
to_schema( self, obj: authentik.core.models.Group, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup) -> msgraph.generated.models.group.Group:
44 def to_schema(self, obj: Group, connection: MicrosoftEntraProviderGroup) -> MSGroup: 45 """Convert authentik group""" 46 raw_microsoft_group = super().to_schema(obj, connection) 47 try: 48 return MSGroup(**raw_microsoft_group) 49 except TypeError as exc: 50 raise StopSync(exc, obj) from exc
Convert authentik group
def
delete(self, identifier: str):
52 def delete(self, identifier: str): 53 """Delete group""" 54 MicrosoftEntraProviderGroup.objects.filter( 55 provider=self.provider, microsoft_id=identifier 56 ).delete() 57 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 58 return self._request(self.client.groups.by_group_id(identifier).delete())
Delete group
60 def create(self, group: Group): 61 """Create group from scratch and create a connection object""" 62 microsoft_group = self.to_schema(group, None) 63 with transaction.atomic(): 64 try: 65 response = self._request(self.client.groups.post(microsoft_group)) 66 except ObjectExistsSyncException: 67 # group already exists in microsoft entra, so we can connect them manually 68 # for groups we need to fetch the group from microsoft as we connect on 69 # ID and not group email 70 query_params = GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters( 71 filter=f"displayName eq '{microsoft_group.display_name}'", 72 ) 73 request_configuration = ( 74 GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration( 75 query_parameters=query_params, 76 ) 77 ) 78 group_data = self._request(self.client.groups.get(request_configuration)) 79 if group_data.odata_count < 1 or len(group_data.value) < 1: 80 self.logger.warning( 81 "Group which could not be created also does not exist", group=group 82 ) 83 return 84 ms_group = group_data.value[0] 85 return MicrosoftEntraProviderGroup.objects.create( 86 provider=self.provider, 87 group=group, 88 microsoft_id=ms_group.id, 89 attributes=self.entity_as_dict(ms_group), 90 ) 91 else: 92 return MicrosoftEntraProviderGroup.objects.create( 93 provider=self.provider, 94 group=group, 95 microsoft_id=response.id, 96 attributes=self.entity_as_dict(response), 97 )
Create group from scratch and create a connection object
def
update( self, group: authentik.core.models.Group, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup):
99 def update(self, group: Group, connection: MicrosoftEntraProviderGroup): 100 """Update existing group""" 101 microsoft_group = self.to_schema(group, connection) 102 microsoft_group.id = connection.microsoft_id 103 try: 104 response = self._request( 105 self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group) 106 ) 107 if response: 108 always_merger.merge(connection.attributes, self.entity_as_dict(response)) 109 connection.save() 110 except NotFoundSyncException: 111 # Resource missing is handled by self.write, which will re-create the group 112 raise
Update existing group
114 def write(self, obj: Group): 115 microsoft_group, created = super().write(obj) 116 self.create_sync_members(obj, microsoft_group) 117 return microsoft_group, created
Write object to destination. Uses self.create and self.update, but can be overwritten for further logic
def
create_sync_members( self, obj: authentik.core.models.Group, microsoft_group: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup):
119 def create_sync_members(self, obj: Group, microsoft_group: MicrosoftEntraProviderGroup): 120 """Sync all members after a group was created""" 121 users = list(obj.users.order_by("id").values_list("id", flat=True)) 122 connections = MicrosoftEntraProviderUser.objects.filter( 123 provider=self.provider, user__pk__in=users 124 ).values_list("microsoft_id", flat=True) 125 self._patch(microsoft_group.microsoft_id, Direction.add, connections)
Sync all members after a group was created
def
update_group( self, group: authentik.core.models.Group, action: authentik.lib.sync.outgoing.base.Direction, users_set: set[int]):
127 def update_group(self, group: Group, action: Direction, users_set: set[int]): 128 """Update a groups members""" 129 if action == Direction.add: 130 return self._patch_add_users(group, users_set) 131 if action == Direction.remove: 132 return self._patch_remove_users(group, users_set)
Update a groups members
def
discover(self):
199 def discover(self): 200 """Iterate through all groups and connect them with authentik groups if possible""" 201 groups = self._request(self.client.groups.get()) 202 next_link = True 203 while next_link: 204 for group in groups.value: 205 self._discover_single_group(group) 206 next_link = groups.odata_next_link 207 if not next_link: 208 break 209 groups = self._request(self.client.groups.with_url(next_link).get())
Iterate through all groups and connect them with authentik groups if possible
def
update_single_attribute( self, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderGroup):
226 def update_single_attribute(self, connection: MicrosoftEntraProviderGroup): 227 data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get()) 228 connection.attributes = self.entity_as_dict(data)
Update connection attributes on a connection object, when the connection is manually created