fix(schema): address Thufir pass 1 blockers on NIP-FI PR 2

Three IMPORTANT findings fixed:

1. Drop effective_at monotonicity from policy revision guard
   identity_enrollment_policy_revision_guard_v1() now enforces only
   strict-greater policy_revision per community. The effective_at check
   had no NIP-FI basis (FI-INV-06 defines assertion_policy_id stability,
   not revision chronology) and would reject legitimately-sequenced
   revisions — the downstream constructor stamps every immediately-
   effective revision with Unix epoch, so revision 2 would fail after
   revision 1 under the old guard.

2. Bind semantic coordinates in denial attempt guard
   authorization_authentication_denial_attempts gains attempt_id UUID
   NOT NULL with a deferred FK to authorization_events on
   (community_id, operation_id, event_kind, attempt_id). The guard
   authorization_denial_attempt_guard_v1() now additionally compares
   correlation_id and reason_code between the event and its denial
   attempt row, raising check_violation (23514) with named constraint
   authorization_denial_attempt_semantic_binding on mismatch. This
   closes the Carl finding 3 gap: a kind-9 event for correlation A /
   reason X can no longer be paired with a denial row carrying
   correlation B / reason Y.

3. Rewrite regression tests to prove the contracts
   - Policy test: seeds a gap (100->101) then inserts unused revision 99
     and asserts 23514 from the named guard (not 23505, which would
     fire on a PK duplicate and not prove the monotonic comparison).
     Adds a two-transaction concurrency regression: two distinct forward
     revisions (102, 103) race through separate connections; both commit
     because the advisory lock serializes them and each is valid.
   - Denial test: replaces the ambiguous negative-B (23503 OR 23514)
     with three single-coordinate-mismatch cases attributed to the named
     guard: B1 correlation_id mismatch (23514), B2 reason_code mismatch
     (23514), B3 attempt_id mismatch (23503 via deferred FK).
   - Admission test: adds negative C -- mismatched request_fingerprint
     rejected by the immediate composite FK (23503) at INSERT, proving
     the coordinate binding half of Carl finding 2.

All four changed files byte-identical between migration files and
schema/schema.sql (verified by extraction+cmp). All 5 NIP-FI tests,
admin_schema_parity, and 2 unit tests pass.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
This commit is contained in:
Hayt
2026-08-29 14:47:08 -04:00
parent 1016f0a57e
commit 12686a6cd9
4 changed files with 466 additions and 111 deletions

View File

@@ -2309,16 +2309,14 @@ CREATE INDEX identity_lifecycle_selectors_asserted_history
(community_id, asserted_history_id, selector_kind);
-- Serializes policy-revision inserts per community: each new revision must
-- strictly exceed the current maximum, and each effective_at must strictly
-- exceed the current maximum effective_at (FI-INV-06 — stable assertion
-- policy; a revision that moves either coordinate backward is incoherent).
-- The per-community advisory lock prevents two concurrent writers from both
-- passing a plain SELECT MAX() check and committing conflicting revisions.
-- strictly exceed the current maximum (FI-INV-06 — stable assertion policy
-- anchor; a backfilled or replayed revision is incoherent). The per-community
-- advisory lock prevents two concurrent writers from both passing a plain
-- SELECT MAX() check and committing conflicting revisions.
CREATE FUNCTION identity_enrollment_policy_revision_guard_v1() RETURNS TRIGGER AS $$
DECLARE
lock_key BIGINT;
max_revision BIGINT;
max_effective_at TIMESTAMPTZ;
BEGIN
-- Acquire a per-community exclusive transaction-scoped advisory lock so
-- that concurrent insertions serialize here. The key is a stable hash of
@@ -2329,8 +2327,8 @@ BEGIN
);
PERFORM pg_advisory_xact_lock(lock_key);
SELECT MAX(policy_revision), MAX(effective_at)
INTO max_revision, max_effective_at
SELECT MAX(policy_revision)
INTO max_revision
FROM identity_enrollment_policies
WHERE community_id = NEW.community_id;
@@ -2344,16 +2342,6 @@ BEGIN
CONSTRAINT = 'identity_enrollment_policy_revision_monotonic';
END IF;
IF max_effective_at IS NOT NULL
AND NEW.effective_at <= max_effective_at
THEN
RAISE EXCEPTION
'effective_at % does not strictly exceed current maximum % for community %',
NEW.effective_at, max_effective_at, NEW.community_id
USING ERRCODE = 'check_violation',
CONSTRAINT = 'identity_enrollment_policy_revision_monotonic';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -3031,6 +3019,7 @@ CREATE TABLE authorization_authentication_denial_attempts (
reason_code SMALLINT NOT NULL CHECK (
reason_code IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
),
attempt_id UUID NOT NULL,
audit_event_id UUID NOT NULL,
audit_event_kind SMALLINT NOT NULL DEFAULT 9 CHECK (audit_event_kind = 9),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT transaction_timestamp(),
@@ -3045,8 +3034,12 @@ CREATE TABLE authorization_authentication_denial_attempts (
FOREIGN KEY (community_id, audit_event_id, audit_event_kind, operation_id)
REFERENCES authorization_events (community_id, event_id, event_kind, operation_id)
DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (community_id, operation_id, audit_event_kind, attempt_id)
REFERENCES authorization_events (community_id, operation_id, event_kind, attempt_id)
DEFERRABLE INITIALLY DEFERRED,
CHECK (operation_id <> '00000000-0000-0000-0000-000000000000'::uuid),
CHECK (correlation_id <> '00000000-0000-0000-0000-000000000000'::uuid)
CHECK (correlation_id <> '00000000-0000-0000-0000-000000000000'::uuid),
CHECK (attempt_id <> '00000000-0000-0000-0000-000000000000'::uuid)
);
-- Exact per-operation authority-version attribution for restore. Empty
@@ -3299,12 +3292,15 @@ CREATE TRIGGER authorization_authentication_denial_attempts_no_truncate
-- Bidirectional deferred guard: a kind-9 (pre-authentication denial) audit
-- event must commit with exactly one denial attempt; a denial attempt must
-- commit with its audit event present and kind-9. Both directions deferred so
-- commit with its audit event present, kind-9, and matching semantic
-- coordinates (correlation_id and reason_code). Both directions deferred so
-- event and attempt may be inserted in any order inside one transaction.
CREATE FUNCTION authorization_denial_attempt_guard_v1()
RETURNS TRIGGER AS $$
DECLARE
found_event_kind SMALLINT;
found_correlation_id UUID;
found_reason_code SMALLINT;
attempt_count BIGINT;
BEGIN
IF TG_TABLE_NAME = 'authorization_events' THEN
@@ -3325,10 +3321,34 @@ BEGIN
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_event_cardinality';
END IF;
-- Verify semantic coordinates match between event and denial attempt.
SELECT correlation_id, reason_code
INTO found_correlation_id, found_reason_code
FROM authorization_authentication_denial_attempts
WHERE community_id = NEW.community_id
AND audit_event_id = NEW.event_id;
IF found_correlation_id IS DISTINCT FROM NEW.correlation_id THEN
RAISE EXCEPTION
'denial attempt correlation_id % does not match event correlation_id % for event %',
found_correlation_id, NEW.correlation_id, NEW.event_id
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
IF found_reason_code IS DISTINCT FROM NEW.reason_code THEN
RAISE EXCEPTION
'denial attempt reason_code % does not match event reason_code % for event %',
found_reason_code, NEW.reason_code, NEW.event_id
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
ELSE
-- Firing from the denial-attempt side: verify the audit event is kind-9
-- and that exactly one denial attempt references it.
SELECT event_kind INTO found_event_kind
SELECT event_kind, correlation_id, reason_code
INTO found_event_kind, found_correlation_id, found_reason_code
FROM authorization_events
WHERE community_id = NEW.community_id
AND event_id = NEW.audit_event_id;
@@ -3349,6 +3369,23 @@ BEGIN
CONSTRAINT = 'authorization_denial_attempt_event_kind';
END IF;
-- Verify semantic coordinates match.
IF found_correlation_id IS DISTINCT FROM NEW.correlation_id THEN
RAISE EXCEPTION
'denial attempt correlation_id % does not match event correlation_id % for event %',
NEW.correlation_id, found_correlation_id, NEW.audit_event_id
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
IF found_reason_code IS DISTINCT FROM NEW.reason_code THEN
RAISE EXCEPTION
'denial attempt reason_code % does not match event reason_code % for event %',
NEW.reason_code, found_reason_code, NEW.audit_event_id
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
SELECT count(*) INTO attempt_count
FROM authorization_authentication_denial_attempts
WHERE community_id = NEW.community_id