Enforce Policy ceilings in View resolution

This commit is contained in:
2026-07-31 17:55:46 +02:00
parent 554705c3c2
commit 303f1556c2
5 changed files with 619 additions and 38 deletions
+117
View File
@@ -6,6 +6,11 @@ from sqlalchemy.orm import Session
from govoplan_core.audit.logging import audit_event
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory
from govoplan_core.core.policy import (
DefinitionScopeRef,
ViewGovernanceRequest,
view_governance_policy,
)
from govoplan_core.core.views import VIEW_SURFACE_CONTRACT_VERSION, ViewSurface
from govoplan_core.db.session import get_session
from govoplan_views.backend.manifest import (
@@ -81,6 +86,49 @@ def _catalogue() -> tuple[ViewSurface, ...]:
return get_registry().view_surfaces()
def _view_governance_policy():
return view_governance_policy(get_registry())
def _enforce_view_policy_action(
session: Session,
principal: ApiPrincipal,
*,
action: str,
scope_type: str,
scope_id: str | None,
view_id: str | None = None,
surface_ids: tuple[str, ...] = (),
) -> None:
policy = _view_governance_policy()
if policy is None:
return
target_id = scope_id
if scope_type == "tenant":
target_id = target_id or principal.tenant_id
decision = policy.resolve_view_action(
session,
request=ViewGovernanceRequest(
tenant_id=principal.tenant_id,
action=action, # type: ignore[arg-type]
actor=principal.principal,
target_scope=DefinitionScopeRef(
scope_type, # type: ignore[arg-type]
target_id,
),
view_id=view_id,
candidate_view_ids=(view_id,) if view_id is not None else (),
candidate_surface_ids=tuple(surface.id for surface in _catalogue()),
requested_surface_ids=surface_ids,
),
)
if not decision.allowed:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=decision.reason or f"Policy denied View action: {action}",
)
def _optional_access_directory() -> AccessDirectory | None:
registry = get_registry()
if not registry.has_capability(CAPABILITY_ACCESS_DIRECTORY):
@@ -432,6 +480,7 @@ def _effective_response(state: EffectiveViewState) -> EffectiveViewResponse:
active_revision_id=effective.revision_id,
active_view_name=effective.name,
visible_surface_ids=sorted(effective.visible_surface_ids),
projection_active=effective.projection_active,
locked=effective.locked,
available_views=[
EffectiveViewOptionResponse(
@@ -490,6 +539,7 @@ def api_effective_view(
account_id=principal.account_id,
group_ids=principal.group_ids,
catalogue=_catalogue(),
governance_policy=_view_governance_policy(),
)
)
@@ -515,6 +565,7 @@ def api_workflow_effective_view(
workflow_view_id=payload.view_id,
workflow_revision_id=payload.revision_id,
workflow_surface_ids=payload.visible_surface_ids,
governance_policy=_view_governance_policy(),
)
)
@@ -534,6 +585,7 @@ def api_select_view(
group_ids=principal.group_ids,
view_id=payload.view_id,
catalogue=_catalogue(),
governance_policy=_view_governance_policy(),
)
_audit(
session,
@@ -651,6 +703,14 @@ def api_create_definition(
payload.scope_id,
)
_require_definition_write(principal, payload.scope_type, target_id)
_enforce_view_policy_action(
session,
principal,
action="derive",
scope_type=payload.scope_type,
scope_id=target_id,
surface_ids=tuple(payload.visible_surface_ids),
)
try:
definition = create_definition(
session,
@@ -700,6 +760,14 @@ def api_update_definition(
definition.scope_type,
definition.scope_id,
)
_enforce_view_policy_action(
session,
principal,
action="edit",
scope_type=definition.scope_type,
scope_id=definition.scope_id,
view_id=definition.id,
)
update_definition(
session,
definition,
@@ -775,6 +843,15 @@ def api_create_revision(
definition.scope_type,
definition.scope_id,
)
_enforce_view_policy_action(
session,
principal,
action="edit",
scope_type=definition.scope_type,
scope_id=definition.scope_id,
view_id=definition.id,
surface_ids=tuple(payload.visible_surface_ids),
)
revision = create_revision(
session,
definition,
@@ -821,6 +898,14 @@ def api_publish_revision(
definition.scope_type,
definition.scope_id,
)
_enforce_view_policy_action(
session,
principal,
action="edit",
scope_type=definition.scope_type,
scope_id=definition.scope_id,
view_id=definition.id,
)
revision = get_revision(
session,
definition_id=definition.id,
@@ -868,6 +953,14 @@ def api_archive_definition(
definition.scope_type,
definition.scope_id,
)
_enforce_view_policy_action(
session,
principal,
action="edit",
scope_type=definition.scope_type,
scope_id=definition.scope_id,
view_id=definition.id,
)
archive_definition(
session,
definition,
@@ -977,6 +1070,14 @@ def api_create_assignment(
tenant_id=principal.tenant_id,
definition_id=payload.definition_id,
)
_enforce_view_policy_action(
session,
principal,
action="assign",
scope_type=payload.scope_type,
scope_id=payload.scope_id,
view_id=definition.id,
)
assignment = create_assignment(
session,
tenant_id=principal.tenant_id,
@@ -1033,6 +1134,14 @@ def api_update_assignment(
assignment_id=assignment_id,
)
_require_assignment_write(principal, assignment.scope_type)
_enforce_view_policy_action(
session,
principal,
action="assign",
scope_type=assignment.scope_type,
scope_id=assignment.scope_id,
view_id=assignment.definition_id,
)
update_assignment(
session,
assignment,
@@ -1076,6 +1185,14 @@ def api_delete_assignment(
assignment_id=assignment_id,
)
_require_assignment_write(principal, assignment.scope_type)
_enforce_view_policy_action(
session,
principal,
action="assign",
scope_type=assignment.scope_type,
scope_id=assignment.scope_id,
view_id=assignment.definition_id,
)
assignment_details = {
"scope_type": assignment.scope_type,
"scope_id": assignment.scope_id,