264 lines
7.2 KiB
Python
264 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_policy.backend.db.models import PolicyOverride
|
|
from govoplan_policy.backend.policy_overrides import (
|
|
delete_policy_override,
|
|
get_policy_override,
|
|
normalize_policy_scope,
|
|
resolution_policy_overrides,
|
|
set_policy_override,
|
|
)
|
|
from govoplan_policy.backend.view_governance import (
|
|
VIEW_POLICY_BOOLEAN_FIELDS,
|
|
ViewPolicyResolution,
|
|
resolve_view_policy_rows,
|
|
validate_view_policy,
|
|
)
|
|
|
|
|
|
class ViewPolicyError(ValueError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ViewPolicyState:
|
|
row: PolicyOverride | None
|
|
local_policy: Mapping[str, bool | tuple[str, ...]]
|
|
effective: ViewPolicyResolution
|
|
parent: ViewPolicyResolution
|
|
|
|
|
|
def view_policy_state(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
scope_type: str,
|
|
scope_id: str | None = None,
|
|
) -> ViewPolicyState:
|
|
_, clean_scope_id, _ = normalize_policy_scope(
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
row = get_policy_override(
|
|
session,
|
|
policy_family="view",
|
|
target_key="*",
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=clean_scope_id,
|
|
)
|
|
local_policy, malformed = validate_view_policy(
|
|
row.policy if row is not None else {}
|
|
)
|
|
if malformed:
|
|
local_policy = {}
|
|
rows = _rows_for_scope(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=clean_scope_id,
|
|
)
|
|
rank = _scope_rank(scope_type)
|
|
return ViewPolicyState(
|
|
row=row,
|
|
local_policy=local_policy,
|
|
effective=resolve_view_policy_rows(rows),
|
|
parent=resolve_view_policy_rows(
|
|
tuple(item for item in rows if _scope_rank(item.scope_type) < rank)
|
|
),
|
|
)
|
|
|
|
|
|
def save_view_policy(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
scope_type: str,
|
|
scope_id: str | None,
|
|
policy: object,
|
|
actor_id: str | None,
|
|
) -> ViewPolicyState:
|
|
clean_policy, _before = validate_view_policy_change(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
policy=policy,
|
|
)
|
|
set_policy_override(
|
|
session,
|
|
policy_family="view",
|
|
target_key="*",
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
policy={
|
|
key: list(value) if isinstance(value, tuple) else value
|
|
for key, value in clean_policy.items()
|
|
},
|
|
actor_id=actor_id,
|
|
)
|
|
_clear_resolution_cache(session)
|
|
return view_policy_state(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
|
|
|
|
def validate_view_policy_change(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
scope_type: str,
|
|
scope_id: str | None,
|
|
policy: object,
|
|
) -> tuple[dict[str, bool | tuple[str, ...]], ViewPolicyState]:
|
|
clean_policy, malformed = validate_view_policy(policy)
|
|
if malformed:
|
|
raise ViewPolicyError("View policy fields have invalid names or values")
|
|
before = view_policy_state(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
broadened = sorted(
|
|
field
|
|
for field in VIEW_POLICY_BOOLEAN_FIELDS
|
|
if clean_policy.get(field) is True and before.parent.limits[field] is False
|
|
)
|
|
for field, parent_values in (
|
|
("allowed_view_ids", before.parent.allowed_view_ids),
|
|
("visible_surface_ids", before.parent.visible_surface_ids),
|
|
):
|
|
local_values = clean_policy.get(field)
|
|
if (
|
|
isinstance(local_values, tuple)
|
|
and parent_values is not None
|
|
and not set(local_values).issubset(parent_values)
|
|
):
|
|
broadened.append(field)
|
|
if broadened:
|
|
raise ViewPolicyError(
|
|
"Lower-scope View policy cannot broaden parent restrictions: "
|
|
+ ", ".join(broadened)
|
|
)
|
|
return clean_policy, before
|
|
|
|
|
|
def remove_view_policy(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
scope_type: str,
|
|
scope_id: str | None,
|
|
) -> bool:
|
|
row = get_policy_override(
|
|
session,
|
|
policy_family="view",
|
|
target_key="*",
|
|
tenant_id=tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
if row is None:
|
|
return False
|
|
delete_policy_override(session, row)
|
|
_clear_resolution_cache(session)
|
|
return True
|
|
|
|
|
|
def view_policy_response_payload(state: ViewPolicyState) -> dict[str, Any]:
|
|
local_policy: Mapping[str, Any] = state.local_policy
|
|
if state.row is not None:
|
|
_, malformed = validate_view_policy(state.row.policy)
|
|
if malformed:
|
|
local_policy = {"configuration_status": "invalid_fail_closed"}
|
|
return {
|
|
"id": state.row.id if state.row is not None else None,
|
|
"revision": state.row.revision if state.row is not None else None,
|
|
"policy": _json_policy(local_policy),
|
|
"effective_policy": _resolution_payload(state.effective),
|
|
"parent_policy": _resolution_payload(state.parent),
|
|
"source_path": [step.to_dict() for step in state.effective.source_path],
|
|
"diagnostics": [dict(item) for item in state.effective.diagnostics],
|
|
}
|
|
|
|
|
|
def _resolution_payload(resolution: ViewPolicyResolution) -> dict[str, Any]:
|
|
return {
|
|
**dict(resolution.limits),
|
|
"allowed_view_ids": (
|
|
sorted(resolution.allowed_view_ids)
|
|
if resolution.allowed_view_ids is not None
|
|
else None
|
|
),
|
|
"visible_surface_ids": (
|
|
sorted(resolution.visible_surface_ids)
|
|
if resolution.visible_surface_ids is not None
|
|
else None
|
|
),
|
|
}
|
|
|
|
|
|
def _json_policy(policy: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
key: list(value) if isinstance(value, tuple) else value
|
|
for key, value in policy.items()
|
|
}
|
|
|
|
|
|
def _rows_for_scope(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
scope_type: str,
|
|
scope_id: str | None,
|
|
) -> tuple[PolicyOverride, ...]:
|
|
clean_scope = scope_type.strip().casefold()
|
|
rows = resolution_policy_overrides(
|
|
session,
|
|
policy_family="view",
|
|
target_keys=("*",),
|
|
tenant_id=tenant_id,
|
|
group_ids=(scope_id,) if clean_scope == "group" and scope_id else (),
|
|
user_ids=(scope_id,) if clean_scope == "user" and scope_id else (),
|
|
)
|
|
maximum_rank = _scope_rank(clean_scope)
|
|
return tuple(row for row in rows if _scope_rank(row.scope_type) <= maximum_rank)
|
|
|
|
|
|
def _scope_rank(scope_type: str) -> int:
|
|
try:
|
|
return ("system", "tenant", "group", "user").index(
|
|
scope_type.strip().casefold()
|
|
)
|
|
except ValueError as exc:
|
|
raise ViewPolicyError(
|
|
"View policy scope must be system, tenant, group, or user"
|
|
) from exc
|
|
|
|
|
|
def _clear_resolution_cache(session: Session) -> None:
|
|
session.info.pop("govoplan_policy_override_resolution", None)
|
|
|
|
|
|
__all__ = [
|
|
"ViewPolicyError",
|
|
"ViewPolicyState",
|
|
"remove_view_policy",
|
|
"save_view_policy",
|
|
"validate_view_policy_change",
|
|
"view_policy_response_payload",
|
|
"view_policy_state",
|
|
]
|