authentik.enterprise.providers.microsoft_entra.tests.test_groups

Microsoft Entra Group tests

  1"""Microsoft Entra Group tests"""
  2
  3from unittest.mock import AsyncMock, MagicMock, patch
  4
  5from azure.identity.aio import ClientSecretCredential
  6from django.test import TestCase
  7from msgraph.generated.models.group import Group as MSGroup
  8from msgraph.generated.models.group_collection_response import GroupCollectionResponse
  9from msgraph.generated.models.organization import Organization
 10from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
 11from msgraph.generated.models.user import User as MSUser
 12from msgraph.generated.models.user_collection_response import UserCollectionResponse
 13from msgraph.generated.models.verified_domain import VerifiedDomain
 14
 15from authentik.blueprints.tests import apply_blueprint
 16from authentik.core.models import Application, Group, User
 17from authentik.core.tests.utils import create_test_user
 18from authentik.enterprise.providers.microsoft_entra.models import (
 19    MicrosoftEntraProvider,
 20    MicrosoftEntraProviderGroup,
 21    MicrosoftEntraProviderMapping,
 22    MicrosoftEntraProviderUser,
 23)
 24from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
 25from authentik.events.models import Event, EventAction
 26from authentik.lib.generators import generate_id
 27from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
 28from authentik.tenants.models import Tenant
 29
 30
 31class MicrosoftEntraGroupTests(TestCase):
 32    """Microsoft Entra Group tests"""
 33
 34    @apply_blueprint("system/providers-microsoft-entra.yaml")
 35    def setUp(self) -> None:
 36        # Delete all groups and groups as the mocked HTTP responses only return one ID
 37        # which will cause errors with multiple groups
 38        Tenant.objects.update(avatars="none")
 39        User.objects.all().exclude_anonymous().delete()
 40        Group.objects.all().delete()
 41        self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
 42            name=generate_id(),
 43            client_id=generate_id(),
 44            client_secret=generate_id(),
 45            tenant_id=generate_id(),
 46            exclude_users_service_account=True,
 47        )
 48        self.app: Application = Application.objects.create(
 49            name=generate_id(),
 50            slug=generate_id(),
 51        )
 52        self.app.backchannel_providers.add(self.provider)
 53        self.provider.property_mappings.add(
 54            MicrosoftEntraProviderMapping.objects.get(
 55                managed="goauthentik.io/providers/microsoft_entra/user"
 56            )
 57        )
 58        self.provider.property_mappings_group.add(
 59            MicrosoftEntraProviderMapping.objects.get(
 60                managed="goauthentik.io/providers/microsoft_entra/group"
 61            )
 62        )
 63        self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())
 64
 65    def test_group_create(self):
 66        """Test group creation"""
 67        uid = generate_id()
 68        with (
 69            patch(
 70                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
 71                MagicMock(return_value={"credentials": self.creds}),
 72            ),
 73            patch(
 74                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
 75                AsyncMock(
 76                    return_value=OrganizationCollectionResponse(
 77                        value=[
 78                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
 79                        ]
 80                    )
 81                ),
 82            ),
 83            patch(
 84                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
 85                AsyncMock(return_value=MSGroup(id=generate_id())),
 86            ) as group_create,
 87        ):
 88            group = Group.objects.create(name=uid)
 89            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
 90                provider=self.provider, group=group
 91            ).first()
 92            self.assertIsNotNone(microsoft_group)
 93            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
 94            group_create.assert_called_once()
 95
 96    def test_group_not_created(self):
 97        """Test without group property mappings, no group is created"""
 98        self.provider.property_mappings_group.clear()
 99        uid = generate_id()
100        with (
101            patch(
102                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
103                MagicMock(return_value={"credentials": self.creds}),
104            ),
105            patch(
106                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
107                AsyncMock(
108                    return_value=OrganizationCollectionResponse(
109                        value=[
110                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
111                        ]
112                    )
113                ),
114            ),
115            patch(
116                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
117                AsyncMock(return_value=MSGroup(id=generate_id())),
118            ) as group_create,
119        ):
120            group = Group.objects.create(name=uid)
121            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
122                provider=self.provider, group=group
123            ).first()
124            self.assertIsNone(microsoft_group)
125            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
126            group_create.assert_not_called()
127
128    def test_group_create_update(self):
129        """Test group updating"""
130        uid = generate_id()
131        ext_id = generate_id()
132        with (
133            patch(
134                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
135                MagicMock(return_value={"credentials": self.creds}),
136            ),
137            patch(
138                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
139                AsyncMock(
140                    return_value=OrganizationCollectionResponse(
141                        value=[
142                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
143                        ]
144                    )
145                ),
146            ),
147            patch(
148                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
149                AsyncMock(return_value=MSGroup(id=ext_id)),
150            ) as group_create,
151            patch(
152                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
153                AsyncMock(return_value=MSGroup(id=ext_id)),
154            ) as group_patch,
155        ):
156            group = Group.objects.create(name=uid)
157            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
158                provider=self.provider, group=group
159            ).first()
160            self.assertIsNotNone(microsoft_group)
161
162            group.name = "new name"
163            group.save()
164            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
165            group_create.assert_called_once()
166            group_patch.assert_called_once()
167
168    def test_group_create_delete(self):
169        """Test group deletion"""
170        uid = generate_id()
171        ext_id = generate_id()
172        with (
173            patch(
174                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
175                AsyncMock(
176                    return_value=OrganizationCollectionResponse(
177                        value=[
178                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
179                        ]
180                    )
181                ),
182            ),
183            patch(
184                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
185                MagicMock(return_value={"credentials": self.creds}),
186            ),
187            patch(
188                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
189                AsyncMock(return_value=MSGroup(id=ext_id)),
190            ) as group_create,
191            patch(
192                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
193                AsyncMock(return_value=MSGroup(id=ext_id)),
194            ) as group_delete,
195        ):
196            group = Group.objects.create(name=uid)
197            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
198                provider=self.provider, group=group
199            ).first()
200            self.assertIsNotNone(microsoft_group)
201
202            group.delete()
203            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
204            group_create.assert_called_once()
205            group_delete.assert_called_once()
206
207    def test_group_create_member_add(self):
208        """Test group creation"""
209        uid = generate_id()
210        with (
211            patch(
212                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
213                MagicMock(return_value={"credentials": self.creds}),
214            ),
215            patch(
216                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
217                AsyncMock(
218                    return_value=OrganizationCollectionResponse(
219                        value=[
220                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
221                        ]
222                    )
223                ),
224            ),
225            patch(
226                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
227                AsyncMock(return_value=MSUser(id=generate_id())),
228            ) as user_create,
229            patch(
230                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
231                AsyncMock(return_value=MSUser(id=generate_id())),
232            ),
233            patch(
234                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
235                AsyncMock(return_value=MSGroup(id=uid)),
236            ) as group_create,
237            patch(
238                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
239                AsyncMock(),
240            ) as member_add,
241        ):
242            user = create_test_user(uid)
243            group = Group.objects.create(name=uid)
244            group.users.add(user)
245            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
246                provider=self.provider, group=group
247            ).first()
248            self.assertIsNotNone(microsoft_group)
249            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
250            user_create.assert_called_once()
251            group_create.assert_called_once()
252            member_add.assert_called_once()
253            self.assertEqual(
254                member_add.call_args[0][0].odata_id,
255                f"https://graph.microsoft.com/v1.0/directoryObjects/{
256                    MicrosoftEntraProviderUser.objects.filter(
257                        provider=self.provider,
258                    )
259                    .first()
260                    .microsoft_id
261                }",
262            )
263
264    def test_group_create_member_remove(self):
265        """Test group creation"""
266        uid = generate_id()
267        with (
268            patch(
269                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
270                MagicMock(return_value={"credentials": self.creds}),
271            ),
272            patch(
273                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
274                AsyncMock(
275                    return_value=OrganizationCollectionResponse(
276                        value=[
277                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
278                        ]
279                    )
280                ),
281            ),
282            patch(
283                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
284                AsyncMock(return_value=MSUser(id=generate_id())),
285            ) as user_create,
286            patch(
287                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
288                AsyncMock(return_value=MSUser(id=generate_id())),
289            ),
290            patch(
291                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
292                AsyncMock(return_value=MSGroup(id=uid)),
293            ) as group_create,
294            patch(
295                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
296                AsyncMock(),
297            ) as member_add,
298            patch(
299                "msgraph.generated.groups.item.members.item.ref.ref_request_builder.RefRequestBuilder.delete",
300                AsyncMock(),
301            ) as member_remove,
302        ):
303            user = create_test_user(uid)
304            group = Group.objects.create(name=uid)
305            group.users.add(user)
306            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
307                provider=self.provider, group=group
308            ).first()
309            self.assertIsNotNone(microsoft_group)
310            group.users.remove(user)
311
312            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
313            user_create.assert_called_once()
314            group_create.assert_called_once()
315            member_add.assert_called_once()
316            self.assertEqual(
317                member_add.call_args[0][0].odata_id,
318                f"https://graph.microsoft.com/v1.0/directoryObjects/{
319                    MicrosoftEntraProviderUser.objects.filter(
320                        provider=self.provider,
321                    )
322                    .first()
323                    .microsoft_id
324                }",
325            )
326            member_remove.assert_called_once()
327
328    def test_group_create_delete_do_nothing(self):
329        """Test group deletion (delete action = do nothing)"""
330        self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
331        self.provider.save()
332        uid = generate_id()
333        with (
334            patch(
335                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
336                MagicMock(return_value={"credentials": self.creds}),
337            ),
338            patch(
339                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
340                AsyncMock(
341                    return_value=OrganizationCollectionResponse(
342                        value=[
343                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
344                        ]
345                    )
346                ),
347            ),
348            patch(
349                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
350                AsyncMock(return_value=MSGroup(id=uid)),
351            ) as group_create,
352            patch(
353                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
354                AsyncMock(return_value=MSGroup(id=uid)),
355            ) as group_delete,
356        ):
357            group = Group.objects.create(name=uid)
358            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
359                provider=self.provider, group=group
360            ).first()
361            self.assertIsNotNone(microsoft_group)
362
363            group.delete()
364            self.assertFalse(
365                MicrosoftEntraProviderGroup.objects.filter(
366                    provider=self.provider, group__name=uid
367                ).exists()
368            )
369            group_create.assert_called_once()
370            group_delete.assert_not_called()
371
372    def test_sync_discover(self):
373        """Test group discovery"""
374        uid = generate_id()
375        self.app.backchannel_providers.remove(self.provider)
376        different_group = Group.objects.create(
377            name=uid,
378        )
379        self.app.backchannel_providers.add(self.provider)
380        with (
381            patch(
382                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
383                MagicMock(return_value={"credentials": self.creds}),
384            ),
385            patch(
386                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
387                AsyncMock(
388                    return_value=OrganizationCollectionResponse(
389                        value=[
390                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
391                        ]
392                    )
393                ),
394            ),
395            patch(
396                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
397                AsyncMock(return_value=MSUser(id=generate_id())),
398            ),
399            patch(
400                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
401                AsyncMock(return_value=MSGroup(id=generate_id())),
402            ),
403            patch(
404                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
405                AsyncMock(return_value=MSGroup(id=uid)),
406            ),
407            patch(
408                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
409                AsyncMock(
410                    return_value=UserCollectionResponse(
411                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
412                    )
413                ),
414            ) as user_list,
415            patch(
416                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
417                AsyncMock(
418                    return_value=GroupCollectionResponse(
419                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
420                    )
421                ),
422            ) as group_list,
423        ):
424            microsoft_entra_sync.send(self.provider.pk).get_result()
425            self.assertTrue(
426                MicrosoftEntraProviderGroup.objects.filter(
427                    group=different_group, provider=self.provider
428                ).exists()
429            )
430            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
431            user_list.assert_called_once()
432            group_list.assert_called_once()
433
434    def test_sync_discover_multiple(self):
435        """Test group discovery"""
436        uid = generate_id()
437        self.app.backchannel_providers.remove(self.provider)
438        different_group = Group.objects.create(
439            name=uid,
440        )
441        self.app.backchannel_providers.add(self.provider)
442        with (
443            patch(
444                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
445                MagicMock(return_value={"credentials": self.creds}),
446            ),
447            patch(
448                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
449                AsyncMock(
450                    return_value=OrganizationCollectionResponse(
451                        value=[
452                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
453                        ]
454                    )
455                ),
456            ),
457            patch(
458                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
459                AsyncMock(return_value=MSUser(id=generate_id())),
460            ),
461            patch(
462                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
463                AsyncMock(return_value=MSGroup(id=generate_id())),
464            ),
465            patch(
466                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
467                AsyncMock(return_value=MSGroup(id=uid)),
468            ),
469            patch(
470                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
471                AsyncMock(
472                    return_value=UserCollectionResponse(
473                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
474                    )
475                ),
476            ) as user_list,
477            patch(
478                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
479                AsyncMock(
480                    return_value=GroupCollectionResponse(
481                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
482                    )
483                ),
484            ) as group_list,
485        ):
486            microsoft_entra_sync.send(self.provider.pk).get_result()
487            self.assertTrue(
488                MicrosoftEntraProviderGroup.objects.filter(
489                    group=different_group, provider=self.provider
490                ).exists()
491            )
492            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
493            user_list.assert_called_once()
494            group_list.assert_called_once()
495
496            with patch(
497                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
498                AsyncMock(
499                    return_value=GroupCollectionResponse(
500                        value=[
501                            MSGroup(display_name=uid, unique_name=uid, id=uid, description="foo")
502                        ]
503                    )
504                ),
505            ) as mod_group_list:
506                microsoft_entra_sync.send(self.provider.pk).get_result()
507                self.assertTrue(
508                    MicrosoftEntraProviderGroup.objects.filter(
509                        group=different_group, provider=self.provider
510                    ).exists()
511                )
512                self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
513                mod_group_list.assert_called_once()
class MicrosoftEntraGroupTests(django.test.testcases.TestCase):
 32class MicrosoftEntraGroupTests(TestCase):
 33    """Microsoft Entra Group tests"""
 34
 35    @apply_blueprint("system/providers-microsoft-entra.yaml")
 36    def setUp(self) -> None:
 37        # Delete all groups and groups as the mocked HTTP responses only return one ID
 38        # which will cause errors with multiple groups
 39        Tenant.objects.update(avatars="none")
 40        User.objects.all().exclude_anonymous().delete()
 41        Group.objects.all().delete()
 42        self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
 43            name=generate_id(),
 44            client_id=generate_id(),
 45            client_secret=generate_id(),
 46            tenant_id=generate_id(),
 47            exclude_users_service_account=True,
 48        )
 49        self.app: Application = Application.objects.create(
 50            name=generate_id(),
 51            slug=generate_id(),
 52        )
 53        self.app.backchannel_providers.add(self.provider)
 54        self.provider.property_mappings.add(
 55            MicrosoftEntraProviderMapping.objects.get(
 56                managed="goauthentik.io/providers/microsoft_entra/user"
 57            )
 58        )
 59        self.provider.property_mappings_group.add(
 60            MicrosoftEntraProviderMapping.objects.get(
 61                managed="goauthentik.io/providers/microsoft_entra/group"
 62            )
 63        )
 64        self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())
 65
 66    def test_group_create(self):
 67        """Test group creation"""
 68        uid = generate_id()
 69        with (
 70            patch(
 71                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
 72                MagicMock(return_value={"credentials": self.creds}),
 73            ),
 74            patch(
 75                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
 76                AsyncMock(
 77                    return_value=OrganizationCollectionResponse(
 78                        value=[
 79                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
 80                        ]
 81                    )
 82                ),
 83            ),
 84            patch(
 85                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
 86                AsyncMock(return_value=MSGroup(id=generate_id())),
 87            ) as group_create,
 88        ):
 89            group = Group.objects.create(name=uid)
 90            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
 91                provider=self.provider, group=group
 92            ).first()
 93            self.assertIsNotNone(microsoft_group)
 94            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
 95            group_create.assert_called_once()
 96
 97    def test_group_not_created(self):
 98        """Test without group property mappings, no group is created"""
 99        self.provider.property_mappings_group.clear()
100        uid = generate_id()
101        with (
102            patch(
103                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
104                MagicMock(return_value={"credentials": self.creds}),
105            ),
106            patch(
107                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
108                AsyncMock(
109                    return_value=OrganizationCollectionResponse(
110                        value=[
111                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
112                        ]
113                    )
114                ),
115            ),
116            patch(
117                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
118                AsyncMock(return_value=MSGroup(id=generate_id())),
119            ) as group_create,
120        ):
121            group = Group.objects.create(name=uid)
122            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
123                provider=self.provider, group=group
124            ).first()
125            self.assertIsNone(microsoft_group)
126            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
127            group_create.assert_not_called()
128
129    def test_group_create_update(self):
130        """Test group updating"""
131        uid = generate_id()
132        ext_id = generate_id()
133        with (
134            patch(
135                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
136                MagicMock(return_value={"credentials": self.creds}),
137            ),
138            patch(
139                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
140                AsyncMock(
141                    return_value=OrganizationCollectionResponse(
142                        value=[
143                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
144                        ]
145                    )
146                ),
147            ),
148            patch(
149                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
150                AsyncMock(return_value=MSGroup(id=ext_id)),
151            ) as group_create,
152            patch(
153                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
154                AsyncMock(return_value=MSGroup(id=ext_id)),
155            ) as group_patch,
156        ):
157            group = Group.objects.create(name=uid)
158            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
159                provider=self.provider, group=group
160            ).first()
161            self.assertIsNotNone(microsoft_group)
162
163            group.name = "new name"
164            group.save()
165            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
166            group_create.assert_called_once()
167            group_patch.assert_called_once()
168
169    def test_group_create_delete(self):
170        """Test group deletion"""
171        uid = generate_id()
172        ext_id = generate_id()
173        with (
174            patch(
175                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
176                AsyncMock(
177                    return_value=OrganizationCollectionResponse(
178                        value=[
179                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
180                        ]
181                    )
182                ),
183            ),
184            patch(
185                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
186                MagicMock(return_value={"credentials": self.creds}),
187            ),
188            patch(
189                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
190                AsyncMock(return_value=MSGroup(id=ext_id)),
191            ) as group_create,
192            patch(
193                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
194                AsyncMock(return_value=MSGroup(id=ext_id)),
195            ) as group_delete,
196        ):
197            group = Group.objects.create(name=uid)
198            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
199                provider=self.provider, group=group
200            ).first()
201            self.assertIsNotNone(microsoft_group)
202
203            group.delete()
204            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
205            group_create.assert_called_once()
206            group_delete.assert_called_once()
207
208    def test_group_create_member_add(self):
209        """Test group creation"""
210        uid = generate_id()
211        with (
212            patch(
213                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
214                MagicMock(return_value={"credentials": self.creds}),
215            ),
216            patch(
217                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
218                AsyncMock(
219                    return_value=OrganizationCollectionResponse(
220                        value=[
221                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
222                        ]
223                    )
224                ),
225            ),
226            patch(
227                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
228                AsyncMock(return_value=MSUser(id=generate_id())),
229            ) as user_create,
230            patch(
231                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
232                AsyncMock(return_value=MSUser(id=generate_id())),
233            ),
234            patch(
235                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
236                AsyncMock(return_value=MSGroup(id=uid)),
237            ) as group_create,
238            patch(
239                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
240                AsyncMock(),
241            ) as member_add,
242        ):
243            user = create_test_user(uid)
244            group = Group.objects.create(name=uid)
245            group.users.add(user)
246            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
247                provider=self.provider, group=group
248            ).first()
249            self.assertIsNotNone(microsoft_group)
250            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
251            user_create.assert_called_once()
252            group_create.assert_called_once()
253            member_add.assert_called_once()
254            self.assertEqual(
255                member_add.call_args[0][0].odata_id,
256                f"https://graph.microsoft.com/v1.0/directoryObjects/{
257                    MicrosoftEntraProviderUser.objects.filter(
258                        provider=self.provider,
259                    )
260                    .first()
261                    .microsoft_id
262                }",
263            )
264
265    def test_group_create_member_remove(self):
266        """Test group creation"""
267        uid = generate_id()
268        with (
269            patch(
270                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
271                MagicMock(return_value={"credentials": self.creds}),
272            ),
273            patch(
274                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
275                AsyncMock(
276                    return_value=OrganizationCollectionResponse(
277                        value=[
278                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
279                        ]
280                    )
281                ),
282            ),
283            patch(
284                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
285                AsyncMock(return_value=MSUser(id=generate_id())),
286            ) as user_create,
287            patch(
288                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
289                AsyncMock(return_value=MSUser(id=generate_id())),
290            ),
291            patch(
292                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
293                AsyncMock(return_value=MSGroup(id=uid)),
294            ) as group_create,
295            patch(
296                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
297                AsyncMock(),
298            ) as member_add,
299            patch(
300                "msgraph.generated.groups.item.members.item.ref.ref_request_builder.RefRequestBuilder.delete",
301                AsyncMock(),
302            ) as member_remove,
303        ):
304            user = create_test_user(uid)
305            group = Group.objects.create(name=uid)
306            group.users.add(user)
307            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
308                provider=self.provider, group=group
309            ).first()
310            self.assertIsNotNone(microsoft_group)
311            group.users.remove(user)
312
313            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
314            user_create.assert_called_once()
315            group_create.assert_called_once()
316            member_add.assert_called_once()
317            self.assertEqual(
318                member_add.call_args[0][0].odata_id,
319                f"https://graph.microsoft.com/v1.0/directoryObjects/{
320                    MicrosoftEntraProviderUser.objects.filter(
321                        provider=self.provider,
322                    )
323                    .first()
324                    .microsoft_id
325                }",
326            )
327            member_remove.assert_called_once()
328
329    def test_group_create_delete_do_nothing(self):
330        """Test group deletion (delete action = do nothing)"""
331        self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
332        self.provider.save()
333        uid = generate_id()
334        with (
335            patch(
336                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
337                MagicMock(return_value={"credentials": self.creds}),
338            ),
339            patch(
340                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
341                AsyncMock(
342                    return_value=OrganizationCollectionResponse(
343                        value=[
344                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
345                        ]
346                    )
347                ),
348            ),
349            patch(
350                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
351                AsyncMock(return_value=MSGroup(id=uid)),
352            ) as group_create,
353            patch(
354                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
355                AsyncMock(return_value=MSGroup(id=uid)),
356            ) as group_delete,
357        ):
358            group = Group.objects.create(name=uid)
359            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
360                provider=self.provider, group=group
361            ).first()
362            self.assertIsNotNone(microsoft_group)
363
364            group.delete()
365            self.assertFalse(
366                MicrosoftEntraProviderGroup.objects.filter(
367                    provider=self.provider, group__name=uid
368                ).exists()
369            )
370            group_create.assert_called_once()
371            group_delete.assert_not_called()
372
373    def test_sync_discover(self):
374        """Test group discovery"""
375        uid = generate_id()
376        self.app.backchannel_providers.remove(self.provider)
377        different_group = Group.objects.create(
378            name=uid,
379        )
380        self.app.backchannel_providers.add(self.provider)
381        with (
382            patch(
383                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
384                MagicMock(return_value={"credentials": self.creds}),
385            ),
386            patch(
387                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
388                AsyncMock(
389                    return_value=OrganizationCollectionResponse(
390                        value=[
391                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
392                        ]
393                    )
394                ),
395            ),
396            patch(
397                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
398                AsyncMock(return_value=MSUser(id=generate_id())),
399            ),
400            patch(
401                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
402                AsyncMock(return_value=MSGroup(id=generate_id())),
403            ),
404            patch(
405                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
406                AsyncMock(return_value=MSGroup(id=uid)),
407            ),
408            patch(
409                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
410                AsyncMock(
411                    return_value=UserCollectionResponse(
412                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
413                    )
414                ),
415            ) as user_list,
416            patch(
417                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
418                AsyncMock(
419                    return_value=GroupCollectionResponse(
420                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
421                    )
422                ),
423            ) as group_list,
424        ):
425            microsoft_entra_sync.send(self.provider.pk).get_result()
426            self.assertTrue(
427                MicrosoftEntraProviderGroup.objects.filter(
428                    group=different_group, provider=self.provider
429                ).exists()
430            )
431            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
432            user_list.assert_called_once()
433            group_list.assert_called_once()
434
435    def test_sync_discover_multiple(self):
436        """Test group discovery"""
437        uid = generate_id()
438        self.app.backchannel_providers.remove(self.provider)
439        different_group = Group.objects.create(
440            name=uid,
441        )
442        self.app.backchannel_providers.add(self.provider)
443        with (
444            patch(
445                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
446                MagicMock(return_value={"credentials": self.creds}),
447            ),
448            patch(
449                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
450                AsyncMock(
451                    return_value=OrganizationCollectionResponse(
452                        value=[
453                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
454                        ]
455                    )
456                ),
457            ),
458            patch(
459                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
460                AsyncMock(return_value=MSUser(id=generate_id())),
461            ),
462            patch(
463                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
464                AsyncMock(return_value=MSGroup(id=generate_id())),
465            ),
466            patch(
467                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
468                AsyncMock(return_value=MSGroup(id=uid)),
469            ),
470            patch(
471                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
472                AsyncMock(
473                    return_value=UserCollectionResponse(
474                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
475                    )
476                ),
477            ) as user_list,
478            patch(
479                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
480                AsyncMock(
481                    return_value=GroupCollectionResponse(
482                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
483                    )
484                ),
485            ) as group_list,
486        ):
487            microsoft_entra_sync.send(self.provider.pk).get_result()
488            self.assertTrue(
489                MicrosoftEntraProviderGroup.objects.filter(
490                    group=different_group, provider=self.provider
491                ).exists()
492            )
493            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
494            user_list.assert_called_once()
495            group_list.assert_called_once()
496
497            with patch(
498                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
499                AsyncMock(
500                    return_value=GroupCollectionResponse(
501                        value=[
502                            MSGroup(display_name=uid, unique_name=uid, id=uid, description="foo")
503                        ]
504                    )
505                ),
506            ) as mod_group_list:
507                microsoft_entra_sync.send(self.provider.pk).get_result()
508                self.assertTrue(
509                    MicrosoftEntraProviderGroup.objects.filter(
510                        group=different_group, provider=self.provider
511                    ).exists()
512                )
513                self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
514                mod_group_list.assert_called_once()

Microsoft Entra Group tests

@apply_blueprint('system/providers-microsoft-entra.yaml')
def setUp(self) -> None:
35    @apply_blueprint("system/providers-microsoft-entra.yaml")
36    def setUp(self) -> None:
37        # Delete all groups and groups as the mocked HTTP responses only return one ID
38        # which will cause errors with multiple groups
39        Tenant.objects.update(avatars="none")
40        User.objects.all().exclude_anonymous().delete()
41        Group.objects.all().delete()
42        self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
43            name=generate_id(),
44            client_id=generate_id(),
45            client_secret=generate_id(),
46            tenant_id=generate_id(),
47            exclude_users_service_account=True,
48        )
49        self.app: Application = Application.objects.create(
50            name=generate_id(),
51            slug=generate_id(),
52        )
53        self.app.backchannel_providers.add(self.provider)
54        self.provider.property_mappings.add(
55            MicrosoftEntraProviderMapping.objects.get(
56                managed="goauthentik.io/providers/microsoft_entra/user"
57            )
58        )
59        self.provider.property_mappings_group.add(
60            MicrosoftEntraProviderMapping.objects.get(
61                managed="goauthentik.io/providers/microsoft_entra/group"
62            )
63        )
64        self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())

Hook method for setting up the test fixture before exercising it.

def test_group_create(self):
66    def test_group_create(self):
67        """Test group creation"""
68        uid = generate_id()
69        with (
70            patch(
71                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
72                MagicMock(return_value={"credentials": self.creds}),
73            ),
74            patch(
75                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
76                AsyncMock(
77                    return_value=OrganizationCollectionResponse(
78                        value=[
79                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
80                        ]
81                    )
82                ),
83            ),
84            patch(
85                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
86                AsyncMock(return_value=MSGroup(id=generate_id())),
87            ) as group_create,
88        ):
89            group = Group.objects.create(name=uid)
90            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
91                provider=self.provider, group=group
92            ).first()
93            self.assertIsNotNone(microsoft_group)
94            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
95            group_create.assert_called_once()

Test group creation

def test_group_not_created(self):
 97    def test_group_not_created(self):
 98        """Test without group property mappings, no group is created"""
 99        self.provider.property_mappings_group.clear()
100        uid = generate_id()
101        with (
102            patch(
103                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
104                MagicMock(return_value={"credentials": self.creds}),
105            ),
106            patch(
107                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
108                AsyncMock(
109                    return_value=OrganizationCollectionResponse(
110                        value=[
111                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
112                        ]
113                    )
114                ),
115            ),
116            patch(
117                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
118                AsyncMock(return_value=MSGroup(id=generate_id())),
119            ) as group_create,
120        ):
121            group = Group.objects.create(name=uid)
122            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
123                provider=self.provider, group=group
124            ).first()
125            self.assertIsNone(microsoft_group)
126            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
127            group_create.assert_not_called()

Test without group property mappings, no group is created

def test_group_create_update(self):
129    def test_group_create_update(self):
130        """Test group updating"""
131        uid = generate_id()
132        ext_id = generate_id()
133        with (
134            patch(
135                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
136                MagicMock(return_value={"credentials": self.creds}),
137            ),
138            patch(
139                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
140                AsyncMock(
141                    return_value=OrganizationCollectionResponse(
142                        value=[
143                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
144                        ]
145                    )
146                ),
147            ),
148            patch(
149                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
150                AsyncMock(return_value=MSGroup(id=ext_id)),
151            ) as group_create,
152            patch(
153                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
154                AsyncMock(return_value=MSGroup(id=ext_id)),
155            ) as group_patch,
156        ):
157            group = Group.objects.create(name=uid)
158            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
159                provider=self.provider, group=group
160            ).first()
161            self.assertIsNotNone(microsoft_group)
162
163            group.name = "new name"
164            group.save()
165            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
166            group_create.assert_called_once()
167            group_patch.assert_called_once()

Test group updating

def test_group_create_delete(self):
169    def test_group_create_delete(self):
170        """Test group deletion"""
171        uid = generate_id()
172        ext_id = generate_id()
173        with (
174            patch(
175                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
176                AsyncMock(
177                    return_value=OrganizationCollectionResponse(
178                        value=[
179                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
180                        ]
181                    )
182                ),
183            ),
184            patch(
185                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
186                MagicMock(return_value={"credentials": self.creds}),
187            ),
188            patch(
189                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
190                AsyncMock(return_value=MSGroup(id=ext_id)),
191            ) as group_create,
192            patch(
193                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
194                AsyncMock(return_value=MSGroup(id=ext_id)),
195            ) as group_delete,
196        ):
197            group = Group.objects.create(name=uid)
198            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
199                provider=self.provider, group=group
200            ).first()
201            self.assertIsNotNone(microsoft_group)
202
203            group.delete()
204            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
205            group_create.assert_called_once()
206            group_delete.assert_called_once()

Test group deletion

def test_group_create_member_add(self):
208    def test_group_create_member_add(self):
209        """Test group creation"""
210        uid = generate_id()
211        with (
212            patch(
213                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
214                MagicMock(return_value={"credentials": self.creds}),
215            ),
216            patch(
217                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
218                AsyncMock(
219                    return_value=OrganizationCollectionResponse(
220                        value=[
221                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
222                        ]
223                    )
224                ),
225            ),
226            patch(
227                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
228                AsyncMock(return_value=MSUser(id=generate_id())),
229            ) as user_create,
230            patch(
231                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
232                AsyncMock(return_value=MSUser(id=generate_id())),
233            ),
234            patch(
235                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
236                AsyncMock(return_value=MSGroup(id=uid)),
237            ) as group_create,
238            patch(
239                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
240                AsyncMock(),
241            ) as member_add,
242        ):
243            user = create_test_user(uid)
244            group = Group.objects.create(name=uid)
245            group.users.add(user)
246            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
247                provider=self.provider, group=group
248            ).first()
249            self.assertIsNotNone(microsoft_group)
250            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
251            user_create.assert_called_once()
252            group_create.assert_called_once()
253            member_add.assert_called_once()
254            self.assertEqual(
255                member_add.call_args[0][0].odata_id,
256                f"https://graph.microsoft.com/v1.0/directoryObjects/{
257                    MicrosoftEntraProviderUser.objects.filter(
258                        provider=self.provider,
259                    )
260                    .first()
261                    .microsoft_id
262                }",
263            )

Test group creation

def test_group_create_member_remove(self):
265    def test_group_create_member_remove(self):
266        """Test group creation"""
267        uid = generate_id()
268        with (
269            patch(
270                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
271                MagicMock(return_value={"credentials": self.creds}),
272            ),
273            patch(
274                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
275                AsyncMock(
276                    return_value=OrganizationCollectionResponse(
277                        value=[
278                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
279                        ]
280                    )
281                ),
282            ),
283            patch(
284                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
285                AsyncMock(return_value=MSUser(id=generate_id())),
286            ) as user_create,
287            patch(
288                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
289                AsyncMock(return_value=MSUser(id=generate_id())),
290            ),
291            patch(
292                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
293                AsyncMock(return_value=MSGroup(id=uid)),
294            ) as group_create,
295            patch(
296                "msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
297                AsyncMock(),
298            ) as member_add,
299            patch(
300                "msgraph.generated.groups.item.members.item.ref.ref_request_builder.RefRequestBuilder.delete",
301                AsyncMock(),
302            ) as member_remove,
303        ):
304            user = create_test_user(uid)
305            group = Group.objects.create(name=uid)
306            group.users.add(user)
307            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
308                provider=self.provider, group=group
309            ).first()
310            self.assertIsNotNone(microsoft_group)
311            group.users.remove(user)
312
313            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
314            user_create.assert_called_once()
315            group_create.assert_called_once()
316            member_add.assert_called_once()
317            self.assertEqual(
318                member_add.call_args[0][0].odata_id,
319                f"https://graph.microsoft.com/v1.0/directoryObjects/{
320                    MicrosoftEntraProviderUser.objects.filter(
321                        provider=self.provider,
322                    )
323                    .first()
324                    .microsoft_id
325                }",
326            )
327            member_remove.assert_called_once()

Test group creation

def test_group_create_delete_do_nothing(self):
329    def test_group_create_delete_do_nothing(self):
330        """Test group deletion (delete action = do nothing)"""
331        self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
332        self.provider.save()
333        uid = generate_id()
334        with (
335            patch(
336                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
337                MagicMock(return_value={"credentials": self.creds}),
338            ),
339            patch(
340                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
341                AsyncMock(
342                    return_value=OrganizationCollectionResponse(
343                        value=[
344                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
345                        ]
346                    )
347                ),
348            ),
349            patch(
350                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
351                AsyncMock(return_value=MSGroup(id=uid)),
352            ) as group_create,
353            patch(
354                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
355                AsyncMock(return_value=MSGroup(id=uid)),
356            ) as group_delete,
357        ):
358            group = Group.objects.create(name=uid)
359            microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
360                provider=self.provider, group=group
361            ).first()
362            self.assertIsNotNone(microsoft_group)
363
364            group.delete()
365            self.assertFalse(
366                MicrosoftEntraProviderGroup.objects.filter(
367                    provider=self.provider, group__name=uid
368                ).exists()
369            )
370            group_create.assert_called_once()
371            group_delete.assert_not_called()

Test group deletion (delete action = do nothing)

def test_sync_discover(self):
373    def test_sync_discover(self):
374        """Test group discovery"""
375        uid = generate_id()
376        self.app.backchannel_providers.remove(self.provider)
377        different_group = Group.objects.create(
378            name=uid,
379        )
380        self.app.backchannel_providers.add(self.provider)
381        with (
382            patch(
383                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
384                MagicMock(return_value={"credentials": self.creds}),
385            ),
386            patch(
387                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
388                AsyncMock(
389                    return_value=OrganizationCollectionResponse(
390                        value=[
391                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
392                        ]
393                    )
394                ),
395            ),
396            patch(
397                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
398                AsyncMock(return_value=MSUser(id=generate_id())),
399            ),
400            patch(
401                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
402                AsyncMock(return_value=MSGroup(id=generate_id())),
403            ),
404            patch(
405                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
406                AsyncMock(return_value=MSGroup(id=uid)),
407            ),
408            patch(
409                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
410                AsyncMock(
411                    return_value=UserCollectionResponse(
412                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
413                    )
414                ),
415            ) as user_list,
416            patch(
417                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
418                AsyncMock(
419                    return_value=GroupCollectionResponse(
420                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
421                    )
422                ),
423            ) as group_list,
424        ):
425            microsoft_entra_sync.send(self.provider.pk).get_result()
426            self.assertTrue(
427                MicrosoftEntraProviderGroup.objects.filter(
428                    group=different_group, provider=self.provider
429                ).exists()
430            )
431            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
432            user_list.assert_called_once()
433            group_list.assert_called_once()

Test group discovery

def test_sync_discover_multiple(self):
435    def test_sync_discover_multiple(self):
436        """Test group discovery"""
437        uid = generate_id()
438        self.app.backchannel_providers.remove(self.provider)
439        different_group = Group.objects.create(
440            name=uid,
441        )
442        self.app.backchannel_providers.add(self.provider)
443        with (
444            patch(
445                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
446                MagicMock(return_value={"credentials": self.creds}),
447            ),
448            patch(
449                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
450                AsyncMock(
451                    return_value=OrganizationCollectionResponse(
452                        value=[
453                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
454                        ]
455                    )
456                ),
457            ),
458            patch(
459                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
460                AsyncMock(return_value=MSUser(id=generate_id())),
461            ),
462            patch(
463                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
464                AsyncMock(return_value=MSGroup(id=generate_id())),
465            ),
466            patch(
467                "msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
468                AsyncMock(return_value=MSGroup(id=uid)),
469            ),
470            patch(
471                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
472                AsyncMock(
473                    return_value=UserCollectionResponse(
474                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
475                    )
476                ),
477            ) as user_list,
478            patch(
479                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
480                AsyncMock(
481                    return_value=GroupCollectionResponse(
482                        value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
483                    )
484                ),
485            ) as group_list,
486        ):
487            microsoft_entra_sync.send(self.provider.pk).get_result()
488            self.assertTrue(
489                MicrosoftEntraProviderGroup.objects.filter(
490                    group=different_group, provider=self.provider
491                ).exists()
492            )
493            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
494            user_list.assert_called_once()
495            group_list.assert_called_once()
496
497            with patch(
498                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
499                AsyncMock(
500                    return_value=GroupCollectionResponse(
501                        value=[
502                            MSGroup(display_name=uid, unique_name=uid, id=uid, description="foo")
503                        ]
504                    )
505                ),
506            ) as mod_group_list:
507                microsoft_entra_sync.send(self.provider.pk).get_result()
508                self.assertTrue(
509                    MicrosoftEntraProviderGroup.objects.filter(
510                        group=different_group, provider=self.provider
511                    ).exists()
512                )
513                self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
514                mod_group_list.assert_called_once()

Test group discovery