feat: govern selected-user access diagnostics
This commit is contained in:
@@ -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",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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=(
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user