feat: govern selected-user access diagnostics

This commit is contained in:
2026-08-20 06:16:43 +02:00
parent a8ec72d8a9
commit 8fcc12dbb2
4 changed files with 184 additions and 0 deletions
@@ -0,0 +1,50 @@
from __future__ import annotations
from govoplan_core.core.access import (
AccessExplanationSubjectDecision,
PrincipalRef,
)
from govoplan_core.security.module_permissions import scopes_grant_compatible
ACCESS_EXPLANATION_SUBJECT_SCOPE = "policy:access_explanation:select_user"
class AccessExplanationSubjectPolicyProvider:
"""Decide whether an actor may run an explanation for another user."""
def decide_subject_selection(
self,
session: object,
principal: PrincipalRef,
*,
tenant_id: str,
) -> AccessExplanationSubjectDecision:
del session
if principal.tenant_id != tenant_id:
return AccessExplanationSubjectDecision(
allow_other_users=False,
reason="Access explanations are limited to the active tenant.",
source="policy.tenant_boundary",
required_scope=ACCESS_EXPLANATION_SUBJECT_SCOPE,
provenance={"tenant_id": tenant_id, "mode": "current_user"},
)
allowed = scopes_grant_compatible(
principal.scopes,
ACCESS_EXPLANATION_SUBJECT_SCOPE,
)
return AccessExplanationSubjectDecision(
allow_other_users=allowed,
reason=(
"Policy permits selected-user access diagnostics."
if allowed
else "Policy limits access explanations to the signed-in user."
),
source="policy.permission",
required_scope=ACCESS_EXPLANATION_SUBJECT_SCOPE,
provenance={
"tenant_id": tenant_id,
"mode": "cross_user" if allowed else "current_user",
},
)
+69
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from govoplan_core.core.access import ( from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS,
) )
from govoplan_core.core.distribution_lists import ( from govoplan_core.core.distribution_lists import (
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS, CAPABILITY_POLICY_DISTRIBUTION_CHANNELS,
@@ -28,6 +29,7 @@ from govoplan_core.core.modules import (
ModuleContext, ModuleContext,
ModuleInterfaceProvider, ModuleInterfaceProvider,
ModuleManifest, ModuleManifest,
PermissionDefinition,
) )
from govoplan_core.core.reporting import CAPABILITY_POLICY_REPORTING_GOVERNANCE from govoplan_core.core.reporting import CAPABILITY_POLICY_REPORTING_GOVERNANCE
from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.provider_governance import declared_module_architecture
@@ -104,10 +106,36 @@ def _reporting_governance_policy(context: ModuleContext) -> object:
return ReportingGovernancePolicyProvider() return ReportingGovernancePolicyProvider()
def _access_explanation_subject_policy(context: ModuleContext) -> object:
del context
from govoplan_policy.backend.access_explanation_subjects import (
AccessExplanationSubjectPolicyProvider,
)
return AccessExplanationSubjectPolicyProvider()
ACCESS_EXPLANATION_SUBJECT_SCOPE = "policy:access_explanation:select_user"
manifest = ModuleManifest( manifest = ModuleManifest(
id="policy", id="policy",
name="Policy", name="Policy",
version="0.1.18", version="0.1.18",
permissions=(
PermissionDefinition(
scope=ACCESS_EXPLANATION_SUBJECT_SCOPE,
label="Select users for access diagnostics",
description=(
"Run resource-access explanations for another user in the active tenant."
),
category="Policy",
level="tenant",
module_id="policy",
resource="access_explanation",
action="select_user",
),
),
required_capabilities=( required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -133,9 +161,37 @@ manifest = ModuleManifest(
name=CAPABILITY_POLICY_REPORTING_GOVERNANCE, name=CAPABILITY_POLICY_REPORTING_GOVERNANCE,
version="1.0.0", version="1.0.0",
), ),
ModuleInterfaceProvider(
name=CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS,
version="1.0.0",
),
), ),
route_factory=_route_factory, route_factory=_route_factory,
documentation=( documentation=(
DocumentationTopic(
id="policy.access-explanation-subjects",
title="Choose subjects for access diagnostics",
summary=(
"Policy keeps access explanations on the signed-in user unless "
"the actor has the selected-user diagnostic permission."
),
body=(
"Files and Campaign use the shared access-explanation picker. "
"Without policy:access_explanation:select_user, Access returns "
"only the signed-in user and does not disclose tenant-directory "
"metadata. Permitted cross-user explanations remain limited to "
"the active tenant and are recorded as administrator diagnostics "
"in audit evidence. The permission changes diagnostic visibility; "
"it does not grant access to the explained resource."
),
documentation_types=("admin", "user"),
audience=("user", "tenant_admin", "policy_admin"),
related_modules=("access", "audit", "campaign", "files"),
metadata={
"kind": "reference",
"help_contexts": ["access.resource-explanation.subject"],
},
),
DocumentationTopic( DocumentationTopic(
id="policy.view-governance-administration", id="policy.view-governance-administration",
title="Govern View availability and actions", title="Govern View availability and actions",
@@ -371,10 +427,23 @@ manifest = ModuleManifest(
), ),
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS: _distribution_channel_policy, CAPABILITY_POLICY_DISTRIBUTION_CHANNELS: _distribution_channel_policy,
CAPABILITY_POLICY_REPORTING_GOVERNANCE: _reporting_governance_policy, CAPABILITY_POLICY_REPORTING_GOVERNANCE: _reporting_governance_policy,
CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS: (
_access_explanation_subject_policy
),
CAPABILITY_POLICY_PRIVACY_RETENTION: _privacy_retention_service, CAPABILITY_POLICY_PRIVACY_RETENTION: _privacy_retention_service,
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY: _scheduling_participant_privacy_policy, CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY: _scheduling_participant_privacy_policy,
}, },
capability_documentation={ capability_documentation={
CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS: CapabilityDocumentation(
label="Access-explanation subject policy",
summary=(
"Limits access explanations to the current user or permits "
"audited selected-user administrator diagnostics."
),
contract_version="1.0",
documentation_types=("admin", "user"),
audience=("tenant_admin", "policy_admin"),
),
CAPABILITY_POLICY_REPORTING_GOVERNANCE: CapabilityDocumentation( CAPABILITY_POLICY_REPORTING_GOVERNANCE: CapabilityDocumentation(
label="Reporting privacy governance", label="Reporting privacy governance",
summary=( summary=(
+63
View File
@@ -0,0 +1,63 @@
from __future__ import annotations
import unittest
from govoplan_core.core.access import PrincipalRef
from govoplan_policy.backend.access_explanation_subjects import (
ACCESS_EXPLANATION_SUBJECT_SCOPE,
AccessExplanationSubjectPolicyProvider,
)
class AccessExplanationSubjectPolicyTests(unittest.TestCase):
def setUp(self) -> None:
self.provider = AccessExplanationSubjectPolicyProvider()
def test_defaults_to_current_user_without_permission(self) -> None:
decision = self.provider.decide_subject_selection(
object(),
PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
),
tenant_id="tenant-1",
)
self.assertFalse(decision.allow_other_users)
self.assertEqual(ACCESS_EXPLANATION_SUBJECT_SCOPE, decision.required_scope)
self.assertEqual("current_user", decision.provenance["mode"])
def test_permission_enables_cross_user_diagnostics(self) -> None:
decision = self.provider.decide_subject_selection(
object(),
PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
scopes=frozenset({ACCESS_EXPLANATION_SUBJECT_SCOPE}),
),
tenant_id="tenant-1",
)
self.assertTrue(decision.allow_other_users)
self.assertEqual("cross_user", decision.provenance["mode"])
def test_never_crosses_the_active_tenant(self) -> None:
decision = self.provider.decide_subject_selection(
object(),
PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
scopes=frozenset({ACCESS_EXPLANATION_SUBJECT_SCOPE}),
),
tenant_id="tenant-2",
)
self.assertFalse(decision.allow_other_users)
self.assertEqual("policy.tenant_boundary", decision.source)
if __name__ == "__main__":
unittest.main()
+2
View File
@@ -11,6 +11,7 @@ from govoplan_core.core.policy import (
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY, CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY,
CAPABILITY_POLICY_VIEW_GOVERNANCE, CAPABILITY_POLICY_VIEW_GOVERNANCE,
) )
from govoplan_core.core.access import CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS
from govoplan_core.core.distribution_lists import ( from govoplan_core.core.distribution_lists import (
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS, CAPABILITY_POLICY_DISTRIBUTION_CHANNELS,
) )
@@ -54,6 +55,7 @@ class PolicyModuleContractTests(unittest.TestCase):
CAPABILITY_POLICY_REPORTING_GOVERNANCE, CAPABILITY_POLICY_REPORTING_GOVERNANCE,
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY, CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY,
CAPABILITY_POLICY_VIEW_GOVERNANCE, CAPABILITY_POLICY_VIEW_GOVERNANCE,
CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS,
}, },
set(manifest.capability_factories), set(manifest.capability_factories),
) )