188 lines
6.1 KiB
Python
188 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
|
from govoplan_core.core.postbox import PostboxActorRef
|
|
from govoplan_postbox.backend.access_decisions import (
|
|
ACCESS_DECISION_TABLE,
|
|
evaluate_postbox_access,
|
|
)
|
|
|
|
|
|
def assignment(
|
|
source: str = "direct",
|
|
*,
|
|
assignment_id: str | None = None,
|
|
tenant_id: str = "tenant-1",
|
|
acting_for_account_id: str | None = None,
|
|
) -> OrganizationFunctionAssignmentRef:
|
|
return OrganizationFunctionAssignmentRef(
|
|
id=assignment_id or f"{source}-assignment",
|
|
tenant_id=tenant_id,
|
|
identity_id="identity-1",
|
|
account_id="account-1",
|
|
function_id="function-1",
|
|
organization_unit_id="unit-1",
|
|
source=source, # type: ignore[arg-type]
|
|
acting_for_account_id=acting_for_account_id,
|
|
)
|
|
|
|
|
|
def decide(
|
|
*,
|
|
status: str = "active",
|
|
action: str = "read",
|
|
authorized_actions: frozenset[str] = frozenset({"read"}),
|
|
binding_available: bool = True,
|
|
assignments=(),
|
|
selected_assignment_id: str | None = None,
|
|
acting_for_account_id: str | None = None,
|
|
):
|
|
return evaluate_postbox_access(
|
|
postbox_id="postbox-1",
|
|
postbox_active=status == "active",
|
|
action=action, # type: ignore[arg-type]
|
|
actor=PostboxActorRef(
|
|
account_id="account-1",
|
|
identity_id="identity-1",
|
|
selected_assignment_id=selected_assignment_id,
|
|
acting_for_account_id=acting_for_account_id,
|
|
authorized_actions=authorized_actions, # type: ignore[arg-type]
|
|
),
|
|
organization_unit_id="unit-1" if binding_available else None,
|
|
function_id="function-1" if binding_available else None,
|
|
holder_count=len(assignments),
|
|
binding_available=binding_available,
|
|
binding_assignments=assignments,
|
|
)
|
|
|
|
|
|
class PostboxAccessDecisionTableTests(unittest.TestCase):
|
|
def test_rule_order_is_fail_closed(self) -> None:
|
|
self.assertEqual(
|
|
[rule.name for rule in ACCESS_DECISION_TABLE],
|
|
[
|
|
"inactive_postbox",
|
|
"generic_permission",
|
|
"administrator",
|
|
"active_function_binding",
|
|
],
|
|
)
|
|
inactive_admin = decide(
|
|
status="archived",
|
|
action="administer",
|
|
authorized_actions=frozenset({"administer"}),
|
|
)
|
|
self.assertFalse(inactive_admin.allowed)
|
|
self.assertEqual(inactive_admin.reason_code, "postbox_inactive")
|
|
|
|
def test_permission_and_binding_denials_have_stable_provenance(self) -> None:
|
|
missing_permission = decide(
|
|
assignments=(assignment(),),
|
|
authorized_actions=frozenset(),
|
|
)
|
|
missing_binding = decide(binding_available=False)
|
|
|
|
self.assertEqual(
|
|
missing_permission.reason_code,
|
|
"generic_permission_missing",
|
|
)
|
|
self.assertEqual(
|
|
missing_binding.reason_code,
|
|
"function_binding_missing",
|
|
)
|
|
|
|
def test_administration_is_generic_but_still_requires_active_postbox(self) -> None:
|
|
decision = decide(
|
|
action="administer",
|
|
authorized_actions=frozenset({"administer"}),
|
|
binding_available=False,
|
|
)
|
|
self.assertTrue(decision.allowed)
|
|
self.assertEqual(decision.reason_code, "generic_administrator")
|
|
|
|
def test_direct_delegated_directory_governance_and_system_sources_are_allowed(self) -> None:
|
|
for source in (
|
|
"direct",
|
|
"delegated",
|
|
"directory",
|
|
"governance",
|
|
"system",
|
|
):
|
|
with self.subTest(source=source):
|
|
decision = decide(assignments=(assignment(source),))
|
|
self.assertTrue(decision.allowed)
|
|
self.assertEqual(
|
|
decision.reason_code,
|
|
f"effective_{source}_assignment",
|
|
)
|
|
|
|
def test_selected_direct_context_is_preferred_without_hiding_other_matches(self) -> None:
|
|
direct = assignment("direct", assignment_id="direct-1")
|
|
delegated = assignment("delegated", assignment_id="delegated-1")
|
|
decision = decide(
|
|
assignments=(direct, delegated),
|
|
selected_assignment_id=delegated.id,
|
|
)
|
|
|
|
self.assertTrue(decision.allowed)
|
|
self.assertEqual(decision.selected_assignment_id, delegated.id)
|
|
self.assertEqual(
|
|
decision.assignment_ids,
|
|
("direct-1", "delegated-1"),
|
|
)
|
|
|
|
def test_acting_access_requires_exact_assignment_and_represented_account(self) -> None:
|
|
acting = assignment(
|
|
"acting_for",
|
|
assignment_id="acting-1",
|
|
acting_for_account_id="represented-1",
|
|
)
|
|
missing_context = decide(assignments=(acting,))
|
|
wrong_assignment = decide(
|
|
assignments=(acting,),
|
|
selected_assignment_id="acting-other",
|
|
)
|
|
wrong_account = decide(
|
|
assignments=(acting,),
|
|
selected_assignment_id=acting.id,
|
|
acting_for_account_id="represented-other",
|
|
)
|
|
allowed = decide(
|
|
assignments=(acting,),
|
|
selected_assignment_id=acting.id,
|
|
acting_for_account_id="represented-1",
|
|
)
|
|
|
|
self.assertEqual(
|
|
missing_context.reason_code,
|
|
"acting_context_required",
|
|
)
|
|
self.assertEqual(
|
|
wrong_assignment.reason_code,
|
|
"acting_assignment_not_selected",
|
|
)
|
|
self.assertEqual(
|
|
wrong_account.reason_code,
|
|
"acting_account_mismatch",
|
|
)
|
|
self.assertTrue(allowed.allowed)
|
|
self.assertEqual(
|
|
allowed.reason_code,
|
|
"effective_acting_for_assignment",
|
|
)
|
|
|
|
def test_vacancy_is_provenance_not_an_implicit_access_override(self) -> None:
|
|
denied = decide(assignments=())
|
|
self.assertFalse(denied.allowed)
|
|
self.assertTrue(denied.vacant)
|
|
self.assertEqual(
|
|
denied.reason_code,
|
|
"effective_assignment_missing",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|