Complete governed reporting execution and publication
This commit is contained in:
@@ -37,6 +37,12 @@ from govoplan_reporting.backend.db.models import (
|
||||
ReportingQualityResult,
|
||||
)
|
||||
from govoplan_reporting.backend.definitions import get_definition, list_definitions
|
||||
from govoplan_reporting.backend.domain import ReportingDefinitionRecord
|
||||
from govoplan_reporting.backend.governance import require_definition_action
|
||||
from govoplan_reporting.backend.postgres_planner import (
|
||||
POSTGRES_PLANNER_VERSION,
|
||||
execute_postgres_query,
|
||||
)
|
||||
from govoplan_reporting.backend.query_engine import (
|
||||
QUERY_ENGINE_VERSION,
|
||||
DefaultChartRenderer,
|
||||
@@ -99,7 +105,12 @@ class SqlReportingRunner:
|
||||
*,
|
||||
execution_id: str,
|
||||
) -> Mapping[str, object] | None:
|
||||
return get_execution(_session(session), principal, execution_id=execution_id)
|
||||
return get_execution(
|
||||
_session(session),
|
||||
principal,
|
||||
execution_id=execution_id,
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
|
||||
def execute_report(
|
||||
@@ -126,6 +137,13 @@ def execute_report(
|
||||
if report_record.status != "active":
|
||||
raise ReportingExecutionError("Only active report definitions can run.")
|
||||
report = ReportDefinition.model_validate(report_record.payload)
|
||||
report_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=report_record,
|
||||
action="run",
|
||||
)
|
||||
semantic_record = get_definition(
|
||||
session,
|
||||
principal,
|
||||
@@ -138,6 +156,13 @@ def execute_report(
|
||||
"The report's pinned semantic model is unavailable or inactive."
|
||||
)
|
||||
semantic = SemanticModelDefinition.model_validate(semantic_record.payload)
|
||||
semantic_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=semantic_record,
|
||||
action="view",
|
||||
)
|
||||
dataset_record = get_definition(
|
||||
session,
|
||||
principal,
|
||||
@@ -150,8 +175,15 @@ def execute_report(
|
||||
"The report's pinned analytical dataset is unavailable or inactive."
|
||||
)
|
||||
dataset = DatasetDefinition.model_validate(dataset_record.payload)
|
||||
dataset_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=dataset_record,
|
||||
action="view",
|
||||
)
|
||||
bound_parameters = _bind_parameters(report, parameters)
|
||||
effective_query = query or report.default_query
|
||||
effective_query = _enforce_query_access(report, query or report.default_query)
|
||||
clean_idempotency_key = _required(
|
||||
idempotency_key,
|
||||
"Reporting execution idempotency key",
|
||||
@@ -176,7 +208,19 @@ def execute_report(
|
||||
request_sha256=request_sha256,
|
||||
)
|
||||
if replay is not None:
|
||||
return _execution_payload(replay, report=report, registry=registry)
|
||||
delivery = _authorize_execution_delivery(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
row=replay,
|
||||
report_record=report_record,
|
||||
)
|
||||
return _execution_payload(
|
||||
replay,
|
||||
report=report,
|
||||
registry=registry,
|
||||
delivery_authorization=delivery,
|
||||
)
|
||||
started_at = utc_now()
|
||||
execution = ReportingExecution(
|
||||
tenant_id=_tenant(principal),
|
||||
@@ -232,7 +276,14 @@ def execute_report(
|
||||
output_hash=source.output_hash,
|
||||
source_fingerprints=source.source_fingerprints,
|
||||
)
|
||||
result = execute_semantic_query(authorized_rows, semantic, effective_query)
|
||||
result = execute_postgres_query(
|
||||
session,
|
||||
rows=authorized_rows,
|
||||
dataset=dataset,
|
||||
semantic_model=semantic,
|
||||
query=effective_query,
|
||||
) or execute_semantic_query(authorized_rows, semantic, effective_query)
|
||||
diagnostics.extend(result.diagnostics)
|
||||
output_hash = _sha256(
|
||||
{
|
||||
"rows": result.rows,
|
||||
@@ -244,7 +295,12 @@ def execute_report(
|
||||
execution.status = "succeeded"
|
||||
execution.source_fingerprints = _json_value(source.source_fingerprints)
|
||||
execution.output_hash = output_hash
|
||||
execution.executor_version = f"{QUERY_ENGINE_VERSION}+{source.executor_version}"
|
||||
planner_version = (
|
||||
POSTGRES_PLANNER_VERSION
|
||||
if any(item.get("code") == "postgresql_semantic_plan" for item in result.diagnostics)
|
||||
else QUERY_ENGINE_VERSION
|
||||
)
|
||||
execution.executor_version = f"{planner_version}+{source.executor_version}"
|
||||
execution.result_schema = list(result.schema)
|
||||
execution.result_rows = list(result.rows)
|
||||
execution.total_rows = result.total_rows
|
||||
@@ -259,11 +315,35 @@ def execute_report(
|
||||
"report_content_hash": report_record.content_hash,
|
||||
"semantic_model_content_hash": semantic_record.content_hash,
|
||||
"dataset_content_hash": dataset_record.content_hash,
|
||||
"definition_governance": {
|
||||
"report": report_decision.to_dict(),
|
||||
"semantic_model": semantic_decision.to_dict(),
|
||||
"dataset": dataset_decision.to_dict(),
|
||||
},
|
||||
"access_explanation": _access_explanation(
|
||||
report,
|
||||
effective_query,
|
||||
source_rows=len(normalized_rows),
|
||||
authorized_rows=len(authorized_rows),
|
||||
row_policy=policy_provenance,
|
||||
),
|
||||
}
|
||||
execution.finished_at = utc_now()
|
||||
session.flush()
|
||||
_emit_execution_event(session, execution, report_record.name)
|
||||
return _execution_payload(execution, report=report, registry=registry)
|
||||
delivery = _authorize_execution_delivery(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
row=execution,
|
||||
report_record=report_record,
|
||||
)
|
||||
return _execution_payload(
|
||||
execution,
|
||||
report=report,
|
||||
registry=registry,
|
||||
delivery_authorization=delivery,
|
||||
)
|
||||
except Exception as exc:
|
||||
execution.status = "failed"
|
||||
execution.finished_at = utc_now()
|
||||
@@ -284,6 +364,7 @@ def get_execution(
|
||||
principal: object,
|
||||
*,
|
||||
execution_id: str,
|
||||
registry: object | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
row = (
|
||||
session.query(ReportingExecution)
|
||||
@@ -304,10 +385,18 @@ def get_execution(
|
||||
)
|
||||
if report_record is None:
|
||||
return None
|
||||
delivery = _authorize_execution_delivery(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
row=row,
|
||||
report_record=report_record,
|
||||
)
|
||||
return _execution_payload(
|
||||
row,
|
||||
report=ReportDefinition.model_validate(report_record.payload),
|
||||
registry=None,
|
||||
registry=registry,
|
||||
delivery_authorization=delivery,
|
||||
)
|
||||
|
||||
|
||||
@@ -317,16 +406,15 @@ def list_executions(
|
||||
*,
|
||||
report_id: str,
|
||||
limit: int = 100,
|
||||
registry: object | None = None,
|
||||
) -> tuple[dict[str, object], ...]:
|
||||
if (
|
||||
get_definition(
|
||||
session,
|
||||
principal,
|
||||
definition_kind="report",
|
||||
definition_id=report_id,
|
||||
)
|
||||
is None
|
||||
):
|
||||
current_report = get_definition(
|
||||
session,
|
||||
principal,
|
||||
definition_kind="report",
|
||||
definition_id=report_id,
|
||||
)
|
||||
if current_report is None:
|
||||
return ()
|
||||
rows = (
|
||||
session.query(ReportingExecution)
|
||||
@@ -338,7 +426,46 @@ def list_executions(
|
||||
.limit(max(1, min(limit, 200)))
|
||||
.all()
|
||||
)
|
||||
return tuple(_execution_payload(row, report=None, registry=None) for row in rows)
|
||||
authorization_cache: dict[tuple[int, int, int], dict[str, object]] = {}
|
||||
payloads: list[dict[str, object]] = []
|
||||
for row in rows:
|
||||
key = (
|
||||
row.report_revision,
|
||||
row.semantic_model_revision,
|
||||
row.dataset_revision,
|
||||
)
|
||||
report_record = (
|
||||
current_report
|
||||
if current_report.revision == row.report_revision
|
||||
else get_definition(
|
||||
session,
|
||||
principal,
|
||||
definition_kind="report",
|
||||
definition_id=row.report_id,
|
||||
revision=row.report_revision,
|
||||
)
|
||||
)
|
||||
if report_record is None:
|
||||
continue
|
||||
delivery = authorization_cache.get(key)
|
||||
if delivery is None:
|
||||
delivery = _authorize_execution_delivery(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
row=row,
|
||||
report_record=report_record,
|
||||
)
|
||||
authorization_cache[key] = delivery
|
||||
payloads.append(
|
||||
_execution_payload(
|
||||
row,
|
||||
report=ReportDefinition.model_validate(report_record.payload),
|
||||
registry=registry,
|
||||
delivery_authorization=delivery,
|
||||
)
|
||||
)
|
||||
return tuple(payloads)
|
||||
|
||||
|
||||
def _read_dataset(
|
||||
@@ -717,11 +844,144 @@ def _evaluate_assertion(
|
||||
}
|
||||
|
||||
|
||||
def _enforce_query_access(
|
||||
report: ReportDefinition,
|
||||
query: ReportQuery,
|
||||
) -> ReportQuery:
|
||||
policy = report.access_policy
|
||||
hidden_dimensions = _policy_strings(policy, "hidden_dimensions")
|
||||
hidden_measures = _policy_strings(policy, "hidden_measures")
|
||||
requested_dimensions = set(query.dimensions)
|
||||
requested_dimensions.update(item.dimension for item in query.filters)
|
||||
if query.pivot is not None:
|
||||
requested_dimensions.update(query.pivot.rows)
|
||||
requested_dimensions.update(query.pivot.columns)
|
||||
requested_measures = set(query.measures)
|
||||
if query.pivot is not None:
|
||||
requested_measures.update(query.pivot.measures)
|
||||
blocked = (requested_dimensions & hidden_dimensions) | (
|
||||
requested_measures & hidden_measures
|
||||
)
|
||||
if blocked:
|
||||
raise PermissionError(
|
||||
"Policy hides requested Reporting fields: "
|
||||
+ ", ".join(sorted(blocked))
|
||||
)
|
||||
if "run" in _policy_strings(policy, "disabled_actions"):
|
||||
raise PermissionError(_policy_reason(policy, "run"))
|
||||
return query
|
||||
|
||||
|
||||
def _access_explanation(
|
||||
report: ReportDefinition,
|
||||
query: ReportQuery,
|
||||
*,
|
||||
source_rows: int,
|
||||
authorized_rows: int,
|
||||
row_policy: Mapping[str, object],
|
||||
) -> dict[str, object]:
|
||||
policy = report.access_policy
|
||||
hidden_dimensions = sorted(_policy_strings(policy, "hidden_dimensions"))
|
||||
hidden_measures = sorted(_policy_strings(policy, "hidden_measures"))
|
||||
disabled_actions = sorted(_policy_strings(policy, "disabled_actions"))
|
||||
reasons = policy.get("reasons")
|
||||
return {
|
||||
"hidden_dimensions": hidden_dimensions,
|
||||
"hidden_measures": hidden_measures,
|
||||
"hidden_rows": max(0, source_rows - authorized_rows),
|
||||
"disabled_actions": disabled_actions,
|
||||
"reasons": dict(reasons) if isinstance(reasons, Mapping) else {},
|
||||
"row_policy": dict(row_policy),
|
||||
"effective_query": query.model_dump(mode="json"),
|
||||
}
|
||||
|
||||
|
||||
def _authorize_execution_delivery(
|
||||
session: Session,
|
||||
principal: object,
|
||||
*,
|
||||
registry: object | None,
|
||||
row: ReportingExecution,
|
||||
report_record: ReportingDefinitionRecord,
|
||||
) -> dict[str, object]:
|
||||
report_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=report_record,
|
||||
action="view",
|
||||
)
|
||||
semantic_record = get_definition(
|
||||
session,
|
||||
principal,
|
||||
definition_kind="semantic_model",
|
||||
definition_id=row.semantic_model_id,
|
||||
revision=row.semantic_model_revision,
|
||||
)
|
||||
dataset_record = get_definition(
|
||||
session,
|
||||
principal,
|
||||
definition_kind="dataset",
|
||||
definition_id=row.dataset_id,
|
||||
revision=row.dataset_revision,
|
||||
)
|
||||
if semantic_record is None or dataset_record is None:
|
||||
raise PermissionError(
|
||||
"The source definitions for this report result are no longer accessible."
|
||||
)
|
||||
semantic_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=semantic_record,
|
||||
action="view",
|
||||
)
|
||||
dataset_decision = require_definition_action(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
record=dataset_record,
|
||||
action="view",
|
||||
)
|
||||
dataset = DatasetDefinition.model_validate(dataset_record.payload)
|
||||
_empty, row_policy = _apply_row_policy(
|
||||
session,
|
||||
principal,
|
||||
registry=registry,
|
||||
dataset_id=dataset_record.definition_id,
|
||||
dataset_revision=dataset_record.revision,
|
||||
dataset=dataset,
|
||||
rows=(),
|
||||
)
|
||||
return {
|
||||
"checked": True,
|
||||
"report": report_decision.to_dict(),
|
||||
"semantic_model": semantic_decision.to_dict(),
|
||||
"dataset": dataset_decision.to_dict(),
|
||||
"row_policy": dict(row_policy),
|
||||
}
|
||||
|
||||
|
||||
def _policy_strings(policy: Mapping[str, object], key: str) -> set[str]:
|
||||
raw = policy.get(key, ())
|
||||
if not isinstance(raw, (list, tuple, set, frozenset)):
|
||||
return set()
|
||||
return {str(item) for item in raw if str(item).strip()}
|
||||
|
||||
|
||||
def _policy_reason(policy: Mapping[str, object], action: str) -> str:
|
||||
reasons = policy.get("reasons")
|
||||
if isinstance(reasons, Mapping) and str(reasons.get(action) or "").strip():
|
||||
return str(reasons[action])
|
||||
return f"Policy disables the Reporting {action} action."
|
||||
|
||||
|
||||
def _execution_payload(
|
||||
row: ReportingExecution,
|
||||
*,
|
||||
report: ReportDefinition | None,
|
||||
registry: object | None,
|
||||
delivery_authorization: Mapping[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
payload: dict[str, object] = {
|
||||
"execution_id": row.execution_id,
|
||||
@@ -747,6 +1007,7 @@ def _execution_payload(
|
||||
"started_at": _datetime_text(row.started_at),
|
||||
"finished_at": _datetime_text(row.finished_at),
|
||||
"actor_id": row.actor_id,
|
||||
"delivery_authorization": dict(delivery_authorization or {}),
|
||||
}
|
||||
if row.status == "succeeded" and report is not None:
|
||||
result = QueryResult(
|
||||
|
||||
Reference in New Issue
Block a user