Implement AUDIT-WP-0006 honest operational custody.
Postgres now reports custody_class=operational with a cited 30-day recoverable window. Join ITC-CAP operations.audit at D4, publish the interface card, and overlay user-engine tenants [*] from Git so an ExternalSecret refresh cannot shrink it.
This commit is contained in:
parent
0a3d05ff1c
commit
ded432a63f
25 changed files with 832 additions and 94 deletions
|
|
@ -17,6 +17,7 @@ import hmac
|
|||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from audit_core.redaction import POLICIES, POLICY_REDACT
|
||||
|
|
@ -113,7 +114,7 @@ class SenderRegistry:
|
|||
env = env if env is not None else dict(os.environ)
|
||||
raw = env.get("AUDIT_CORE_SENDERS")
|
||||
if raw:
|
||||
return cls(_parse_identities(raw))
|
||||
return cls(_apply_scope_overlay(_parse_identities(raw), env))
|
||||
|
||||
legacy = (env.get("AUDIT_CORE_INGEST_TOKEN") or "").strip()
|
||||
if not legacy:
|
||||
|
|
@ -134,6 +135,66 @@ class SenderRegistry:
|
|||
])
|
||||
|
||||
|
||||
def _load_scope_overlay(env: dict[str, str]) -> list[dict[str, Any]]:
|
||||
"""Non-secret sender policy. Tokens never come from here.
|
||||
|
||||
``AUDIT_CORE_SENDERS_SCOPE`` is inline JSON (tests).
|
||||
``AUDIT_CORE_SENDERS_SCOPE_PATH`` is a file (production ConfigMap).
|
||||
"""
|
||||
inline = env.get("AUDIT_CORE_SENDERS_SCOPE")
|
||||
if inline:
|
||||
payload = json.loads(inline)
|
||||
else:
|
||||
path = env.get("AUDIT_CORE_SENDERS_SCOPE_PATH")
|
||||
if not path:
|
||||
return []
|
||||
payload = json.loads(Path(path).read_text())
|
||||
if not isinstance(payload, list):
|
||||
raise ValueError("sender scope overlay must be a JSON list")
|
||||
return [entry for entry in payload if isinstance(entry, dict) and entry.get("name")]
|
||||
|
||||
|
||||
def _apply_scope_overlay(
|
||||
identities: list[SenderIdentity], env: dict[str, str]
|
||||
) -> list[SenderIdentity]:
|
||||
"""Overlay non-secret fields from Git/ConfigMap onto Secret-backed tokens.
|
||||
|
||||
ExternalSecret refresh cannot shrink ``user-engine`` tenants below the
|
||||
declared scope (AUDIT-WP-0006-T05). Tokens are never taken from the overlay.
|
||||
"""
|
||||
overlay = {entry["name"]: entry for entry in _load_scope_overlay(env)}
|
||||
if not overlay:
|
||||
return identities
|
||||
merged: list[SenderIdentity] = []
|
||||
for identity in identities:
|
||||
extra = overlay.get(identity.name)
|
||||
if extra is None:
|
||||
merged.append(identity)
|
||||
continue
|
||||
tenants = extra.get("tenants")
|
||||
sources = extra.get("sources")
|
||||
merged.append(
|
||||
SenderIdentity(
|
||||
name=identity.name,
|
||||
tokens=identity.tokens,
|
||||
sources=(
|
||||
frozenset(str(s) for s in sources) if sources else identity.sources
|
||||
),
|
||||
tenants=(
|
||||
frozenset(str(t) for t in tenants) if tenants else identity.tenants
|
||||
),
|
||||
may_write=(
|
||||
bool(extra["may_write"]) if "may_write" in extra else identity.may_write
|
||||
),
|
||||
may_read=(
|
||||
bool(extra["may_read"]) if "may_read" in extra else identity.may_read
|
||||
),
|
||||
secret_policy=identity.secret_policy,
|
||||
)
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
def _parse_identities(raw: str) -> list[SenderIdentity]:
|
||||
try:
|
||||
entries = json.loads(raw)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue