Register informed-decision.memo.posed for the platform tenant.
Accept the retained pose envelope and refuse packet, brief, and secret content. Heartbeat and reconciliation follow the existing load-bearing class pattern. This does not connect the sink or claim runtime readiness. Assistant: grok Assistant-Session: 01a10329-31cb-78d3-b7a3-787a69c89d54
This commit is contained in:
parent
fdca5758e1
commit
70e4044128
16 changed files with 534 additions and 10 deletions
|
|
@ -45,6 +45,7 @@ from audit_core.redaction import (
|
|||
SecretFieldRejection,
|
||||
apply_policy,
|
||||
finding_from_path,
|
||||
scan,
|
||||
)
|
||||
from audit_core.senders import WILDCARD, SenderIdentity, SenderRegistry, development_registry
|
||||
from audit_core.sqlite_backend import SQLiteAuditBackend
|
||||
|
|
@ -552,11 +553,71 @@ class IngestionApplication:
|
|||
return [body]
|
||||
|
||||
|
||||
# Local registration of informed-decision.memo.posed (intake dependency T07).
|
||||
# The retained envelope is the one informed-decision already queues: the eight
|
||||
# wire fields, with data limited to the six ids and digests below. Packet,
|
||||
# brief, and secret-shaped keys are refused before any other error so a later
|
||||
# invalid_event cannot write that body into the dead-letter store.
|
||||
POSED_EVENT_TYPE = "informed-decision.memo.posed"
|
||||
POSED_SOURCE = "informed-decision"
|
||||
POSED_TENANT = "tenant:platform"
|
||||
POSED_DATA_KEYS = frozenset({
|
||||
"memo_id",
|
||||
"approval_id",
|
||||
"approval_binding_digest",
|
||||
"submitter",
|
||||
"memo_class",
|
||||
"submission_digest",
|
||||
})
|
||||
CONTENT_REFUSED = "content_refused"
|
||||
_POSED_CONTENT_KEYS = frozenset({"packet", "brief"})
|
||||
|
||||
|
||||
def _has_posed_content_key(value: Any) -> bool:
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
if str(key).lower() in _POSED_CONTENT_KEYS or _has_posed_content_key(item):
|
||||
return True
|
||||
elif isinstance(value, list):
|
||||
return any(_has_posed_content_key(item) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def _refuse_posed_content(payload: dict[str, Any]) -> None:
|
||||
"""Refuse packet, brief, and secret-shaped keys on this class only.
|
||||
|
||||
Other event types keep the sender's secret_policy. This class does not:
|
||||
the dependency says refuse, and redacting would still accept the event.
|
||||
"""
|
||||
findings = scan(payload)
|
||||
if findings:
|
||||
raise SecretFieldRejection(findings)
|
||||
if _has_posed_content_key(payload):
|
||||
raise ValueError(CONTENT_REFUSED)
|
||||
|
||||
|
||||
def _require_posed_envelope(payload: dict[str, Any]) -> None:
|
||||
"""Accept only the locally retained field set, for this source and tenant."""
|
||||
if str(payload.get("source") or "") != POSED_SOURCE:
|
||||
raise ValueError("source_not_allowed")
|
||||
if str(payload.get("tenant") or "") != POSED_TENANT:
|
||||
raise ValueError("tenant_not_allowed")
|
||||
data = payload.get("data")
|
||||
if (
|
||||
not isinstance(data, dict)
|
||||
or set(data) != POSED_DATA_KEYS
|
||||
or any(type(data[key]) is not str or not data[key].strip() for key in POSED_DATA_KEYS)
|
||||
):
|
||||
raise ValueError("invalid_event")
|
||||
|
||||
|
||||
def normalize(
|
||||
payload: dict[str, Any],
|
||||
idempotency_key: str | None,
|
||||
identity: SenderIdentity | None = None,
|
||||
) -> AuditEvent:
|
||||
if isinstance(payload, dict) and payload.get("type") == POSED_EVENT_TYPE:
|
||||
_refuse_posed_content(payload)
|
||||
required = (
|
||||
"id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data",
|
||||
)
|
||||
|
|
@ -575,6 +636,8 @@ def normalize(
|
|||
raise ValueError("tenant_not_allowed")
|
||||
elif source != "user-engine":
|
||||
raise ValueError("source_not_allowed")
|
||||
if payload.get("type") == POSED_EVENT_TYPE:
|
||||
_require_posed_envelope(payload)
|
||||
observed_at = _normalize_timestamp(payload["occurred_at"])
|
||||
policy = identity.secret_policy if identity is not None else POLICY_REDACT
|
||||
# Raises SecretFieldRejection under the reject policy; that exception
|
||||
|
|
|
|||
|
|
@ -166,10 +166,10 @@ MIGRATIONS: list[tuple[str, str]] = [
|
|||
),
|
||||
]
|
||||
|
||||
# Rejection reasons whose payload must never be persisted — storing the body of
|
||||
# an event rejected *for carrying secret-shaped material* would write that
|
||||
# material into the audit store.
|
||||
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field"})
|
||||
# Rejection reasons whose payload must never be persisted. Storing the body of
|
||||
# an event rejected for secret-shaped material, or for packet/brief content,
|
||||
# would write that material into the audit store.
|
||||
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field", "content_refused"})
|
||||
|
||||
|
||||
class PostgresAuditBackend:
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ CREATE TABLE IF NOT EXISTS secret_findings (
|
|||
"""
|
||||
|
||||
# Rejection reasons whose payload must never be persisted. Storing the body of
|
||||
# an event rejected *for containing secret-shaped material* would write that
|
||||
# material into the audit store — the precise outcome the rejection prevents.
|
||||
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field"})
|
||||
# an event rejected for secret-shaped material, or for packet/brief content,
|
||||
# would write that material into the audit store.
|
||||
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field", "content_refused"})
|
||||
|
||||
|
||||
class SQLiteAuditBackend:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue