control plane: tenant filtering for GraphQL listings#3028
Open
GregorShear wants to merge 4 commits into
Open
Conversation
…int, and stateful bearer authentication Adds GraphQL operations for managing refresh tokens (refreshTokens query, createRefreshToken / revokeRefreshToken mutations) and a POST /api/v1/auth/token REST endpoint that exchanges a refresh token for a signed access token. The endpoint transitionally delegates to the SQL generate_access_token function, which existing PostgREST clients still depend on; the plan is to migrate those callers here and then retire the SQL function. Also reworks how the Envelope extractor handles refresh tokens presented as bearer credentials. Previously it called generate_access_token to mint a signed JWT and then immediately verified that JWT — a sign/verify round trip that crosses no trust boundary, since the token never leaves the process. The envelope now validates the credential directly against the refresh_tokens table and constructs claims from the verified row, via a new Verified::assert_stateful_authentication seam in the tokens crate that keeps 'holding a Verified<T>' as a type-level proof of authentication. One behavior change: single-use refresh tokens are no longer accepted as bearer credentials. The previous path consumed them on first use and discarded the rotated secret, silently bricking the token; they now fail authentication outright. Single-use tokens remain exchangeable via /api/v1/auth/token, which returns the rotation to the caller. Because the bearer path no longer depends on pgjwt signing (absent from the sqlx::test polyfill), its happy path is now covered by an integration test: create a token via GraphQL, authenticate with it as a bearer, and observe identity and usage stamping.
… cache to 0.8.6 output The unpinned 'cargo install sqlx-cli' in build:sqlx-prepare silently upgrades to the latest release. sqlx-cli 0.9 requires DATABASE_URL in its own process environment — where cargo's [env] from .cargo/config.toml doesn't reach — breaking the bare 'cargo sqlx prepare' workflow, and its nullability inference differs from 0.8.6 in two cached queries, which would fail CI's 0.8.6 cache check. Pin to 0.8.6 to match ci/sqlx-check and platform-test.yaml, and regenerate the two drifted cache entries with 0.8.6.
Restacked onto greg/refresh-token-exchange, which carries the refresh-token GraphQL operations, the /api/v1/auth/token endpoint, and stateful bearer authentication that this branch previously bundled. This commit squashes the prior branch history (PR feedback, tenant filtering, revocation semantics, SHA-256 key hashing) into the remaining service-account delta: - internal.service_accounts and internal.api_keys tables. A service account is a non-login auth.users identity whose access is determined solely by its user_grants; its prefix is a management anchor determining who may manage it. - GraphQL operations: serviceAccounts query (tenant-filterable, paginated, with batch-loaded API keys), createServiceAccount, revokeServiceAccountGrants (the kill switch), createApiKey, revokeApiKey. - api_key grant on POST /api/v1/auth/token: verifies flow_sa_ keys against SHA-256 hashes and mints access tokens in the application layer. - ManageServiceAccounts orthogonal capability, bundled into TeamAdmin; authorized_prefixes gains a tenant filter that narrows (never widens) the caller's authorized set. - Service-account principals are denied refresh tokens, and SA synthetic emails are excluded from Stripe billing-contact selection.
b2d35a5 to
509dcde
Compare
Adds a reusable tenant filter for GraphQL queries that list resources scoped
to the caller's authorized prefixes, and wires it into the serviceAccounts
query as filter: { tenant: { eq: "acmeCo/" } }.
authorized_prefixes gains a tenant parameter backed by a new tenant_prefixes
helper, which computes the tenant's own reach through the role-grant graph
(its prefix plus every prefix reachable at the requested capability,
following delegating admin grants) and intersects it with the caller's
authorized prefixes, keeping the narrower prefix of each overlapping pair.
Every returned prefix lies within both sets, so the filter can never widen
the caller's access, and listings never extend past the tenant's scope.
The existing prefix-overlap filter (catalogPrefix / catalogPrefixOrName on
invite links and alert configs) is marked deprecated in favor of the tenant
filter; TODOs note the migration path for those callers.
5c0786b to
b797d42
Compare
f96bc46 to
4c1627b
Compare
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.
Stacked on #2943 (
greg/service-accounts-phase1).Summary
Adds a reusable tenant filter for GraphQL listings and wires it into the
serviceAccountsquery:serviceAccounts(filter: { tenant: { eq: "acmeCo/" } }).Design
authorized_prefixesgains atenantparameter backed by a newtenant_prefixeshelper.tenant_prefixescomputes the tenant's own reach through the role-grant graph: the tenant prefix itself, plus every prefix reachable from it at the requested capability, using the same BFS as authorization (RoleGrant::reachable_nodes) — admin grants delegate and extend the chain, while plain read/write grants are leaves.The caller's authorized prefixes are then intersected with the tenant's reach, keeping the narrower prefix of each overlapping pair. Every returned prefix lies within both sets, so the filter can never widen the caller's access, and the listing never extends past the tenant's scope — including the case where a caller's broad grant (e.g. all of
betaCo/) is clamped to the tenant's narrower reach (betaCo/nested/).The existing prefix-overlap filter (
catalogPrefix/catalogPrefixOrNameon invite links and alert configs) is marked deprecated in favor of the tenant filter, with TODOs noting the migration path for those callers.Testing
Unit tests cover
tenant_prefixes(transitive admin chains, leaf grants, fine-grained capability filtering, cross-tenant exclusion, bare tenants) and the tenant clamping inauthorized_prefixes(narrows to tenant scope, cannot widen access, keeps shared reach, clamps broad grants). TheserviceAccountslifecycle test exercises the filter end-to-end.