Skip to content

service accounts: Phase 1 — backend CRUD and API key token exchange#2943

Draft
GregorShear wants to merge 1 commit into
masterfrom
greg/service-accounts-phase1
Draft

service accounts: Phase 1 — backend CRUD and API key token exchange#2943
GregorShear wants to merge 1 commit into
masterfrom
greg/service-accounts-phase1

Conversation

@GregorShear
Copy link
Copy Markdown
Contributor

@GregorShear GregorShear commented May 13, 2026

Summary

  • Adds internal.service_accounts and internal.api_keys tables for non-human identities that authenticate via API keys and are authorized through the existing user_grants / role_grants system
  • Implements GraphQL mutations (createServiceAccount, disableServiceAccount, enableServiceAccount, createApiKey, revokeApiKey) and a paginated serviceAccounts query, all gated on admin capability of the service account's prefix
  • Extends generate_access_token to accept a flow_sa_-prefixed API key alongside the existing refresh token path — the two input shapes are mutually exclusive, and existing callers are unaffected

Phase 1 of the service accounts plan. Unlocks Phases 2–7 (management UI, OIDC, flowctl integration).

Key design decisions

  • Service accounts are auth.users rows. All existing RLS policies, PostgREST authorization, user_roles() resolution, and role_grants traversal work unchanged. This avoids putting the PostgREST→GraphQL migration on the critical path.
  • One user_grants row per service account. Disabling deletes the grant and all API keys; re-enabling restores the grant from the service account's stored prefix/capability.
  • API keys use fixed expiry (not sliding window like refresh tokens). The flow_sa_ prefix routes server-side lookups to internal.api_keys and prevents collisions with base64-encoded refresh tokens.
  • generate_access_token drops the old 2-arg signature and replaces it with a 3-arg version (all with defaults). Existing positional and named callers continue to work unchanged.

Test plan

  • CI build passes (native deps not available locally — jemalloc/rocksdb are in the build VM)
  • sqlx prepare — new queries need to be added to the offline cache in CI
  • Integration test test_service_account_lifecycle covers:
    • Create service account → create API key → exchange for access token
    • Authorization checks (Bob can't manage Alice's service accounts)
    • Revoke API key → exchange fails
    • Disable service account → API keys revoked, grants removed, new key creation blocked
    • Re-enable service account → new keys can be created, exchange works
    • Idempotency guards (disable-while-disabled and enable-while-enabled both error)
    • List query scoped to admin prefixes

@GregorShear GregorShear marked this pull request as draft May 13, 2026 22:49
@GregorShear GregorShear force-pushed the greg/service-accounts-phase1 branch from 8a5b938 to 044861d Compare May 19, 2026 22:17
  Adds service account lifecycle management via GraphQL and API key authentication
  via a new REST token exchange endpoint.

  Data model:
  - internal.service_accounts: identities keyed by auth.users.id, with owning
    prefix, capability, and disabled state
  - internal.api_keys: long-lived credentials (bcrypt-hashed) exchanged for
    short-lived JWTs, with fixed expiry (no sliding window)

  GraphQL mutations (all require admin on the service account's prefix):
  - createServiceAccount: creates auth.users + service_accounts + user_grants rows
  - disableServiceAccount: revokes all API keys and grants, sets disabled_at
  - enableServiceAccount: clears disabled_at, restores the user_grants row
  - createApiKey: mints a flow_sa_-prefixed secret (returned once, stored as hash)
  - revokeApiKey: deletes the API key row

  GraphQL query:
  - serviceAccounts: paginated list scoped to caller's admin prefixes, includes
    nested apiKeys (without secrets)

  Token exchange:
  - POST /api/v1/auth/token accepts {"grant_type": "api_key", "api_key": "..."}
    or {"grant_type": "refresh_token", "refresh_token_id": "...", "secret": "..."}.
    The API key path validates credentials and signs JWTs in the Rust application
    layer. The refresh token path delegates to the existing generate_access_token
    SQL function. The existing function is unchanged.

  Phase 1 of the service accounts plan (#2857).
@GregorShear GregorShear force-pushed the greg/service-accounts-phase1 branch from 044861d to 899d324 Compare May 19, 2026 22:22
Comment thread .gitignore

.claude/*
!.claude/skills/
mise.local.toml
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.

not related to this PR but helping me not commit a file related to a parallel workstream...

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