Skip to content

feat: store principal invalidation records - #10

Closed
pyropy wants to merge 1 commit into
srdjan/feat/iam-store-eventsfrom
srdjan/feat/iam-principal-records
Closed

pyropy wants to merge 1 commit into
srdjan/feat/iam-store-eventsfrom
srdjan/feat/iam-principal-records

Conversation

@pyropy

@pyropy pyropy commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

RevocationStore gains AddPrincipalRevocation, recording that every proof a gateway cached for a principal's keys is void. Postgres reads both tables in one UNION ALL ordered by recorded_at, keeping a single horizon, cursor and dedup map. GET /revocation/:cid is unchanged. Part of the Forge S3 tenant IAM work (RFC).

Change log

  • AddPrincipalRevocation on the interface and both backends
  • Migration 00002: principal_invalidation table and index
  • One query over both tables in the stream poll
  • Tests: interleaved ordering, late arrivals inside the settle window, invalid input

🤖 Generated with Claude Code

@pyropy
pyropy added this pull request to stack #12 September 10, 2026 16:19
@pyropy
pyropy force-pushed the srdjan/feat/iam-principal-records branch from 9507aca to bf74330 Compare September 11, 2026 12:40
@pyropy
pyropy requested a lite review from Copilot September 11, 2026 12:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical PostgreSQL streaming and firehose compatibility issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds principal invalidation persistence and unified streaming across memory and PostgreSQL backends.

Changes:

  • Extends the store API with AddPrincipalRevocation.
  • Adds PostgreSQL schema, storage, and unified polling.
  • Adds backend and validation tests.
File summaries
File Summary
pkg/store/store.go Extends the store interface.
pkg/store/postgres/store.go Persists and streams principal invalidations.
pkg/store/postgres/store_test.go Tests PostgreSQL behavior.
pkg/store/postgres/migrations/sql/00002_principal_invalidation.sql Adds the invalidation table and index.
pkg/store/memory/store.go Adds in-memory support.
pkg/store/memory/store_test.go Tests in-memory behavior.
pkg/fx/app_test.go Updates the test store implementation.
Review details

Suppressed comments (1)

pkg/store/postgres/store.go:208

  • The UNION now permits the same invocation CID to exist once in each table: the primary keys are table-local and both Add methods accept the same invocation. However, the shared seen map below is keyed only by rec.Cause().Link(), so recording both effects with one invocation silently drops the second event even though Stream promises all records. Key deduplication by record kind plus cause, and prune the same composite key.
			`SELECT kind, cause, revoked_delegation, path_witness, tenant, principal, recorded_at
  • Files reviewed: 7/7 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/store/memory/store.go
s.mu.Lock()
defer s.mu.Unlock()
record.RecordedAt = time.Now() // under the lock, as in Add
s.appendLocked(store.PrincipalRevocationEvent(record))
FROM principal_invalidation
WHERE recorded_at >= $1
) AS record
ORDER BY recorded_at, id`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing back: the query is valid PostgreSQL. ORDER BY may reference any column of the FROM item, and record is a derived table that projects id; the projected-columns rule applies to SELECT DISTINCT and to a top-level UNION, and the UNION ALL here is inside the derived table. The Postgres store suite runs this exact query on every stream test, and the integration test streams both record kinds through it.

yield(store.Event{}, fmt.Errorf("decoding principal invalidation: %w", err))
return
}
event = store.PrincipalRevocationEvent(record)
Comment thread pkg/store/store.go
Comment on lines +105 to +108
// AddPrincipalRevocation adds a principal invalidation record to the store.
// The invalidation is the invocation that voids every proof cached for the
// principal's keys; tenant must be defined and principal must not be empty.
AddPrincipalRevocation(ctx context.Context, invalidation ucan.Invocation, tenant did.DID, principal string) error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing back on this one and on the two sibling comments (memory/store.go:98, postgres/store.go:262). This PR is one layer of a stack: it adds the record and the stream event, the next PR (#11) adds the wire representation and the writer for it, and the route that creates a principal revocation record arrives in #13. Within this PR nothing can produce such a record, so the writer's refusal of a kind it has no format for is unreachable here and is the correct intermediate state for a reviewer reading the PR on its own.

On the suppressed note about one invocation CID appearing in both tables: an invocation carries one command, and /ucan/revoke and /principal/invalidate are different commands stored by different routes, so a single invocation cannot land in both tables. Deduplicating by cause alone is exact.

RevocationStore gains AddPrincipalRevocation, which records that every
proof cached for a principal's keys is void. Both backends reject an
undefined tenant or an empty principal and otherwise mirror Add.

Postgres stores the records in a new principal_invalidation table
(migration 00002) and reads both tables in one UNION ALL ordered by
recorded_at and id, so the stream keeps one now() horizon, one cursor
and one dedup map across both kinds. The memory backend appends to the
same log Stream already reads, which dedups a retried invalidation by
cause link as it does for revocations. Get is unaffected: it reads only
the revocation table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy
pyropy force-pushed the srdjan/feat/iam-principal-records branch from bf74330 to 04d3973 Compare September 14, 2026 10:11
@pyropy

pyropy commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the per-key marker design in fil-one/RFC#30 (f61f474): no principal invalidation record; markers are revoked through the existing /ucan/revoke, and the client's nonce option is #15. Branch kept.

@pyropy pyropy closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants