Complete governed reporting execution and publication

This commit is contained in:
2026-08-04 02:23:24 +02:00
parent a81151391b
commit 8337aec19a
29 changed files with 3611 additions and 81 deletions
+75 -17
View File
@@ -27,6 +27,11 @@ from govoplan_reporting.backend.domain import (
ReportingDefinitionRecord,
definition_from_row,
)
from govoplan_reporting.backend.governance import (
apply_parent_governance,
normalize_definition_governance,
scope_visible,
)
from govoplan_reporting.backend.schemas import validate_definition_payload
@@ -86,17 +91,29 @@ def create_definition(
clean_reason = _required(change_reason, "Reporting change reason", 1_000)
_aware(recorded_at, "Reporting recorded_at")
validated_payload = validate_definition_payload(kind, dict(payload))
validated_payload = validate_definition_payload(
kind,
normalize_definition_governance(
validated_payload,
principal,
administrative=_has_scope(principal, ADMIN_SCOPE),
),
)
parent_kind, parent_id, parent_revision = _parent_reference(
kind,
validated_payload,
)
_validate_parent(
session,
tenant_id=tenant_id,
child_status=clean_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
validated_payload = validate_definition_payload(
kind,
_validate_parent(
session,
tenant_id=tenant_id,
child_status=clean_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
child_payload=validated_payload,
),
)
request = {
"definition_kind": kind,
@@ -241,14 +258,26 @@ def update_definition(
kind,
dict(changes.get("payload", current.payload)),
)
next_payload = validate_definition_payload(
kind,
normalize_definition_governance(
next_payload,
principal,
administrative=_has_scope(principal, ADMIN_SCOPE),
),
)
parent_kind, parent_id, parent_revision = _parent_reference(kind, next_payload)
_validate_parent(
session,
tenant_id=tenant_id,
child_status=next_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
next_payload = validate_definition_payload(
kind,
_validate_parent(
session,
tenant_id=tenant_id,
child_status=next_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
child_payload=next_payload,
),
)
identity = _identity(session, tenant_id, kind, definition_id)
if identity is None:
@@ -584,9 +613,10 @@ def _validate_parent(
parent_kind: str | None,
parent_id: str | None,
parent_revision: int | None,
) -> None:
child_payload: Mapping[str, object],
) -> dict[str, object]:
if parent_kind is None:
return
return dict(child_payload)
row = (
session.query(ReportingDefinitionRevision)
.filter(
@@ -605,6 +635,7 @@ def _validate_parent(
raise ReportingDefinitionError(
f"An active Reporting definition requires an active {parent_kind} revision."
)
return apply_parent_governance(child_payload, row.payload)
def _parent_reference(
@@ -667,6 +698,8 @@ def _can_access(
)
if row is None:
return False
if not scope_visible(row.payload, principal):
return False
if _has_scope(principal, ADMIN_SCOPE):
return True
identity = _identity(session, tenant_id, definition_kind, definition_id)
@@ -712,6 +745,24 @@ def _require_scope(principal: object, scope: str) -> None:
def _filter_accessible(query: Query, principal: object) -> Query:
if _has_scope(principal, ADMIN_SCOPE):
return query
governance = ReportingDefinitionRevision.payload["governance"]
scope_type = governance["scope_type"].as_string()
scope_id = governance["scope_id"].as_string()
inherited = governance["inherit_to_lower_scopes"].as_boolean()
scope_conditions = [
scope_type.is_(None),
and_(
scope_type == "tenant",
or_(scope_id.is_(None), scope_id == _principal_tenant(principal)),
),
and_(scope_type == "system", inherited.is_(True)),
]
group_ids = tuple(_string_subject_ids(principal, "group_ids"))
if group_ids:
scope_conditions.append(and_(scope_type == "group", scope_id.in_(group_ids)))
user_ids = _actor_ids(principal)
if user_ids:
scope_conditions.append(and_(scope_type == "user", scope_id.in_(user_ids)))
conditions = [ReportingDefinitionRevision.visibility == "tenant"]
actor_ids = _actor_ids(principal)
if actor_ids:
@@ -752,7 +803,14 @@ def _filter_accessible(query: Query, principal: object) -> Query:
)
)
)
return query.filter(or_(*conditions))
return query.filter(and_(or_(*scope_conditions), or_(*conditions)))
def _string_subject_ids(principal: object, attribute: str) -> tuple[str, ...]:
raw = getattr(principal, attribute, ()) or ()
if isinstance(raw, (str, bytes)):
return (str(raw),) if raw else ()
return tuple(dict.fromkeys(str(value) for value in raw if str(value or "").strip()))
def _current_row(