Add resource access explanation contract
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled

This commit is contained in:
2026-07-11 00:39:39 +02:00
parent c32951393a
commit 060f4da751
4 changed files with 170 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ from govoplan_core.core.access import (
AccessDecisionProvenance,
AccessExplanationService,
AccessGovernanceMaterializer,
ResourceAccessExplanationProvider,
AccessSemanticDirectory,
AccessSubjectRef,
AccountRef,
@@ -75,6 +76,7 @@ from govoplan_core.core.campaigns import (
CampaignPolicyContextProvider,
CampaignRetentionProvider,
)
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS, FileAccessProvider
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, OrganizationFunctionAssignmentRef
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory as CoreOrganizationDirectory, OrganizationFunctionRef as CoreOrganizationFunctionRef, OrganizationUnitRef as CoreOrganizationUnitRef
@@ -239,6 +241,24 @@ class _FakeResourceAccessService:
return AccessDecision(allowed=True)
class _FakeResourceAccessExplanationProvider:
def explain_resource_provenance(
self,
session: object,
principal: PrincipalRef,
*,
resource_type: str,
resource_id: str,
action: str,
):
del session, principal, resource_type, action
return (AccessDecisionProvenance(kind="resource", id=resource_id, label="Demo resource"),)
class _FakeFileAccessProvider(_FakeResourceAccessExplanationProvider):
pass
class _FakeAccessExplanationService:
provenance = (
AccessDecisionProvenance(kind="identity", id="identity-1", label="Demo Person"),
@@ -481,6 +501,7 @@ class AccessContractTests(unittest.TestCase):
self.assertEqual("access.administration", CAPABILITY_ACCESS_ADMINISTRATION)
self.assertEqual("access.governanceMaterializer", CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER)
self.assertEqual("campaigns.access", CAPABILITY_CAMPAIGNS_ACCESS)
self.assertEqual("files.access", CAPABILITY_FILES_ACCESS)
self.assertEqual("campaigns.deliveryTasks", CAPABILITY_CAMPAIGNS_DELIVERY_TASKS)
self.assertEqual("campaigns.mailPolicyContext", CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT)
self.assertEqual("campaigns.policyContext", CAPABILITY_CAMPAIGNS_POLICY_CONTEXT)
@@ -584,6 +605,8 @@ class AccessContractTests(unittest.TestCase):
self.assertIsInstance(_FakeAccessSemanticDirectory(), AccessSemanticDirectory)
self.assertIsInstance(_FakePermissionEvaluator(), PermissionEvaluator)
self.assertIsInstance(_FakeResourceAccessService(), ResourceAccessService)
self.assertIsInstance(_FakeResourceAccessExplanationProvider(), ResourceAccessExplanationProvider)
self.assertIsInstance(_FakeFileAccessProvider(), FileAccessProvider)
self.assertIsInstance(_FakeAccessExplanationService(), AccessExplanationService)
self.assertIsInstance(_FakeTenantAccessProvisioner(), TenantAccessProvisioner)
self.assertIsInstance(_FakeAccessAdministration(), AccessAdministration)
@@ -1108,6 +1131,32 @@ class AccessContractTests(unittest.TestCase):
idm_directory = FakeIdmDirectory()
organization_directory = FakeOrganizationDirectory()
class FakeFileAccessProvider:
def explain_resource_provenance(
self,
session: object,
principal: PrincipalRef,
*,
resource_type: str,
resource_id: str,
action: str,
):
del session, principal, action
if resource_type != "file":
return ()
return (
AccessDecisionProvenance(
kind="resource",
id=resource_id,
label="Registry.pdf",
tenant_id=tenant_id,
source="files.file",
details={"resource_type": "file"},
),
)
resource_provider = FakeFileAccessProvider()
with temporary_database(f"sqlite:///{root / 'test.db'}") as database:
create_scope_tables(database.engine)
Base.metadata.create_all(bind=database.engine)
@@ -1165,6 +1214,7 @@ class AccessContractTests(unittest.TestCase):
service = SqlAccessExplanationService(
idm_directory=idm_directory,
organization_directory=organization_directory,
resource_explanation_providers=(resource_provider,),
)
provenance = service.explain_scope_provenance(
PrincipalRef(
@@ -1178,8 +1228,30 @@ class AccessContractTests(unittest.TestCase):
)
self.assertTrue(any(item.kind == "function" and item.label == "Registry Clerk" for item in provenance))
self.assertTrue(any(item.kind == "role" and item.label == "File Reader" for item in provenance))
resource_provenance = service.explain_resource_provenance(
PrincipalRef(
account_id=account_id,
membership_id=user_id,
tenant_id=tenant_id,
identity_id=identity_id,
scopes=frozenset({"files:file:read"}),
),
resource_type="file",
resource_id="file-access-explanation",
action="files:file:read",
)
self.assertTrue(any(item.kind == "resource" and item.id == "file-access-explanation" for item in resource_provenance))
self.assertTrue(any(item.kind == "right" and item.id == "files:file:read" for item in resource_provenance))
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="access",
name="Access",
version="test",
capability_factories={CAPABILITY_ACCESS_EXPLANATION: lambda context: service},
)
)
registry.register(
ModuleManifest(
id="idm",
@@ -1230,6 +1302,18 @@ class AccessContractTests(unittest.TestCase):
self.assertIn("files:file:read", payload["user"]["effective_scopes"])
self.assertEqual("idm_function_role", payload["role_sources"][0]["source_type"])
self.assertEqual("Registry Clerk", payload["function_facts"][0]["function_name"])
resource_response = client.get(
"/admin/access/resource-explanation",
params={
"user_id": user_id,
"resource_type": "file",
"resource_id": "file-access-explanation",
"action": "files:file:read",
},
)
self.assertEqual(200, resource_response.status_code, resource_response.text)
resource_payload = resource_response.json()
self.assertTrue(any(item["kind"] == "resource" and item["id"] == "file-access-explanation" for item in resource_payload["provenance"]))
finally:
clear_runtime()
shutil.rmtree(root, ignore_errors=True)