From 1bf49747adaefcb19db66274478f6933342112c1 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:01:51 +0100 Subject: [PATCH] chore(entra): enhance performance for `user_registration_details` and user `mfa` evaluation (#9236) --- .../providers/azure/authentication.mdx | 6 +- prowler/CHANGELOG.md | 1 + .../entra_non_privileged_user_has_mfa.py | 2 +- .../entra_privileged_user_has_mfa.py | 2 +- .../azure/services/entra/entra_service.py | 52 +++++++++------ .../entra_user_with_vm_access_has_mfa.py | 2 +- .../m365/services/entra/entra_service.py | 51 ++++++++++----- .../entra_non_privileged_user_has_mfa_test.py | 18 ++---- .../entra_privileged_user_has_mfa_test.py | 18 ++---- .../services/entra/entra_service_test.py | 63 ++++++++++--------- .../entra_user_with_vm_access_has_mfa_test.py | 33 ++-------- .../entra/microsoft365_entra_service_test.py | 49 ++++++++++++++- 12 files changed, 171 insertions(+), 126 deletions(-) diff --git a/docs/user-guide/providers/azure/authentication.mdx b/docs/user-guide/providers/azure/authentication.mdx index 57746e2d60..d1abb023f7 100644 --- a/docs/user-guide/providers/azure/authentication.mdx +++ b/docs/user-guide/providers/azure/authentication.mdx @@ -30,6 +30,7 @@ Assign the following Microsoft Graph permissions: - `Directory.Read.All` - `Policy.Read.All` - `UserAuthenticationMethod.Read.All` (optional, for multifactor authentication (MFA) checks) +- `AuditLog.Read.All` (optional, for multifactor authentication (MFA) checks) Replace `Directory.Read.All` with `Domain.Read.All` for more restrictive permissions. Note that Entra checks related to DirectoryRoles and GetUsers will not run with this permission. @@ -51,6 +52,7 @@ Replace `Directory.Read.All` with `Domain.Read.All` for more restrictive permiss - `Directory.Read.All` - `Policy.Read.All` - `UserAuthenticationMethod.Read.All` + - `AuditLog.Read.All` ![Permission Screenshots](/images/providers/domain-permission.png) @@ -62,7 +64,7 @@ Replace `Directory.Read.All` with `Domain.Read.All` for more restrictive permiss 1. To grant permissions to a Service Principal, execute the following command in a terminal: ```console - az ad app permission add --id {appId} --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role 246dd0d5-5bd0-4def-940b-0421030a5b68=Role 38d9df27-64da-44fd-b7c5-a6fbac20248f=Role + az ad app permission add --id {appId} --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role 246dd0d5-5bd0-4def-940b-0421030a5b68=Role 38d9df27-64da-44fd-b7c5-a6fbac20248f=Role b0afded3-3588-46d8-8b3d-9842eff778da=Role ``` @@ -375,7 +377,7 @@ The ProwlerRole is a custom role required for specific security checks. First, c #### Step 4: (Optional) Assign Microsoft Graph Permissions -For Entra ID (Azure AD) checks, the Managed Identity needs Microsoft Graph API permissions: `Directory.Read.All`, `Policy.Read.All`, and optionally `UserAuthenticationMethod.Read.All`. +For Entra ID (Azure AD) checks, the Managed Identity needs Microsoft Graph API permissions: `Directory.Read.All`, `Policy.Read.All`, and optionally `UserAuthenticationMethod.Read.All` and `AuditLog.Read.All`. Assigning Microsoft Graph API permissions to a Managed Identity requires Azure CLI or PowerShell - it cannot be done through the Azure Portal's standard role assignment interface. diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c1db3dc0af..dca740ab56 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -49,6 +49,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update AWS RDS service metadata to new format [(#9551)](https://github.com/prowler-cloud/prowler/pull/9551) - Update AWS Bedrock service metadata to new format [(#8827)](https://github.com/prowler-cloud/prowler/pull/8827) - Update AWS IAM service metadata to new format [(#9550)](https://github.com/prowler-cloud/prowler/pull/9550) +- Enhance `user_registration_details` perfomance and user `mfa` evaluation [(#9236)](https://github.com/prowler-cloud/prowler/pull/9236) - Update AWS Cognito service metadata to new format [(#8853)](https://github.com/prowler-cloud/prowler/pull/8853) --- diff --git a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py index 706f912a82..c86fc02da7 100644 --- a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py @@ -21,7 +21,7 @@ class entra_non_privileged_user_has_mfa(Check): f"Non-privileged user {user.name} does not have MFA." ) - if len(user.authentication_methods) > 1: + if user.is_mfa_capable: report.status = "PASS" report.status_extended = ( f"Non-privileged user {user.name} has MFA." diff --git a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py index 5605ba4a83..c8c625f927 100644 --- a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py @@ -21,7 +21,7 @@ class entra_privileged_user_has_mfa(Check): f"Privileged user {user.name} does not have MFA." ) - if len(user.authentication_methods) > 1: + if user.is_mfa_capable: report.status = "PASS" report.status_extended = f"Privileged user {user.name} has MFA." diff --git a/prowler/providers/azure/services/entra/entra_service.py b/prowler/providers/azure/services/entra/entra_service.py index 841283d42a..035b864e5e 100644 --- a/prowler/providers/azure/services/entra/entra_service.py +++ b/prowler/providers/azure/services/entra/entra_service.py @@ -66,6 +66,7 @@ class Entra(AzureService): for tenant, client in self.clients.items(): users.update({tenant: {}}) users_response = await client.users.get() + registration_details = await self._get_user_registration_details(client) try: while users_response: @@ -75,19 +76,9 @@ class Entra(AzureService): user.id: User( id=user.id, name=user.display_name, - authentication_methods=[ - AuthMethod( - id=auth_method.id, - type=getattr( - auth_method, "odata_type", None - ), - ) - for auth_method in ( - await client.users.by_user_id( - user.id - ).authentication.methods.get() - ).value - ], + is_mfa_capable=registration_details.get( + user.id, False + ), ) } ) @@ -116,6 +107,34 @@ class Entra(AzureService): return users + async def _get_user_registration_details(self, client): + registration_details = {} + try: + registration_builder = ( + client.reports.authentication_methods.user_registration_details + ) + registration_response = await registration_builder.get() + + while registration_response: + for detail in getattr(registration_response, "value", []) or []: + registration_details.update( + {detail.id: getattr(detail, "is_mfa_capable", False)} + ) + + next_link = getattr(registration_response, "odata_next_link", None) + if not next_link: + break + registration_response = await registration_builder.with_url( + next_link + ).get() + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return registration_details + async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") @@ -391,15 +410,10 @@ class Entra(AzureService): return conditional_access_policy -class AuthMethod(BaseModel): - id: str - type: str - - class User(BaseModel): id: str name: str - authentication_methods: List[AuthMethod] = [] + is_mfa_capable: bool = False class DefaultUserRolePermissions(BaseModel): diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py index ad3b6819ae..2c6e53d153 100644 --- a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py @@ -43,7 +43,7 @@ class entra_user_with_vm_access_has_mfa(Check): report.subscription = subscription_name report.status = "FAIL" report.status_extended = f"User {user.name} without MFA can access VMs in subscription {subscription_name}" - if len(user.authentication_methods) > 1: + if user.is_mfa_capable: report.status = "PASS" report.status_extended = f"User {user.name} can access VMs in subscription {subscription_name} but it has MFA." diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index e7751fdef9..4addbd07a2 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -402,18 +402,7 @@ class Entra(M365Service): for member in members: user_roles_map.setdefault(member.id, []).append(role_template_id) - try: - registration_details_list = ( - await self.client.reports.authentication_methods.user_registration_details.get() - ) - registration_details = { - detail.id: detail for detail in registration_details_list.value - } - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - registration_details = {} + registration_details = await self._get_user_registration_details() while users_response: for user in getattr(users_response, "value", []) or []: @@ -424,11 +413,7 @@ class Entra(M365Service): True if (user.on_premises_sync_enabled) else False ), directory_roles_ids=user_roles_map.get(user.id, []), - is_mfa_capable=( - registration_details.get(user.id, {}).is_mfa_capable - if registration_details.get(user.id, None) is not None - else False - ), + is_mfa_capable=(registration_details.get(user.id, False)), account_enabled=not self.user_accounts_status.get( user.id, {} ).get("AccountDisabled", False), @@ -444,6 +429,38 @@ class Entra(M365Service): ) return users + async def _get_user_registration_details(self): + registration_details = {} + try: + registration_builder = ( + self.client.reports.authentication_methods.user_registration_details + ) + registration_response = await registration_builder.get() + + while registration_response: + for detail in getattr(registration_response, "value", []) or []: + registration_details.update( + {detail.id: getattr(detail, "is_mfa_capable", False)} + ) + + next_link = getattr(registration_response, "odata_next_link", None) + if not next_link: + break + registration_response = await registration_builder.with_url( + next_link + ).get() + + except Exception as error: + if ( + error.__class__.__name__ == "ODataError" + and error.__dict__.get("response_status_code", None) == 403 + ): + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return registration_details + class ConditionalAccessPolicyState(Enum): ENABLED = "enabled" diff --git a/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py b/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py index 84dab8985b..4667b665ed 100644 --- a/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py @@ -69,7 +69,6 @@ class Test_entra_non_privileged_user_has_mfa: entra_non_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -77,7 +76,7 @@ class Test_entra_non_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[AuthMethod(id=str(uuid4()), type="foo")], + is_mfa_capable=False, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -117,7 +116,6 @@ class Test_entra_non_privileged_user_has_mfa: entra_non_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -125,10 +123,7 @@ class Test_entra_non_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="foo"), - AuthMethod(id=str(uuid4()), type="bar"), - ], + is_mfa_capable=True, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -165,7 +160,6 @@ class Test_entra_non_privileged_user_has_mfa: entra_non_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -173,7 +167,7 @@ class Test_entra_non_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[AuthMethod(id=str(uuid4()), type="foo")], + is_mfa_capable=False, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -207,7 +201,6 @@ class Test_entra_non_privileged_user_has_mfa: entra_non_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -215,10 +208,7 @@ class Test_entra_non_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="foo"), - AuthMethod(id=str(uuid4()), type="bar"), - ], + is_mfa_capable=True, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} diff --git a/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py b/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py index 51ffba58da..31e0a57bff 100644 --- a/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py @@ -69,7 +69,6 @@ class Test_entra_privileged_user_has_mfa: entra_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -77,7 +76,7 @@ class Test_entra_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[AuthMethod(id=str(uuid4()), type="foo")], + is_mfa_capable=False, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -109,7 +108,6 @@ class Test_entra_privileged_user_has_mfa: entra_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -117,10 +115,7 @@ class Test_entra_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="foo"), - AuthMethod(id=str(uuid4()), type="bar"), - ], + is_mfa_capable=True, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -152,7 +147,6 @@ class Test_entra_privileged_user_has_mfa: entra_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -160,7 +154,7 @@ class Test_entra_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[AuthMethod(id=str(uuid4()), type="foo")], + is_mfa_capable=False, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} @@ -199,7 +193,6 @@ class Test_entra_privileged_user_has_mfa: entra_privileged_user_has_mfa, ) from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, DirectoryRole, User, ) @@ -207,10 +200,7 @@ class Test_entra_privileged_user_has_mfa: user = User( id=user_id, name="foo", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="foo"), - AuthMethod(id=str(uuid4()), type="bar"), - ], + is_mfa_capable=True, ) entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} diff --git a/tests/providers/azure/services/entra/entra_service_test.py b/tests/providers/azure/services/entra/entra_service_test.py index 85f9a3c558..b038d075b1 100644 --- a/tests/providers/azure/services/entra/entra_service_test.py +++ b/tests/providers/azure/services/entra/entra_service_test.py @@ -145,10 +145,7 @@ class Test_Entra_Service: assert len(entra_client.users) == 1 assert entra_client.users[DOMAIN]["user-1@tenant1.es"].id == "id-1" assert entra_client.users[DOMAIN]["user-1@tenant1.es"].name == "User 1" - assert ( - len(entra_client.users[DOMAIN]["user-1@tenant1.es"].authentication_methods) - == 0 - ) + assert entra_client.users[DOMAIN]["user-1@tenant1.es"].is_mfa_capable is False def test_get_authorization_policy(self): entra_client = Entra(set_mocked_azure_provider()) @@ -251,38 +248,48 @@ def test_azure_entra__get_users_handles_pagination(): ) with_url_mock = MagicMock(return_value=users_with_url_builder) - def by_user_id_side_effect(user_id): - auth_methods_response = SimpleNamespace( - value=[ - SimpleNamespace( - id=f"{user_id}-method", - odata_type="#microsoft.graph.passwordAuthenticationMethod", - ) - ] - ) - return SimpleNamespace( - authentication=SimpleNamespace( - methods=SimpleNamespace( - get=AsyncMock(return_value=auth_methods_response) - ) - ) - ) - users_builder = SimpleNamespace( get=AsyncMock(return_value=users_response_page_one), with_url=with_url_mock, - by_user_id=MagicMock(side_effect=by_user_id_side_effect), ) - entra_service.clients = {"tenant-1": SimpleNamespace(users=users_builder)} + registration_details_response = SimpleNamespace( + value=[ + SimpleNamespace( + id="user-1", + is_mfa_capable=True, + ), + SimpleNamespace( + id="user-2", + is_mfa_capable=True, + ), + ], + odata_next_link=None, + ) + + registration_details_builder = SimpleNamespace( + get=AsyncMock(return_value=registration_details_response), + with_url=MagicMock(), + ) + + entra_service.clients = { + "tenant-1": SimpleNamespace( + users=users_builder, + reports=SimpleNamespace( + authentication_methods=SimpleNamespace( + user_registration_details=registration_details_builder + ) + ), + ) + } users = asyncio.run(entra_service._get_users()) assert len(users["tenant-1"]) == 3 assert users_builder.get.await_count == 1 with_url_mock.assert_called_once_with("next-link") - assert users["tenant-1"]["user-1"].authentication_methods[0].id == "user-1-method" - assert ( - users["tenant-1"]["user-3"].authentication_methods[0].type - == "#microsoft.graph.passwordAuthenticationMethod" - ) + registration_details_builder.get.assert_awaited() + registration_details_builder.with_url.assert_not_called() + assert users["tenant-1"]["user-1"].is_mfa_capable is True + assert users["tenant-1"]["user-2"].is_mfa_capable is True + assert users["tenant-1"]["user-3"].is_mfa_capable is False diff --git a/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py index a02995c45b..b9ebe959ef 100644 --- a/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py @@ -61,10 +61,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: new=entra_client, ), ): - from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, - User, - ) + from prowler.providers.azure.services.entra.entra_service import User from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( entra_user_with_vm_access_has_mfa, ) @@ -90,12 +87,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: f"test@{DOMAIN}": User( id=user_id, name="test", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="Password"), - AuthMethod( - id=str(uuid4()), type="MicrosoftAuthenticator" - ), - ], + is_mfa_capable=True, ) } } @@ -138,10 +130,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: new=entra_client, ), ): - from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, - User, - ) + from prowler.providers.azure.services.entra.entra_service import User from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( entra_user_with_vm_access_has_mfa, ) @@ -167,9 +156,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: f"test@{DOMAIN}": User( id=user_id, name="test", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="Password"), - ], + is_mfa_capable=False, ) } } @@ -264,10 +251,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: new=entra_client, ), ): - from prowler.providers.azure.services.entra.entra_service import ( - AuthMethod, - User, - ) + from prowler.providers.azure.services.entra.entra_service import User from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( entra_user_with_vm_access_has_mfa, ) @@ -293,12 +277,7 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa: f"test@{DOMAIN}": User( id=user_id, name="test", - authentication_methods=[ - AuthMethod(id=str(uuid4()), type="Password"), - AuthMethod( - id=str(uuid4()), type="MicrosoftAuthenticator" - ), - ], + is_mfa_capable=True, ) } } diff --git a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py index 9848507b93..b56939c2cb 100644 --- a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py @@ -401,10 +401,14 @@ class Test_Entra_Service: value=[ SimpleNamespace(id="user-1", is_mfa_capable=True), SimpleNamespace(id="user-6", is_mfa_capable=True), - ] + ], + odata_next_link=None, ) registration_details_builder = SimpleNamespace( - get=AsyncMock(return_value=registration_details_response) + get=AsyncMock(return_value=registration_details_response), + with_url=MagicMock( + return_value=SimpleNamespace(get=AsyncMock(return_value=None)) + ), ) reports_builder = SimpleNamespace( authentication_methods=SimpleNamespace( @@ -429,3 +433,44 @@ class Test_Entra_Service: assert users["user-6"].account_enabled is False assert users["user-1"].is_mfa_capable is True assert users["user-2"].is_mfa_capable is False + + def test__get_user_registration_details_handles_pagination(self): + entra_service = Entra.__new__(Entra) + + registration_response_page_one = SimpleNamespace( + value=[ + SimpleNamespace(id="user-1", is_mfa_capable=True), + ], + odata_next_link="next-link", + ) + registration_response_page_two = SimpleNamespace( + value=[ + SimpleNamespace(id="user-2", is_mfa_capable=False), + ], + odata_next_link=None, + ) + + registration_builder_next = SimpleNamespace( + get=AsyncMock(return_value=registration_response_page_two) + ) + registration_builder = SimpleNamespace( + get=AsyncMock(return_value=registration_response_page_one), + with_url=MagicMock(return_value=registration_builder_next), + ) + + entra_service.client = SimpleNamespace( + reports=SimpleNamespace( + authentication_methods=SimpleNamespace( + user_registration_details=registration_builder + ) + ) + ) + + registration_details = asyncio.run( + entra_service._get_user_registration_details() + ) + + assert registration_details == {"user-1": True, "user-2": False} + registration_builder.get.assert_awaited() + registration_builder.with_url.assert_called_once_with("next-link") + registration_builder_next.get.assert_awaited()