Skip to content

feat(api-keys): support unscoped API keys (optional project_id) - #584

Merged
arantespp merged 1 commit into
mainfrom
claude/admin-api-key-full-access-9i0ppl
Jul 18, 2026
Merged

feat(api-keys): support unscoped API keys (optional project_id)#584
arantespp merged 1 commit into
mainfrom
claude/admin-api-key-full-access-9i0ppl

Conversation

@arantespp

Copy link
Copy Markdown
Member

Summary

API keys were hard-required to be scoped to a single project. This PR makes project_id optional: a key created without one is unscoped and spans every project its owner can reach, bounded by the intersection of the owner's permissions and the key's own attached policies. Multi-project access is now expressed through IAM policies rather than a per-key project — as requested.

The intersection ceiling is unchanged: an unscoped key can never exceed its owner's permissions. When project_id is null and the key has no attached policies, effective permissions are just the owner's; with policies, it's the intersection of owner policies ∩ key policies — across all reachable projects.

Behavior

Configuration Effective permissions
project_id set Owner permissions, hard-locked to that project (unchanged)
unscoped (omitted/null) Owner permissions, across every reachable project
unscoped + policy_ids Intersection of owner policies and key policies, across reachable projects

Because an unscoped key has no implicit project, requests that operate on a specific project must supply project_id explicitly (endpoints already return 400 in that case).

Changes

  • DB: ApiKey.projectId is now nullable (was NOT NULL).
  • REST: POST /api-keys no longer requires project_id (omit or null → unscoped). PUT /api-keys/:id accepts project_id: null to clear scope, a value to (re)scope, omitted to leave unchanged.
  • Auth: resolveProjectKey tolerates a null project (no more non-null assertion) and builds an unscoped resolveProjectIds that enumerates reachable projects through the key's own authorizer (which already intersects owner + key policies). Admin owner with no key boundary keeps the unrestricted fast path.
  • Refactor: extracted the project-ID resolver factories from auth.ts into authProjectResolvers.ts (keeps auth.ts under the max-lines limit; no behavior change).
  • OpenAPI / SDK / CLI: project_id marked optional + nullable; SDK and CLI manifests regenerated (generated files are gitignored and rebuilt in CI).
  • Docs: modules/api-keys.md updated (overview, data model, permission inheritance, project scoping).
  • Tests: apiKeys.test.ts covers unscoped create (omit/null), null-clearing and re-scoping via PUT, unscoped keys spanning projects, intersection narrowing via a key policy, and the owner-ceiling bound. Smoke test adds an unscoped-key flow.

Testing

  • pnpm typecheck (src + tests)
  • pnpm eslint (no warnings), pnpm docs-lint
  • ✅ SDK + CLI regeneration
  • ⏳ Unit/MCP/smoke suites run in CI — they require the Postgres testcontainer (Docker), which isn't available in this session.

🤖 Generated with Claude Code


Generated by Claude Code

API keys were hard-required to be scoped to a single project. This makes
`project_id` optional: a key created without one is "unscoped" and spans
every project its owner can reach, bounded by the intersection of the
owner's permissions and the key's own attached policies. Multi-project
access is now expressed through IAM policies rather than a per-key project.

- ApiKey.projectId is now nullable (was NOT NULL)
- POST /api-keys: project_id no longer required; omit or null → unscoped
- PUT /api-keys/:id: project_id null clears the scope; a value (re)scopes it
- auth: resolveProjectKey tolerates a null project and builds an unscoped
  resolveProjectIds that enumerates reachable projects via the key's own
  authorizer (which already intersects owner + key policies)
- extracted the project-id resolver factories into authProjectResolvers.ts
- OpenAPI, module docs, and smoke tests updated

Scoped-key behavior is unchanged: a key with a project is still hard-locked
to it. The intersection ceiling still applies — an unscoped key can never
exceed its owner's permissions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKN1qmitp2qFuGW1TzxkKg
@github-actions

Copy link
Copy Markdown

Deploy Outputs

Package Stack Output Key Output Value
@soat/website SoatWebsite-claude-admin-api-key-full-access-9i0ppl BucketWebsiteURL http://soatwebsite-claude-admin-api-key-full-staticbucket-zjdaqjuogqvi.s3-website-us-east-1.amazonaws.com

@arantespp
arantespp merged commit 00360c2 into main Jul 18, 2026
6 checks passed
@arantespp
arantespp deleted the claude/admin-api-key-full-access-9i0ppl branch July 18, 2026 10:24
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