Skip to content

feat(auth): federated_write — per-write source authorization (write-side mirror of federated_read)#2248

Open
perennia-regen wants to merge 2 commits into
garrytan:masterfrom
perennia-regen:feat/federated-write
Open

feat(auth): federated_write — per-write source authorization (write-side mirror of federated_read)#2248
perennia-regen wants to merge 2 commits into
garrytan:masterfrom
perennia-regen:feat/federated-write

Conversation

@perennia-regen

Copy link
Copy Markdown

What

Adds federated_write TEXT[] to oauth_clients: the set of sources a token may write to, beyond its single source_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:

  • column + GIN index in all 3 schema sources (schema.sql, pglite-schema.ts, schema-embedded.ts) + pre-bootstrap on both engines
  • idempotent migrations (v118 column, v119 GIN index), no backfill
  • --federated-write SRC1,SRC2,… CLI flag on register-client
  • oauth-provider persist + SELECT/normalize, with the same pre-version back-compat fallback shape
  • AuthInfo.federatedWrite (mirror of allowedSources)

The one non-mirror part: per-write authorization

New resolveWriteSource(ctx, requested?) helper, fail-closed like the read-side scope resolution:

  • no requestedctx.sourceId (UNCHANGED — pre-v118 clients never sent one)
  • requested{sourceId} ∪ federatedWrite → allowed
  • otherwise → permission_denied (source 'X' not in this client's federated_write set)

Empty/undefined federated_write collapses the allowed set to just source_id, so omitting the grant exactly preserves the single-source write lock. Server-stamped provenance (source_kind/source_uri/ingested_via) is untouched — the new source is a separate, authorized write-target field.

put_page gets an optional source; put_raw_data gets write_source (its existing source param is the data-provenance label and was left intact).

Tests

New test/put-page-federated-write.test.ts covers: no sourcesource_id; source in set → allowed; source not in set → rejected; attacker-supplied empty [] does not widen scope; put_raw_data write_source vs data-source distinction. tsc --noEmit clean; existing oauth/source-isolation guard checks pass.

Compatibility

Default '{}' (empty) for every existing client → writes stay locked to source_id. No backfill. Pre-v118 brains fall back to the federated_read projection until apply-migrations runs.

Usage

gbrain auth register-client director --scopes read,write --source directorio \
  --federated-read campo,lideres,directorio --federated-write campo,lideres,directorio

perennia-regen and others added 2 commits June 17, 2026 10:31
…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>
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.

1 participant