Enforce Policy ceilings in View resolution
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user