chore: consolidate platform split checks
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal
|
||||
from govoplan_access.auth import ApiPrincipal
|
||||
from govoplan_core.core.change_sequence import record_change
|
||||
from govoplan_core.core.events import current_event_trace, normalize_trace_id
|
||||
from govoplan_core.privacy.retention import sanitize_audit_details_for_policy
|
||||
from govoplan_audit.backend.db.models import AuditLog
|
||||
|
||||
|
||||
AUDIT_MODULE_ID = "audit"
|
||||
AUDIT_TENANT_EVENTS_COLLECTION = "audit.admin.tenant_events"
|
||||
AUDIT_SYSTEM_EVENTS_COLLECTION = "audit.admin.system_events"
|
||||
|
||||
SENSITIVE_DETAIL_KEYS = {
|
||||
"password",
|
||||
"smtp_password",
|
||||
@@ -16,9 +23,40 @@ SENSITIVE_DETAIL_KEYS = {
|
||||
"secret",
|
||||
"api_key",
|
||||
"token",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"authorization",
|
||||
"cookie",
|
||||
"credentials",
|
||||
"credential",
|
||||
"private_key",
|
||||
"claim_token",
|
||||
"build_token",
|
||||
}
|
||||
|
||||
|
||||
def _optional_text(value: object | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
candidate = str(value).strip()
|
||||
return candidate or None
|
||||
|
||||
|
||||
def _compact_trace(trace: Mapping[str, object] | None) -> dict[str, str]:
|
||||
if not isinstance(trace, Mapping):
|
||||
return {}
|
||||
compact: dict[str, str] = {}
|
||||
correlation_id = _optional_text(trace.get("correlation_id"))
|
||||
causation_id = _optional_text(trace.get("causation_id"))
|
||||
clean_correlation_id = normalize_trace_id(correlation_id)
|
||||
clean_causation_id = normalize_trace_id(causation_id)
|
||||
if clean_correlation_id:
|
||||
compact["correlation_id"] = clean_correlation_id
|
||||
if clean_causation_id:
|
||||
compact["causation_id"] = clean_causation_id
|
||||
return compact
|
||||
|
||||
|
||||
def _sanitize_details(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
sanitized: dict[str, Any] = {}
|
||||
@@ -33,6 +71,70 @@ def _sanitize_details(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def audit_operation_context(
|
||||
*,
|
||||
module_id: object | None = None,
|
||||
request_id: object | None = None,
|
||||
run_id: object | None = None,
|
||||
outcome: object | None = None,
|
||||
trace: Mapping[str, object] | None = None,
|
||||
**details: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Build concise operational audit details for admin and lifecycle actions."""
|
||||
|
||||
payload: dict[str, Any] = {}
|
||||
for key, value in (
|
||||
("module_id", module_id),
|
||||
("request_id", request_id),
|
||||
("run_id", run_id),
|
||||
("outcome", outcome),
|
||||
):
|
||||
text = _optional_text(value)
|
||||
if text is not None:
|
||||
payload[key] = text
|
||||
for key, value in details.items():
|
||||
if value is not None:
|
||||
payload[key] = _sanitize_details(value)
|
||||
compact_trace = _compact_trace(trace)
|
||||
if compact_trace:
|
||||
existing = payload.get("_trace")
|
||||
payload["_trace"] = {**existing, **compact_trace} if isinstance(existing, dict) else compact_trace
|
||||
return payload
|
||||
|
||||
|
||||
def _trace_details(
|
||||
details: dict[str, Any],
|
||||
*,
|
||||
correlation_id: str | None = None,
|
||||
causation_id: str | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, str]]:
|
||||
active_trace = current_event_trace()
|
||||
trace: dict[str, str] = {}
|
||||
clean_correlation_id = normalize_trace_id(correlation_id)
|
||||
clean_causation_id = normalize_trace_id(causation_id)
|
||||
if clean_correlation_id or active_trace:
|
||||
trace["correlation_id"] = clean_correlation_id or active_trace.correlation_id
|
||||
if clean_causation_id or (active_trace and active_trace.causation_id):
|
||||
trace["causation_id"] = clean_causation_id or str(active_trace.causation_id)
|
||||
if not trace:
|
||||
return details, {}
|
||||
merged = dict(details)
|
||||
existing = merged.get("_trace")
|
||||
merged["_trace"] = {**existing, **trace} if isinstance(existing, dict) else trace
|
||||
return merged, trace
|
||||
|
||||
|
||||
def _restore_trace_after_policy(details: dict[str, Any], trace: dict[str, str]) -> dict[str, Any]:
|
||||
if not trace:
|
||||
return details
|
||||
if details.get("_trace") == trace:
|
||||
return details
|
||||
restored = dict(details)
|
||||
existing = restored.get("_trace")
|
||||
restored["_trace"] = {**existing, **trace} if isinstance(existing, dict) else trace
|
||||
return restored
|
||||
|
||||
|
||||
def audit_event(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -44,6 +146,8 @@ def audit_event(
|
||||
object_type: str | None = None,
|
||||
object_id: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
correlation_id: str | None = None,
|
||||
causation_id: str | None = None,
|
||||
commit: bool = False,
|
||||
) -> AuditLog:
|
||||
"""Persist one audit event.
|
||||
@@ -56,6 +160,16 @@ def audit_event(
|
||||
if scope not in {"tenant", "system"}:
|
||||
raise ValueError(f"Unsupported audit scope: {scope}")
|
||||
|
||||
traced_details, trace = _trace_details(
|
||||
_sanitize_details(details or {}),
|
||||
correlation_id=correlation_id,
|
||||
causation_id=causation_id,
|
||||
)
|
||||
stored_details = _restore_trace_after_policy(
|
||||
sanitize_audit_details_for_policy(session, traced_details),
|
||||
trace,
|
||||
)
|
||||
|
||||
item = AuditLog(
|
||||
scope=scope,
|
||||
tenant_id=tenant_id,
|
||||
@@ -64,13 +178,24 @@ def audit_event(
|
||||
action=action,
|
||||
object_type=object_type,
|
||||
object_id=object_id,
|
||||
details=sanitize_audit_details_for_policy(session, _sanitize_details(details or {})),
|
||||
details=stored_details,
|
||||
)
|
||||
session.add(item)
|
||||
session.flush()
|
||||
record_change(
|
||||
session,
|
||||
module_id=AUDIT_MODULE_ID,
|
||||
collection=AUDIT_SYSTEM_EVENTS_COLLECTION if scope == "system" else AUDIT_TENANT_EVENTS_COLLECTION,
|
||||
resource_type="audit_log",
|
||||
resource_id=item.id,
|
||||
operation="created",
|
||||
tenant_id=None if scope == "system" else tenant_id,
|
||||
actor_type="user" if user_id else ("api_key" if api_key_id else None),
|
||||
actor_id=user_id or api_key_id,
|
||||
payload={"scope": scope, "action": action, "object_type": object_type, "object_id": object_id},
|
||||
)
|
||||
if commit:
|
||||
session.commit()
|
||||
else:
|
||||
session.flush()
|
||||
return item
|
||||
|
||||
|
||||
@@ -83,6 +208,8 @@ def audit_from_principal(
|
||||
object_type: str | None = None,
|
||||
object_id: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
correlation_id: str | None = None,
|
||||
causation_id: str | None = None,
|
||||
commit: bool = False,
|
||||
) -> AuditLog:
|
||||
return audit_event(
|
||||
@@ -95,5 +222,7 @@ def audit_from_principal(
|
||||
object_type=object_type,
|
||||
object_id=object_id,
|
||||
details=details,
|
||||
correlation_id=correlation_id,
|
||||
causation_id=causation_id,
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user