feat(m365): add CIS M365 v7.0.0 entra authentication method, PIM and access review checks (#12155)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
Pedro Martín
2026-08-10 15:37:48 +01:00
committed by GitHub
co-authored by Daniel Barranquero Hugo P.Brito
parent 356036fe1f
commit ce037318cd
29 changed files with 1934 additions and 7 deletions
@@ -0,0 +1,147 @@
from unittest import mock
import pytest
from prowler.providers.m365.services.entra.entra_service import (
AccessReviewDefinition,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_access_review_guest_users_configured.entra_access_review_guest_users_configured"
def _definition(
status="InProgress",
scope_query="/users?$filter=(userType eq 'Guest')",
principal_scope_queries=None,
default_decision="Deny",
default_decision_enabled=True,
auto_apply_enabled=True,
mail_notifications_enabled=True,
reminders_enabled=True,
recurrence_pattern_type="weekly",
recurrence_range_type="noEnd",
has_primary_reviewers=True,
):
definition = {
"id": "ar1",
"display_name": "Guest Review",
"status": status,
"scope_query": scope_query,
"principal_scope_queries": principal_scope_queries or [],
"default_decision": default_decision,
"auto_apply_enabled": auto_apply_enabled,
"mail_notifications_enabled": mail_notifications_enabled,
"reminders_enabled": reminders_enabled,
"recurrence_pattern_type": recurrence_pattern_type,
"recurrence_range_type": recurrence_range_type,
"has_primary_reviewers": has_primary_reviewers,
}
if default_decision_enabled is not None:
definition["default_decision_enabled"] = default_decision_enabled
return AccessReviewDefinition(**definition)
class Test_entra_access_review_guest_users_configured:
def _run(self, definitions):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_access_review_guest_users_configured.entra_access_review_guest_users_configured import (
entra_access_review_guest_users_configured,
)
entra_client.access_review_definitions = definitions
return entra_access_review_guest_users_configured().execute()
def test_no_definitions(self):
result = self._run([])
assert result[0].status == "FAIL"
def test_active_failclosed_guest_review(self):
result = self._run([_definition()])
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Access review 'Guest Review' for guest users is active, recurring, "
"reviewer-assigned, and fail-closed."
)
def test_not_active(self):
result = self._run([_definition(status="Completed")])
assert result[0].status == "FAIL"
def test_not_fail_closed(self):
# Active guest review but does nothing on non-response -> FAIL.
result = self._run(
[_definition(default_decision="None", auto_apply_enabled=False)]
)
assert result[0].status == "FAIL"
@pytest.mark.parametrize("default_decision_enabled", [False, None])
def test_default_decision_not_enabled(self, default_decision_enabled):
result = self._run(
[_definition(default_decision_enabled=default_decision_enabled)]
)
assert result[0].status == "FAIL"
def test_not_guest_scope(self):
result = self._run([_definition(scope_query="/roleManagement/directory")])
assert result[0].status == "FAIL"
def test_guest_filter_in_principal_scopes(self):
# Portal-created reviews keep the guest filter in principalScopes and
# leave the top-level scope query empty.
result = self._run(
[
_definition(
scope_query="",
principal_scope_queries=["/users?$filter=(userType eq 'Guest')"],
)
]
)
assert result[0].status == "PASS"
def test_url_encoded_guest_filter(self):
result = self._run(
[_definition(scope_query="/users?$filter=userType%20eq%20%27Guest%27")]
)
assert result[0].status == "PASS"
def test_mixed_case_guest_filter(self):
result = self._run(
[_definition(scope_query="/users?$filter=(USERTYPE EQ 'guest')")]
)
assert result[0].status == "PASS"
@pytest.mark.parametrize(
"scope_query",
[
"/users?$filter=not(userType eq 'Guest')",
"/users?$filter=(userType ne 'Guest')",
"/users?$filter=displayName eq 'Guest account'",
"/groups/guest-review-members",
],
)
def test_incidental_guest_text_does_not_target_guests(self, scope_query):
result = self._run([_definition(scope_query=scope_query)])
assert result[0].status == "FAIL"
@pytest.mark.parametrize(
"definition_overrides",
[
{"recurrence_pattern_type": None},
{"recurrence_pattern_type": "daily"},
{"recurrence_range_type": None},
{"recurrence_range_type": "endDate"},
{"has_primary_reviewers": False},
],
)
def test_invalid_recurrence_or_missing_reviewers(self, definition_overrides):
result = self._run([_definition(**definition_overrides)])
assert result[0].status == "FAIL"
@@ -0,0 +1,97 @@
from unittest import mock
import pytest
from prowler.providers.m365.services.entra.entra_service import (
AccessReviewDefinition,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_access_review_privileged_roles_configured.entra_access_review_privileged_roles_configured"
def _definition(
status="InProgress",
scope_query="",
resource_scope_queries=None,
default_decision="None",
auto_apply_enabled=True,
mail_notifications_enabled=True,
reminders_enabled=True,
recurrence_pattern_type="absoluteMonthly",
recurrence_range_type="noEnd",
has_primary_reviewers=True,
):
return AccessReviewDefinition(
id="ar1",
display_name="Privileged Roles Review",
status=status,
scope_query=scope_query,
resource_scope_queries=(
resource_scope_queries
if resource_scope_queries is not None
else ["/roleManagement/directory/roleDefinitions/62e90394-..."]
),
default_decision=default_decision,
auto_apply_enabled=auto_apply_enabled,
mail_notifications_enabled=mail_notifications_enabled,
reminders_enabled=reminders_enabled,
recurrence_pattern_type=recurrence_pattern_type,
recurrence_range_type=recurrence_range_type,
has_primary_reviewers=has_primary_reviewers,
)
class Test_entra_access_review_privileged_roles_configured:
def _run(self, definitions):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_access_review_privileged_roles_configured.entra_access_review_privileged_roles_configured import (
entra_access_review_privileged_roles_configured,
)
entra_client.access_review_definitions = definitions
return entra_access_review_privileged_roles_configured().execute()
def test_no_definitions(self):
assert self._run([])[0].status == "FAIL"
def test_active_fail_closed_privileged(self):
# Role reference lives in resource scopes (not top-level scope.query).
result = self._run([_definition()])
assert result[0].status == "PASS"
def test_deny_default_decision(self):
result = self._run([_definition(default_decision="Deny")])
assert result[0].status == "FAIL"
def test_guest_review_ignored(self):
result = self._run(
[
_definition(
scope_query="/users?$filter=(userType eq 'Guest')",
resource_scope_queries=[],
)
]
)
assert result[0].status == "FAIL"
@pytest.mark.parametrize(
"definition_overrides",
[
{"recurrence_pattern_type": None},
{"recurrence_pattern_type": "daily"},
{"recurrence_range_type": None},
{"recurrence_range_type": "numbered"},
{"has_primary_reviewers": False},
],
)
def test_invalid_recurrence_or_missing_reviewers(self, definition_overrides):
result = self._run([_definition(**definition_overrides)])
assert result[0].status == "FAIL"
@@ -0,0 +1,59 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
AuthenticationMethodsPolicySettings,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_authentication_method_authenticator_show_context.entra_authentication_method_authenticator_show_context"
class Test_entra_authentication_method_authenticator_show_context:
def _run(self, settings):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_authentication_method_authenticator_show_context.entra_authentication_method_authenticator_show_context import (
entra_authentication_method_authenticator_show_context,
)
entra_client.authentication_methods_policy_settings = settings
return entra_authentication_method_authenticator_show_context().execute()
def test_no_settings(self):
assert self._run(None) == []
def test_both_enabled(self):
result = self._run(
AuthenticationMethodsPolicySettings(
authenticator_state="enabled",
authenticator_display_app_information_state="enabled",
authenticator_display_location_information_state="enabled",
)
)
assert result[0].status == "PASS"
def test_location_disabled(self):
result = self._run(
AuthenticationMethodsPolicySettings(
authenticator_state="enabled",
authenticator_display_app_information_state="enabled",
authenticator_display_location_information_state="disabled",
)
)
assert result[0].status == "FAIL"
def test_context_enabled_but_authenticator_disabled(self):
result = self._run(
AuthenticationMethodsPolicySettings(
authenticator_state="disabled",
authenticator_display_app_information_state="enabled",
authenticator_display_location_information_state="enabled",
)
)
assert result[0].status == "FAIL"
@@ -0,0 +1,139 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
AuthenticationMethodConfiguration,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_entra_authentication_method_email_otp_disabled:
def test_no_configurations(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled import (
entra_authentication_method_email_otp_disabled,
)
entra_client.authentication_method_configurations = {}
entra_client.tenant_domain = DOMAIN
check = entra_authentication_method_email_otp_disabled()
result = check.execute()
assert len(result) == 0
def test_email_otp_enabled(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled import (
entra_authentication_method_email_otp_disabled,
)
entra_client.authentication_method_configurations = {
"Email": AuthenticationMethodConfiguration(
id="Email",
state="enabled",
),
}
entra_client.tenant_domain = DOMAIN
check = entra_authentication_method_email_otp_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Email OTP authentication method is enabled in the tenant."
)
def test_email_otp_disabled(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled import (
entra_authentication_method_email_otp_disabled,
)
entra_client.authentication_method_configurations = {
"Email": AuthenticationMethodConfiguration(
id="Email",
state="disabled",
),
}
entra_client.tenant_domain = DOMAIN
check = entra_authentication_method_email_otp_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Email OTP authentication method is disabled in the tenant."
)
def test_email_otp_unknown_state(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_authentication_method_email_otp_disabled.entra_authentication_method_email_otp_disabled import (
entra_authentication_method_email_otp_disabled,
)
entra_client.authentication_method_configurations = {
"Email": AuthenticationMethodConfiguration(
id="Email",
state="unknown",
),
}
entra_client.tenant_domain = DOMAIN
check = entra_authentication_method_email_otp_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Email OTP authentication method state could not be determined; "
"treating as enabled/non-compliant."
)
@@ -0,0 +1,69 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
PimRoleApprovalSetting,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_pim_global_administrator_approval_required.entra_pim_global_administrator_approval_required"
class Test_entra_pim_global_administrator_approval_required:
def _run(self, settings):
entra_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_pim_global_administrator_approval_required.entra_pim_global_administrator_approval_required import (
entra_pim_global_administrator_approval_required,
)
entra_client.pim_role_approval_settings = settings
return entra_pim_global_administrator_approval_required().execute()
def test_no_setting(self):
assert self._run({}) == []
def test_approval_required(self):
result = self._run(
{
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=True,
has_approvers=True,
)
}
)
assert result[0].status == "PASS"
def test_no_approvers(self):
result = self._run(
{
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=True,
has_approvers=False,
)
}
)
assert result[0].status == "FAIL"
def test_approval_not_required_with_approvers(self):
result = self._run(
{
GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=GLOBAL_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=False,
has_approvers=True,
)
}
)
assert result[0].status == "FAIL"
assert result[0].status_extended == (
"PIM does not require approval to activate the Global Administrator role."
)
@@ -0,0 +1,70 @@
from unittest import mock
from prowler.providers.m365.services.entra.entra_service import (
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
PimRoleApprovalSetting,
)
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_pim_privileged_role_administrator_approval_required.entra_pim_privileged_role_administrator_approval_required"
class Test_entra_pim_privileged_role_administrator_approval_required:
def _run(self, settings):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_pim_privileged_role_administrator_approval_required.entra_pim_privileged_role_administrator_approval_required import (
entra_pim_privileged_role_administrator_approval_required,
)
entra_client.pim_role_approval_settings = settings
return entra_pim_privileged_role_administrator_approval_required().execute()
def test_no_setting(self):
assert self._run({}) == []
def test_approval_required(self):
result = self._run(
{
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=True,
has_approvers=True,
)
}
)
assert result[0].status == "PASS"
def test_no_approvers(self):
result = self._run(
{
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=True,
has_approvers=False,
)
}
)
assert result[0].status == "FAIL"
assert result[0].status_extended == (
"PIM requires approval to activate the Privileged Role Administrator "
"role but no approvers are configured."
)
def test_not_required(self):
result = self._run(
{
PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID: PimRoleApprovalSetting(
role_definition_id=PRIVILEGED_ROLE_ADMINISTRATOR_ROLE_TEMPLATE_ID,
is_approval_required=False,
has_approvers=True,
)
}
)
assert result[0].status == "FAIL"
@@ -3,7 +3,7 @@ import importlib
import json
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest
@@ -42,6 +42,363 @@ UserAction = entra_service.UserAction
UsersConditions = entra_service.UsersConditions
def _get_access_review_definitions(definition):
service = Entra.__new__(Entra)
request_information = MagicMock()
service.client = SimpleNamespace(
identity_governance=SimpleNamespace(
with_url=MagicMock(
return_value=SimpleNamespace(
to_get_request_information=MagicMock(
return_value=request_information
)
)
)
),
request_adapter=SimpleNamespace(
send_primitive_async=AsyncMock(
return_value=json.dumps({"value": [definition]}).encode()
)
),
)
return asyncio.run(service._get_access_review_definitions())
class TestAccessReviewDefinitions:
@pytest.mark.parametrize(
("settings", "expected"),
[
({"defaultDecisionEnabled": True}, True),
({"defaultDecisionEnabled": False}, False),
({}, False),
],
)
def test_parses_default_decision_enabled(self, settings, expected):
definitions = _get_access_review_definitions(
{"id": "review-1", "settings": settings}
)
assert definitions[0].default_decision_enabled is expected
def test_parses_recurrence_and_top_level_reviewers(self):
definitions = _get_access_review_definitions(
{
"id": "review-1",
"reviewers": [{"query": "/users/reviewer-1"}],
"settings": {
"recurrence": {
"pattern": {"type": "weekly"},
"range": {"type": "noEnd"},
}
},
}
)
assert definitions[0].recurrence_pattern_type == "weekly"
assert definitions[0].recurrence_range_type == "noEnd"
assert definitions[0].has_primary_reviewers is True
def test_uses_reviewers_from_every_configured_stage(self):
definitions = _get_access_review_definitions(
{
"id": "review-1",
"reviewers": [],
"stageSettings": [
{"reviewers": [{"query": "/users/reviewer-1"}]},
{"reviewers": [{"query": "/users/reviewer-2"}]},
],
}
)
assert definitions[0].has_primary_reviewers is True
@pytest.mark.parametrize(
"reviewer_configuration",
[
{"fallbackReviewers": [{"query": "/users/fallback-1"}]},
{
"reviewers": [{"query": "/users/top-level-reviewer"}],
"stageSettings": [
{"reviewers": [{"query": "/users/stage-reviewer"}]},
{"reviewers": []},
],
},
],
)
def test_fallback_or_incomplete_stage_reviewers_do_not_count(
self, reviewer_configuration
):
definitions = _get_access_review_definitions(
{"id": "review-1", **reviewer_configuration}
)
assert definitions[0].has_primary_reviewers is False
@pytest.mark.parametrize(
"terminal_response",
[b"", json.dumps({"value": []}).encode()],
)
def test_preserves_definitions_when_terminal_page_is_empty(self, terminal_response):
service = Entra.__new__(Entra)
first_page = json.dumps(
{
"value": [{"id": "review-1", "displayName": "Guest Review"}],
"@odata.nextLink": "next-link",
}
).encode()
send_mock = AsyncMock(side_effect=[first_page, terminal_response])
with_url_mock = MagicMock(
side_effect=[
SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-1")
),
SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-2")
),
]
)
service.client = SimpleNamespace(
identity_governance=SimpleNamespace(with_url=with_url_mock),
request_adapter=SimpleNamespace(send_primitive_async=send_mock),
)
definitions = asyncio.run(service._get_access_review_definitions())
assert [definition.id for definition in definitions] == ["review-1"]
assert send_mock.await_count == 2
assert with_url_mock.call_args_list[-1] == call("next-link")
def test_error_returns_no_access_review_definitions(self):
service = Entra.__new__(Entra)
service.client = SimpleNamespace(
identity_governance=SimpleNamespace(
with_url=MagicMock(
return_value=SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-1")
)
)
),
request_adapter=SimpleNamespace(
send_primitive_async=AsyncMock(side_effect=RuntimeError("Graph error"))
),
)
assert asyncio.run(service._get_access_review_definitions()) == []
class TestPimRoleApprovalSettings:
def test_paginates_and_parses_role_approval_settings(self):
service = Entra.__new__(Entra)
first_page = json.dumps(
{
"value": [
{"policy": {"rules": []}},
{
"roleDefinitionId": "role-unrelated",
"policy": {"rules": [{"id": "Unrelated_Rule"}]},
},
{
"roleDefinitionId": "role-approval-required",
"policy": {
"rules": [
{
"id": "Approval_EndUser_Assignment",
"setting": {
"isApprovalRequired": True,
"approvalStages": [
{"primaryApprovers": []},
{
"primaryApprovers": [
{"id": "approver-1"}
]
},
],
},
}
]
},
},
],
"@odata.nextLink": "next-link",
}
).encode()
final_page = json.dumps(
{
"value": [
{
"roleDefinitionId": "role-approval-disabled",
"policy": {
"rules": [
{
"id": "Approval_EndUser_Assignment",
"setting": {
"isApprovalRequired": False,
"approvalStages": [{"primaryApprovers": []}],
},
}
]
},
}
]
}
).encode()
send_mock = AsyncMock(side_effect=[first_page, final_page])
with_url_mock = MagicMock(
side_effect=[
SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-1")
),
SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-2")
),
]
)
service.client = SimpleNamespace(
policies=SimpleNamespace(with_url=with_url_mock),
request_adapter=SimpleNamespace(send_primitive_async=send_mock),
)
settings = asyncio.run(service._get_pim_role_approval_settings())
assert set(settings) == {
"role-unrelated",
"role-approval-required",
"role-approval-disabled",
}
assert settings["role-unrelated"].is_approval_required is False
assert settings["role-unrelated"].has_approvers is False
assert settings["role-approval-required"].is_approval_required is True
assert settings["role-approval-required"].has_approvers is True
assert settings["role-approval-disabled"].is_approval_required is False
assert settings["role-approval-disabled"].has_approvers is False
assert send_mock.await_count == 2
assert with_url_mock.call_args_list[-1] == call("next-link")
@pytest.mark.parametrize(
"response",
[b"", json.dumps({"value": []}).encode()],
)
def test_stops_on_falsy_or_empty_response(self, response):
service = Entra.__new__(Entra)
service.client = SimpleNamespace(
policies=SimpleNamespace(
with_url=MagicMock(
return_value=SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-1")
)
)
),
request_adapter=SimpleNamespace(
send_primitive_async=AsyncMock(return_value=response)
),
)
assert asyncio.run(service._get_pim_role_approval_settings()) == {}
def test_error_returns_no_role_approval_settings(self):
service = Entra.__new__(Entra)
service.client = SimpleNamespace(
policies=SimpleNamespace(
with_url=MagicMock(
return_value=SimpleNamespace(
to_get_request_information=MagicMock(return_value="request-1")
)
)
),
request_adapter=SimpleNamespace(
send_primitive_async=AsyncMock(side_effect=RuntimeError("Graph error"))
),
)
assert asyncio.run(service._get_pim_role_approval_settings()) == {}
class TestAuthenticationMethodsPolicySettings:
@staticmethod
def _service(response=None, error=None):
send_mock = AsyncMock(return_value=response, side_effect=error)
service = Entra.__new__(Entra)
service.client = SimpleNamespace(
policies=SimpleNamespace(
authentication_methods_policy=SimpleNamespace(
with_url=MagicMock(
return_value=SimpleNamespace(
to_get_request_information=MagicMock(
return_value="request-1"
)
)
)
)
),
request_adapter=SimpleNamespace(send_primitive_async=send_mock),
)
return service
def test_parses_microsoft_authenticator_after_unrelated_configuration(self):
response = json.dumps(
{
"authenticationMethodConfigurations": [
{"id": "Fido2", "state": "enabled"},
{
"id": "MicrosoftAuthenticator",
"state": "enabled",
"featureSettings": {
"displayAppInformationRequiredState": {"state": "enabled"},
"displayLocationInformationRequiredState": {
"state": "disabled"
},
},
},
]
}
).encode()
settings = asyncio.run(
self._service(
response=response
)._get_authentication_methods_policy_settings()
)
assert settings.authenticator_state == "enabled"
assert settings.authenticator_display_app_information_state == "enabled"
assert settings.authenticator_display_location_information_state == "disabled"
def test_missing_microsoft_authenticator_returns_unconfigured_model(self):
response = json.dumps(
{"authenticationMethodConfigurations": [{"id": "Fido2"}]}
).encode()
settings = asyncio.run(
self._service(
response=response
)._get_authentication_methods_policy_settings()
)
assert settings.authenticator_state is None
assert settings.authenticator_display_app_information_state is None
assert settings.authenticator_display_location_information_state is None
def test_falsy_response_returns_none(self):
assert (
asyncio.run(
self._service(
response=b""
)._get_authentication_methods_policy_settings()
)
is None
)
def test_error_returns_none(self):
assert (
asyncio.run(
self._service(
error=RuntimeError("Graph error")
)._get_authentication_methods_policy_settings()
)
is None
)
async def mock_entra_get_authorization_policy(_):
return AuthorizationPolicy(
id="id-1",