feat(auth): federated_write — per-write source authorization (write-side mirror of federated_read)#2248
Open
perennia-regen wants to merge 2 commits into
Open
Conversation
…ide mirror of federated_read)
Add `federated_write TEXT[]` to oauth_clients: the set of sources a token may
write to beyond its single source_id. Write ops (put_page, put_raw_data) accept
an optional per-call write target, authorized FAIL-CLOSED against
`{ctx.sourceId} ∪ federatedWrite` via the new `resolveWriteSource` helper. One
"directorio" curator token can write to campo / lideres / directorio without
re-minting tokens.
Mirrors federated_read end-to-end (3 schema sources, idempotent migration,
--federated-write CLI flag, oauth-provider persist + SELECT/normalize with
back-compat fallbacks, AuthInfo field). The only non-mirror part is the
per-write authorization step.
- schema: federated_write column + GIN index in schema.sql / pglite-schema.ts /
schema-embedded.ts (regenerated); pre-v118 bootstrap adds the column so the
index in SCHEMA_SQL doesn't crash old brains.
- migrate.ts: v118 (column) + v119 (GIN index). No backfill — default '{}'
means writes locked to source_id (the pre-v118 behavior).
- auth.ts: --federated-write SRC1,SRC2,... flag + summary line.
- oauth-provider.ts: persist on register + SELECT/normalize federated_write,
with a pre-v118 fallback projection mirroring the federated_read shape.
- operations.ts: AuthInfo.federatedWrite, resolveWriteSource helper, optional
`source` (put_page) / `write_source` (put_raw_data, to avoid colliding with
the existing data-provenance `source`) input fields threaded into the write.
- tests: resolveWriteSource pure-fn + put_page/put_raw_data dry-run auth
(no/in-set/out-of-set), CLI parser flag, schema bootstrap coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sing needsOauthClientsBootstrap only checked source_id/federated_read, so existing (v117) brains skipped the bootstrap and SCHEMA_SQL's federated_write GIN index crashed (column not yet added). Probe + condition now also check federated_write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
federated_write TEXT[]tooauth_clients: the set of sources a token may write to, beyond its singlesource_id. Write ops choose a target source per call, authorized against that set — so one token (e.g. a "directorio" curator) can write to campo / lideres / directorio without re-minting tokens.Mirror of federated_read
This is the WRITE-side mirror of
federated_read, implemented at every site federated_read touches:schema.sql,pglite-schema.ts,schema-embedded.ts) + pre-bootstrap on both engines--federated-write SRC1,SRC2,…CLI flag onregister-clientAuthInfo.federatedWrite(mirror ofallowedSources)The one non-mirror part: per-write authorization
New
resolveWriteSource(ctx, requested?)helper, fail-closed like the read-side scope resolution:requested→ctx.sourceId(UNCHANGED — pre-v118 clients never sent one)requested∈{sourceId} ∪ federatedWrite→ allowedpermission_denied(source 'X' not in this client's federated_write set)Empty/undefined
federated_writecollapses the allowed set to justsource_id, so omitting the grant exactly preserves the single-source write lock. Server-stamped provenance (source_kind/source_uri/ingested_via) is untouched — the newsourceis a separate, authorized write-target field.put_pagegets an optionalsource;put_raw_datagetswrite_source(its existingsourceparam is the data-provenance label and was left intact).Tests
New
test/put-page-federated-write.test.tscovers: nosource→source_id;sourcein set → allowed;sourcenot in set → rejected; attacker-supplied empty[]does not widen scope;put_raw_datawrite_sourcevs data-sourcedistinction.tsc --noEmitclean; existing oauth/source-isolation guard checks pass.Compatibility
Default
'{}'(empty) for every existing client → writes stay locked tosource_id. No backfill. Pre-v118 brains fall back to the federated_read projection untilapply-migrationsruns.Usage