migrations/0004_breach_records.sql models a case's lifecycle as
append-only events (alleged/cured/determined/terminated) grouped by
case_id rather than one mutable row - resolution is always a new,
later event, never an edit. A CHECK constraint makes the
anonymized-default rule (License V1C1 §7.4) a database fact:
named_entitlement_holder can be set if and only if anonymized = false.
src/target_revenue/breach_record.py's publish_breach_event() enforces
per-Licensor phase ownership and rejects named-disclosure requests
that don't also set named_disclosure_authorized_under_cua: true - the
Trust Service records the Licensor's assertion that the CUA's naming
clause authorizes it, it never verifies the underlying CUA text
itself. Signs every event with the same instance Ed25519 key already
used for Ledger entries and Attestations.
Adds POST/GET /phases/{id}/breach-records. Guarded the .registry
import behind a lazy in-function import (matching attestation.py's
TYPE_CHECKING pattern) so tests/test_breach_record.py (7 tests) runs
under plain system Python with no psycopg dependency. 5 new
Docker-gated tests cover the default-anonymized lifecycle, the
named-disclosure authorization gate, cross-Licensor rejection,
signature verification, and DB-level UPDATE/DELETE rejection.
This closes WP-0006 again - all 9 tasks done.
48 lines
2.2 KiB
PL/PgSQL
48 lines
2.2 KiB
PL/PgSQL
-- WP-0006-T09: Breach/Compliance Record hosting.
|
|
-- Depends on migrations/0001_registries.sql.
|
|
--
|
|
-- Per License V1C1 §7.4: a breach record's lifecycle is a sequence of
|
|
-- distinct, dated events (alleged -> cured, or alleged -> determined ->
|
|
-- terminated), never an edit of a prior event. "The Trust Service shall
|
|
-- update the record promptly upon resolution" (§7.4) means append a new
|
|
-- event for the same case, not mutate the existing one — the same
|
|
-- append-only pattern as phase_manifests/ledger_entries/attestations.
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS breach_records (
|
|
sequence bigint GENERATED ALWAYS AS IDENTITY,
|
|
record_id text NOT NULL UNIQUE,
|
|
case_id text NOT NULL,
|
|
phase_id text NOT NULL REFERENCES phase_manifests(phase_id),
|
|
event_type text NOT NULL
|
|
CHECK (event_type IN ('alleged', 'cured', 'determined', 'terminated')),
|
|
category text NOT NULL,
|
|
event_at timestamptz NOT NULL,
|
|
anonymized boolean NOT NULL DEFAULT true,
|
|
named_entitlement_holder text,
|
|
evidence_reference text,
|
|
published_by text NOT NULL REFERENCES licensors(licensor_id),
|
|
signature text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (phase_id, sequence),
|
|
-- License V1C1 §7.4: naming is governed exclusively by the applicable
|
|
-- Commercial Use Agreement's opt-in; a name may only be stored
|
|
-- alongside an explicit non-anonymized record, never as a side effect
|
|
-- of an anonymized one.
|
|
CHECK (anonymized OR named_entitlement_holder IS NOT NULL),
|
|
CHECK (NOT anonymized OR named_entitlement_holder IS NULL)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS breach_records_phase_sequence_idx
|
|
ON breach_records (phase_id, sequence);
|
|
CREATE INDEX IF NOT EXISTS breach_records_case_idx
|
|
ON breach_records (case_id);
|
|
|
|
GRANT SELECT, INSERT ON breach_records TO trf_app;
|
|
-- Deliberately no UPDATE, no DELETE for trf_app: matching phase_manifests,
|
|
-- ledger_entries, and attestations, a published breach event is
|
|
-- permanent — resolution is recorded as a new event, never an edit.
|
|
GRANT USAGE, SELECT ON breach_records_sequence_seq TO trf_app;
|
|
|
|
COMMIT;
|