Skip to content

fix(auth): scope bearer callers to their token's tenant (RIG-4066) - #1375

Merged
trunk-io[bot] merged 6 commits into
mainfrom
compass-managed/rig-4066-bearer-tenant
Oct 3, 2026
Merged

trunk-io[bot] merged 6 commits into
mainfrom
compass-managed/rig-4066-bearer-tenant

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes RIG-4066. Option 1 of RIG-4067 (ruled 09-28, DL-382).

Problem

BearerInterceptor / BearerStreamInterceptor resolved token → account and attached the caller, but never set store.WithTenant. resolveTenant then fell back to the bootstrap tenant, so every network-door RPC ran under the bootstrap tenant's RLS scope. A caller in any other tenant could not even see its own account.

Change

accounts is FORCE-RLS, so the caller's tenant can't be read from the account row before the GUC exists. tokens stays outside RLS, so it now records the issuing tenant:

  • tokens.tenant_id (NOT NULL, FK tenants) as migration 0004_token_tenant.sql, backfilled to the bootstrap tenant by slug. 0001_init.sql is frozen (RIG-4077), and main already holds 0002 and 0003.
  • Store.PutTokenHash stamps resolveTenant(ctx) (the issuing request's tenant; bootstrap at boot).
  • ResolveTokenHash returns it on Subject.Tenant.
  • Both bearer interceptors wrap the ctx with store.WithTenant. An empty tenant fails closed with the same CodeUnauthenticated.

No request-path BYPASSRLS read (OQ-4/N5 unchanged).

Tests

  • TestBearerTokenScopesCallerToIssuingTenant (server pgtest): a tenant-B token through the real network door → ListAccounts returns the tenant-B caller and not the bootstrap admin. Red on main and red with the unary WithTenant removed (ListAccounts did not return issuing-tenant account).
  • TestResolveTokenHashReturnsIssuingTenant (store pgtest).
  • TestTokenTenantMigrationBackfillsV1Tokens (store pgtest): a v1 database with a live token upgrades through every later migration and the token resolves to the bootstrap tenant; decoy tenants pin the slug-keyed backfill.
  • Stream unit test asserts the injected tenant; round-trip tests expect the stamped tenant.
  • go test -tags pgtest for internal/store, internal/auth, server/..., internal/runnerhub: all ok. go vet -tags pgtest, gofmt, sqlc diff, golangci-lint on the changed files: clean.

Deploy note

store.migrate applies 0004 on the next boot, so live databases gain the column with no recreate.

@trunk-io

trunk-io Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@linear-code

linear-code Bot commented Sep 28, 2026

Copy link
Copy Markdown

RIG-4066

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-managed-rig-4066-bea.compass-eng-docs.pages.dev

Deployed from compass-managed/rig-4066-bearer-tenant at 66a33cf.

Changed pages:

rigel-mintaka and others added 6 commits October 3, 2026 14:23
The bearer interceptors resolved token -> account but never set the
request tenant, so every network-door RPC ran under the bootstrap
tenant's RLS scope. A caller in any other tenant could not see its own
account.

`accounts` is FORCE-RLS, so the tenant cannot be read from the account
row before the GUC is set. `tokens` stays outside RLS, so it now records
the issuing tenant: PutTokenHash stamps the issuing request's tenant,
ResolveTokenHash returns it on Subject.Tenant, and both bearer
interceptors wrap the ctx with store.WithTenant. A token without a tenant
fails closed with the same CodeUnauthenticated as any other rejection.

TestBearerTokenScopesCallerToIssuingTenant drives a tenant-B token
through the real network door. It fails on main (ListAccounts returns
only the bootstrap admin) and passes here.

Spec-impact: pending RIG-4067 (option 1, token carries tenant). Refs RIG-4066

Co-authored-by: Matt Wilkinson <matt@rigel.build>
RIG-4077 froze 0001_init.sql, and live databases never re-run it. The
column now arrives in 0002: it is added nullable, existing tokens are
backfilled to the bootstrap tenant (the only tenant any request path
could issue under), and then it is set NOT NULL. A new pgtest upgrades
a v1 database that holds a live token, and checks that the token still
resolves with its tenant.

Spec-impact: pending RIG-4067. Refs RIG-4066, RIG-4077

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…-4066)

The upgrade test seeded only the bootstrap tenant, so a backfill that
ignored the slug still passed. It now seeds decoy tenants that sort
before and after the bootstrap tenant by id and by age. Four slug-less
selectors (by id or age, ascending or descending) now fail the test.

Spec-impact: none. Refs RIG-4066

Co-authored-by: Matt Wilkinson <matt@rigel.build>
Matt ruled RIG-4067 option 1. Annotate the multitenancy record's
tenant-identity bullet with the post-freeze amendment. Append DL-382: the
token row carries its issuing tenant, and the interceptor scopes the
request from it without BYPASSRLS.

Spec-impact: docs/designs/infra/runtime/compass-managed-multitenancy/design.md, docs/designs/DECISIONS.md. Refs RIG-4066, RIG-4067

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ed id (RIG-4066)

DL-382 was never claimed from the ledger allocator; the allocator issued
DL-381 to RIG-4066 for this row.

Spec-impact: renumbers one DECISIONS.md row and its citation in the
multitenancy record; the ruling is unchanged. Refs RIG-4066

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ok 0002 (RIG-4066)

main landed `0002_backfill_empty_tenant.sql` and `0003_token_usage.sql`. `loadMigrations` rejects a duplicate version, so the token tenant migration moves to the next free number. The SQL is unchanged.

Spec-impact: none. Refs RIG-4066

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-managed/rig-4066-bearer-tenant branch from a129708 to 66a33cf Compare October 3, 2026 18:28
@trunk-io
trunk-io Bot merged commit dc0a9f4 into main Oct 3, 2026
16 checks passed
@trunk-io
trunk-io Bot deleted the compass-managed/rig-4066-bearer-tenant branch October 3, 2026 22:19
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