enterprise/providers/microsoft_entra: initial account sync to microsoft entra (#9632)
* initial Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add entra mappings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make API endpoints more consistent Signed-off-by: Jens Langhammer <jens@goauthentik.io> * implement more things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix most group tests + fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more group tests, fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing __init__ Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ui for provisioned users Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix a bunch of bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add `creating` to property mapping env Signed-off-by: Jens Langhammer <jens@goauthentik.io> * always sync group members Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix group membership Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some types Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add group member add test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * create sync status component to dedupe Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix discovery tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * get rid of more code and fix more issues Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add error handling for auth and transient Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make sure autoretry is on Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format web Signed-off-by: Jens Langhammer <jens@goauthentik.io> * wait for task in signal Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add squashed google migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -0,0 +1,337 @@
|
||||
"""Microsoft Entra User tests"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from azure.identity.aio import ClientSecretCredential
|
||||
from django.test import TestCase
|
||||
from msgraph.generated.models.group_collection_response import GroupCollectionResponse
|
||||
from msgraph.generated.models.organization import Organization
|
||||
from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
|
||||
from msgraph.generated.models.user import User as MSUser
|
||||
from msgraph.generated.models.user_collection_response import UserCollectionResponse
|
||||
from msgraph.generated.models.verified_domain import VerifiedDomain
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.enterprise.providers.microsoft_entra.models import (
|
||||
MicrosoftEntraProvider,
|
||||
MicrosoftEntraProviderMapping,
|
||||
MicrosoftEntraProviderUser,
|
||||
)
|
||||
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class MicrosoftEntraUserTests(TestCase):
|
||||
"""Microsoft Entra User tests"""
|
||||
|
||||
@apply_blueprint("system/providers-microsoft-entra.yaml")
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||
# which will cause errors with multiple users
|
||||
Tenant.objects.update(avatars="none")
|
||||
User.objects.all().exclude_anonymous().delete()
|
||||
Group.objects.all().delete()
|
||||
self.provider: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_id(),
|
||||
tenant_id=generate_id(),
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.add(
|
||||
MicrosoftEntraProviderMapping.objects.get(
|
||||
managed="goauthentik.io/providers/microsoft_entra/user"
|
||||
)
|
||||
)
|
||||
self.provider.property_mappings_group.add(
|
||||
MicrosoftEntraProviderMapping.objects.get(
|
||||
managed="goauthentik.io/providers/microsoft_entra/group"
|
||||
)
|
||||
)
|
||||
self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())
|
||||
|
||||
def test_user_create(self):
|
||||
"""Test user creation"""
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNotNone(microsoft_user)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_called_once()
|
||||
|
||||
def test_user_create_update(self):
|
||||
"""Test user updating"""
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_patch,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNotNone(microsoft_user)
|
||||
|
||||
user.name = "new name"
|
||||
user.save()
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_called_once()
|
||||
user_patch.assert_called_once()
|
||||
|
||||
def test_user_create_delete(self):
|
||||
"""Test user deletion"""
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
|
||||
AsyncMock(),
|
||||
) as user_delete,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNotNone(microsoft_user)
|
||||
|
||||
user.delete()
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_called_once()
|
||||
user_delete.assert_called_once()
|
||||
|
||||
def test_user_create_delete_suspend(self):
|
||||
"""Test user deletion (delete action = Suspend)"""
|
||||
self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND
|
||||
self.provider.save()
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_patch,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
|
||||
AsyncMock(),
|
||||
) as user_delete,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNotNone(microsoft_user)
|
||||
|
||||
user.delete()
|
||||
self.assertFalse(
|
||||
MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user__username=uid
|
||||
).exists()
|
||||
)
|
||||
user_create.assert_called_once()
|
||||
user_patch.assert_called_once()
|
||||
self.assertFalse(user_patch.call_args[0][0].account_enabled)
|
||||
user_delete.assert_not_called()
|
||||
|
||||
def test_user_create_delete_do_nothing(self):
|
||||
"""Test user deletion (delete action = do nothing)"""
|
||||
self.provider.user_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
|
||||
self.provider.save()
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_patch,
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
|
||||
AsyncMock(),
|
||||
) as user_delete,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNotNone(microsoft_user)
|
||||
|
||||
user.delete()
|
||||
self.assertFalse(
|
||||
MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user__username=uid
|
||||
).exists()
|
||||
)
|
||||
user_create.assert_called_once()
|
||||
user_patch.assert_not_called()
|
||||
user_delete.assert_not_called()
|
||||
|
||||
def test_sync_task(self):
|
||||
"""Test user discovery"""
|
||||
uid = generate_id()
|
||||
self.app.backchannel_providers.remove(self.provider)
|
||||
different_user = User.objects.create(
|
||||
username=uid,
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=UserCollectionResponse(
|
||||
value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
|
||||
)
|
||||
),
|
||||
) as user_list,
|
||||
patch(
|
||||
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
|
||||
AsyncMock(return_value=GroupCollectionResponse(value=[])),
|
||||
),
|
||||
):
|
||||
microsoft_entra_sync.delay(self.provider.pk).get()
|
||||
self.assertTrue(
|
||||
MicrosoftEntraProviderUser.objects.filter(
|
||||
user=different_user, provider=self.provider
|
||||
).exists()
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_list.assert_called_once()
|
||||
Reference in New Issue
Block a user