authentik.enterprise.providers.microsoft_entra.tests.test_users

Microsoft Entra User tests

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

Microsoft Entra User tests

@apply_blueprint('system/providers-microsoft-entra.yaml')
def setUp(self) -> None:
34    @apply_blueprint("system/providers-microsoft-entra.yaml")
35    def setUp(self) -> None:
36        # Delete all users and groups as the mocked HTTP responses only return one ID
37        # which will cause errors with multiple users
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())

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

def test_user_create(self):
65    def test_user_create(self):
66        """Test user 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.users.users_request_builder.UsersRequestBuilder.post",
85                AsyncMock(return_value=MSUser(id=generate_id())),
86            ) as user_create,
87        ):
88            user = User.objects.create(
89                username=uid,
90                name=f"{uid} {uid}",
91                email=f"{uid}@goauthentik.io",
92            )
93            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
94                provider=self.provider, user=user
95            ).first()
96            self.assertIsNotNone(microsoft_user)
97            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
98            user_create.assert_called_once()

Test user creation

def test_user_create_dry_run(self):
100    def test_user_create_dry_run(self):
101        """Test user creation (dry run)"""
102        self.provider.dry_run = True
103        self.provider.save()
104        uid = generate_id()
105        with (
106            patch(
107                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
108                MagicMock(return_value={"credentials": self.creds}),
109            ),
110            patch(
111                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
112                AsyncMock(
113                    return_value=OrganizationCollectionResponse(
114                        value=[
115                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
116                        ]
117                    )
118                ),
119            ),
120        ):
121            user = User.objects.create(
122                username=uid,
123                name=f"{uid} {uid}",
124                email=f"{uid}@goauthentik.io",
125            )
126            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
127                provider=self.provider, user=user
128            ).first()
129            self.assertIsNone(microsoft_user)
130            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())

Test user creation (dry run)

def test_user_not_created(self):
132    def test_user_not_created(self):
133        """Test without property mappings, no group is created"""
134        self.provider.property_mappings.clear()
135        uid = generate_id()
136        with (
137            patch(
138                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
139                MagicMock(return_value={"credentials": self.creds}),
140            ),
141            patch(
142                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
143                AsyncMock(
144                    return_value=OrganizationCollectionResponse(
145                        value=[
146                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
147                        ]
148                    )
149                ),
150            ),
151            patch(
152                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
153                AsyncMock(return_value=MSUser(id=generate_id())),
154            ) as user_create,
155        ):
156            user = User.objects.create(
157                username=uid,
158                name=f"{uid} {uid}",
159                email=f"{uid}@goauthentik.io",
160            )
161            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
162                provider=self.provider, user=user
163            ).first()
164            self.assertIsNone(microsoft_user)
165            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
166            user_create.assert_not_called()

Test without property mappings, no group is created

def test_user_create_update(self):
168    def test_user_create_update(self):
169        """Test user updating"""
170        uid = generate_id()
171        with (
172            patch(
173                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
174                MagicMock(return_value={"credentials": self.creds}),
175            ),
176            patch(
177                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
178                AsyncMock(
179                    return_value=OrganizationCollectionResponse(
180                        value=[
181                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
182                        ]
183                    )
184                ),
185            ),
186            patch(
187                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
188                AsyncMock(return_value=MSUser(id=generate_id())),
189            ) as user_create,
190            patch(
191                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
192                AsyncMock(return_value=MSUser(id=generate_id())),
193            ) as user_patch,
194        ):
195            user = User.objects.create(
196                username=uid,
197                name=f"{uid} {uid}",
198                email=f"{uid}@goauthentik.io",
199            )
200            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
201                provider=self.provider, user=user
202            ).first()
203            self.assertIsNotNone(microsoft_user)
204
205            user.name = "new name"
206            user.save()
207            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
208            user_create.assert_called_once()
209            user_patch.assert_called_once()

Test user updating

def test_user_create_delete(self):
211    def test_user_create_delete(self):
212        """Test user deletion"""
213        uid = generate_id()
214        with (
215            patch(
216                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
217                MagicMock(return_value={"credentials": self.creds}),
218            ),
219            patch(
220                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
221                AsyncMock(
222                    return_value=OrganizationCollectionResponse(
223                        value=[
224                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
225                        ]
226                    )
227                ),
228            ),
229            patch(
230                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
231                AsyncMock(return_value=MSUser(id=generate_id())),
232            ) as user_create,
233            patch(
234                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
235                AsyncMock(),
236            ) as user_delete,
237        ):
238            user = User.objects.create(
239                username=uid,
240                name=f"{uid} {uid}",
241                email=f"{uid}@goauthentik.io",
242            )
243            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
244                provider=self.provider, user=user
245            ).first()
246            self.assertIsNotNone(microsoft_user)
247
248            user.delete()
249            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
250            user_create.assert_called_once()
251            user_delete.assert_called_once()

Test user deletion

def test_user_create_delete_suspend(self):
253    def test_user_create_delete_suspend(self):
254        """Test user deletion (delete action = Suspend)"""
255        self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND
256        self.provider.save()
257        uid = generate_id()
258        with (
259            patch(
260                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
261                MagicMock(return_value={"credentials": self.creds}),
262            ),
263            patch(
264                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
265                AsyncMock(
266                    return_value=OrganizationCollectionResponse(
267                        value=[
268                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
269                        ]
270                    )
271                ),
272            ),
273            patch(
274                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
275                AsyncMock(return_value=MSUser(id=generate_id())),
276            ) as user_create,
277            patch(
278                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
279                AsyncMock(return_value=MSUser(id=generate_id())),
280            ) as user_patch,
281            patch(
282                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
283                AsyncMock(),
284            ) as user_delete,
285        ):
286            user = User.objects.create(
287                username=uid,
288                name=f"{uid} {uid}",
289                email=f"{uid}@goauthentik.io",
290            )
291            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
292                provider=self.provider, user=user
293            ).first()
294            self.assertIsNotNone(microsoft_user)
295
296            user.delete()
297            self.assertFalse(
298                MicrosoftEntraProviderUser.objects.filter(
299                    provider=self.provider, user__username=uid
300                ).exists()
301            )
302            user_create.assert_called_once()
303            user_patch.assert_called_once()
304            self.assertFalse(user_patch.call_args[0][0].account_enabled)
305            user_delete.assert_not_called()

Test user deletion (delete action = Suspend)

def test_user_create_delete_do_nothing(self):
307    def test_user_create_delete_do_nothing(self):
308        """Test user deletion (delete action = do nothing)"""
309        self.provider.user_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
310        self.provider.save()
311        uid = generate_id()
312        with (
313            patch(
314                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
315                MagicMock(return_value={"credentials": self.creds}),
316            ),
317            patch(
318                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
319                AsyncMock(
320                    return_value=OrganizationCollectionResponse(
321                        value=[
322                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
323                        ]
324                    )
325                ),
326            ),
327            patch(
328                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
329                AsyncMock(return_value=MSUser(id=generate_id())),
330            ) as user_create,
331            patch(
332                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
333                AsyncMock(return_value=MSUser(id=generate_id())),
334            ) as user_patch,
335            patch(
336                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
337                AsyncMock(),
338            ) as user_delete,
339        ):
340            user = User.objects.create(
341                username=uid,
342                name=f"{uid} {uid}",
343                email=f"{uid}@goauthentik.io",
344            )
345            microsoft_user = MicrosoftEntraProviderUser.objects.filter(
346                provider=self.provider, user=user
347            ).first()
348            self.assertIsNotNone(microsoft_user)
349
350            user.delete()
351            self.assertFalse(
352                MicrosoftEntraProviderUser.objects.filter(
353                    provider=self.provider, user__username=uid
354                ).exists()
355            )
356            user_create.assert_called_once()
357            user_patch.assert_not_called()
358            user_delete.assert_not_called()

Test user deletion (delete action = do nothing)

def test_sync_discover(self):
360    def test_sync_discover(self):
361        """Test user discovery"""
362        uid = generate_id()
363        self.app.backchannel_providers.remove(self.provider)
364        different_user = User.objects.create(
365            username=uid,
366            email=f"{uid}@goauthentik.io",
367        )
368        self.app.backchannel_providers.add(self.provider)
369        with (
370            patch(
371                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
372                MagicMock(return_value={"credentials": self.creds}),
373            ),
374            patch(
375                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
376                AsyncMock(
377                    return_value=OrganizationCollectionResponse(
378                        value=[
379                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
380                        ]
381                    )
382                ),
383            ),
384            patch(
385                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
386                AsyncMock(return_value=MSUser(id=generate_id())),
387            ),
388            patch(
389                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
390                AsyncMock(
391                    return_value=UserCollectionResponse(
392                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
393                    )
394                ),
395            ) as user_list,
396            patch(
397                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
398                AsyncMock(return_value=GroupCollectionResponse(value=[])),
399            ),
400        ):
401            microsoft_entra_sync.send(self.provider.pk).get_result()
402            self.assertTrue(
403                MicrosoftEntraProviderUser.objects.filter(
404                    user=different_user, provider=self.provider
405                ).exists()
406            )
407            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
408            user_list.assert_called_once()

Test user discovery

def test_sync_discover_multiple(self):
410    def test_sync_discover_multiple(self):
411        """Test user discovery (multiple times)"""
412        uid = generate_id()
413        self.app.backchannel_providers.remove(self.provider)
414        different_user = User.objects.create(
415            username=uid,
416            email=f"{uid}@goauthentik.io",
417        )
418        self.app.backchannel_providers.add(self.provider)
419        with (
420            patch(
421                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
422                MagicMock(return_value={"credentials": self.creds}),
423            ),
424            patch(
425                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
426                AsyncMock(
427                    return_value=OrganizationCollectionResponse(
428                        value=[
429                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
430                        ]
431                    )
432                ),
433            ),
434            patch(
435                "msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
436                AsyncMock(return_value=MSUser(id=generate_id())),
437            ),
438            patch(
439                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
440                AsyncMock(
441                    return_value=UserCollectionResponse(
442                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
443                    )
444                ),
445            ) as user_list,
446            patch(
447                "msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
448                AsyncMock(return_value=GroupCollectionResponse(value=[])),
449            ),
450        ):
451            microsoft_entra_sync.send(self.provider.pk).get_result()
452            self.assertTrue(
453                MicrosoftEntraProviderUser.objects.filter(
454                    user=different_user, provider=self.provider
455                ).exists()
456            )
457            self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
458            user_list.assert_called_once()
459
460            with patch(
461                "msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
462                AsyncMock(
463                    return_value=UserCollectionResponse(
464                        value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid, about_me="foo")]
465                    )
466                ),
467            ) as mod_user_list:
468                microsoft_entra_sync.send(self.provider.pk).get_result()
469                self.assertTrue(
470                    MicrosoftEntraProviderUser.objects.filter(
471                        user=different_user, provider=self.provider
472                    ).exists()
473                )
474                self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
475                mod_user_list.assert_called_once()

Test user discovery (multiple times)

def test_connect_manual(self):
477    def test_connect_manual(self):
478        """test manual user connection"""
479        uid = generate_id()
480        self.app.backchannel_providers.remove(self.provider)
481        admin = create_test_admin_user()
482        different_user = User.objects.create(
483            username=uid,
484            email=f"{uid}@goauthentik.io",
485        )
486        self.app.backchannel_providers.add(self.provider)
487        with (
488            patch(
489                "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
490                MagicMock(return_value={"credentials": self.creds}),
491            ),
492            patch(
493                "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
494                AsyncMock(
495                    return_value=OrganizationCollectionResponse(
496                        value=[
497                            Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
498                        ]
499                    )
500                ),
501            ),
502            patch(
503                "authentik.enterprise.providers.microsoft_entra.clients.users.MicrosoftEntraUserClient.update_single_attribute",
504                MagicMock(),
505            ) as user_get,
506        ):
507            self.client.force_login(admin)
508            response = self.client.post(
509                reverse("authentik_api:microsoftentraprovideruser-list"),
510                data={
511                    "microsoft_id": generate_id(),
512                    "user": different_user.pk,
513                    "provider": self.provider.pk,
514                },
515            )
516            self.assertEqual(response.status_code, 201)
517            user_get.assert_called_once()

test manual user connection