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

Three IMPORTANT findings addressed:

IMPORTANT 1 (denial semantic binding, partial): Bind the remaining two
unbound denial identity coordinates.

- Add semantic_fingerprint BYTEA to authorization_events: required non-zero
  for kind-9 events, NULL for all other event kinds (enforced by CHECK
  on the table). This is the redaction-safe intent_digest coordinate.
- Add authorization_denial_reason_reason_code_binding CHECK to
  authorization_authentication_denial_attempts: encodes the canonical
  OperatorAuthenticationDenialReason <-> AuthorizationReasonCode mapping
  from operator_lifecycle.rs:700-706 and authorization_events.rs:215-226:
  MissingCredential(1)<->Missing(2), InvalidCredential(2)<->Invalid(3),
  Unauthenticated(3)<->Unauthenticated(4). Fires at INSERT, not COMMIT.
- Extend authorization_denial_attempt_guard_v1() to compare
  semantic_fingerprint between event and denial attempt in both firing
  directions, raising 23514 'authorization_denial_attempt_semantic_binding'
  on mismatch. Carl's mismatched-reason and mismatched-fingerprint
  cross-attachments are now fully closed.

IMPORTANT 2 (concurrency regression): Replace the 10ms-sleep approach
with a tokio::sync::Barrier(2) that holds both connections after BEGIN
and before INSERT. Both race to pg_advisory_xact_lock; one blocks, the
winner commits, the loser sees MAX=102 and fails with 23514 (not 23505).
XOR assertion proves exactly one INSERT succeeds, and the loser's 23514
(not PK 23505) proves the advisory lock — not just PK uniqueness — is the
serialization mechanism. Contradictory comments fixed.

MINORs (folded in):
- Fix stale test doc comment claiming effective_at must advance (it does
  not; the downstream constructor stamps Unix epoch for immediate policy).
- Fix equal-revision comment incorrectly claiming different policy_digest
  avoids the PK; the PK is (community_id, policy_revision).

CI fmt failure: cargo fmt --all run; whitespace-only reformatting of
some query blocks in migration.rs.

Regressions added/updated:
- B4: denial_reason/reason_code mapping violation rejected at INSERT by
  the immediate CHECK (23514 from authorization_denial_reason_reason_code_binding).
- B5: semantic_fingerprint mismatch between event and denial attempt
  rejected at COMMIT by the deferred guard (23514 from
  authorization_denial_attempt_semantic_binding).

Byte-parity (extraction+cmp):
- authorization_events table: 3394 bytes, migration == schema.sql
- authorization_authentication_denial_attempts table: 2062 bytes, migration == schema.sql
- authorization_denial_attempt_guard_v1(): 5579 bytes, migration == schema.sql
- identity_enrollment_policy_revision_guard_v1(): 1113 bytes, migration == schema.sql
- authorization_admission_result_guard_v1(): 2164 bytes, migration == schema.sql

All five NIP-FI tests green locally (run in isolation to avoid pre-existing
pool-state flakiness in the full suite).

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 15:39:20 -04:00
parent 5ba7d2614c
commit fd2a79bdba
3 changed files with 361 additions and 67 deletions

View File

@@ -2977,6 +2977,12 @@ CREATE TABLE authorization_events (
),
correlation_id UUID NOT NULL,
attempt_id UUID NOT NULL,
-- Redaction-safe pre-authentication denial identity. Present and non-zero
-- for kind-9 events; NULL for all other event kinds. Binds the event to the
-- exact denial attempt's semantic_fingerprint (intent_digest) for exact replay.
semantic_fingerprint BYTEA CHECK (
semantic_fingerprint IS NULL OR octet_length(semantic_fingerprint) = 32
),
occurred_at TIMESTAMPTZ NOT NULL,
accepted_at TIMESTAMPTZ NOT NULL DEFAULT transaction_timestamp(),
canonical_envelope BYTEA NOT NULL CONSTRAINT authorization_events_envelope_size CHECK (
@@ -3002,6 +3008,13 @@ CREATE TABLE authorization_events (
CHECK (
(actor_kind = 4 AND actor_fingerprint IS NULL AND subject_fingerprint IS NULL)
OR (actor_kind IN (1, 2, 3) AND actor_fingerprint IS NOT NULL)
),
-- Kind-9 (pre-auth denial) events must carry a non-zero semantic_fingerprint;
-- all other event kinds must not.
CHECK (
(event_kind = 9 AND semantic_fingerprint IS NOT NULL
AND semantic_fingerprint <> decode(repeat('00', 32), 'hex'))
OR (event_kind <> 9 AND semantic_fingerprint IS NULL)
)
);
@@ -3037,6 +3050,13 @@ CREATE TABLE authorization_authentication_denial_attempts (
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,
-- Canonical denial_reason ↔ reason_code binding: MissingCredential(1)↔Missing(2),
-- InvalidCredential(2)↔Invalid(3), Unauthenticated(3)↔Unauthenticated(4).
CONSTRAINT authorization_denial_reason_reason_code_binding CHECK (
(denial_reason = 1 AND reason_code = 2)
OR (denial_reason = 2 AND reason_code = 3)
OR (denial_reason = 3 AND reason_code = 4)
),
CHECK (operation_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)
@@ -3293,14 +3313,18 @@ 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, 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.
-- coordinates (correlation_id, reason_code, and semantic_fingerprint). Both
-- directions deferred so event and attempt may be inserted in any order inside
-- one transaction. The static denial_reason↔reason_code mapping is enforced
-- by an immediate CHECK on the denial attempt table; the guard enforces the
-- matching semantic coordinates between event and attempt.
CREATE FUNCTION authorization_denial_attempt_guard_v1()
RETURNS TRIGGER AS $$
DECLARE
found_event_kind SMALLINT;
found_correlation_id UUID;
found_reason_code SMALLINT;
found_semantic_fingerprint BYTEA;
attempt_count BIGINT;
BEGIN
IF TG_TABLE_NAME = 'authorization_events' THEN
@@ -3323,8 +3347,8 @@ BEGIN
END IF;
-- Verify semantic coordinates match between event and denial attempt.
SELECT correlation_id, reason_code
INTO found_correlation_id, found_reason_code
SELECT correlation_id, reason_code, semantic_fingerprint
INTO found_correlation_id, found_reason_code, found_semantic_fingerprint
FROM authorization_authentication_denial_attempts
WHERE community_id = NEW.community_id
AND audit_event_id = NEW.event_id;
@@ -3344,11 +3368,20 @@ BEGIN
USING ERRCODE = 'check_violation',
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
IF found_semantic_fingerprint IS DISTINCT FROM NEW.semantic_fingerprint THEN
RAISE EXCEPTION
'denial attempt semantic_fingerprint does not match event semantic_fingerprint for event %',
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, correlation_id, reason_code
INTO found_event_kind, found_correlation_id, found_reason_code
SELECT event_kind, correlation_id, reason_code, semantic_fingerprint
INTO found_event_kind, found_correlation_id, found_reason_code,
found_semantic_fingerprint
FROM authorization_events
WHERE community_id = NEW.community_id
AND event_id = NEW.audit_event_id;
@@ -3386,6 +3419,14 @@ BEGIN
CONSTRAINT = 'authorization_denial_attempt_semantic_binding';
END IF;
IF found_semantic_fingerprint IS DISTINCT FROM NEW.semantic_fingerprint THEN
RAISE EXCEPTION
'denial attempt semantic_fingerprint does not match event semantic_fingerprint for event %',
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