Skip to content

feat: enforce API key scopes#356

Merged
taqh merged 5 commits into
mainfrom
feat/enforce-api-scopes
Jun 17, 2026
Merged

feat: enforce API key scopes#356
taqh merged 5 commits into
mainfrom
feat/enforce-api-scopes

Conversation

@taqh

@taqh taqh commented Jun 17, 2026

Copy link
Copy Markdown
Member

Description

Enforces stored API key scopes across the v1 API routes. The API key middleware now exposes key scopes on the request context, and a new scope authorization middleware rejects requests that do not have the required read or write scope for the target resource.

This also centralizes API key scope constants in @marble/utils, adds fields_read and fields_write for upcoming custom fields API work, rejects write scopes on public API keys in CMS key management, and removes the undocumented /v1/cache/invalidate API-key route while keeping the system-authenticated /cache/invalidate route.

Motivation and Context

API key scopes were being collected and stored, but API requests were not enforcing them. This meant public/read-only keys could still reach write endpoints if they were otherwise valid.

The cache invalidation change keeps invalidation as a system-only path. Repo references call /cache/invalidate with X-System-Secret, and the live production OpenAPI spec does not advertise /v1/cache/invalidate.

How to Test

  1. Run pnpm lint.
  2. Generate the Prisma client after the enum update with pnpm --filter @marble/db db:generate.
  3. Start the API locally.
  4. Create one public API key and one private API key.
  5. Confirm public keys can read normal resources like posts, categories, tags, authors, and media.
  6. Confirm public keys are rejected with 403 for write endpoints.
  7. Confirm public keys are rejected with 403 for restricted post reads such as GET /v1/posts?status=draft and GET /v1/posts?status=all.
  8. Confirm private keys still pass read/write scope authorization.
  9. Confirm POST /cache/invalidate requires X-System-Secret and POST /v1/cache/invalidate returns 404.

Screenshots (if applicable)

N/A

Video Demo (if applicable)

N/A

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

New Features

  • API key scope-based authorization now enforces granular resource access control (including draft post reads and field access), denying requests with missing/insufficient scopes.
  • Field resource is now available for scope-based access management.

Improvements

  • Public API keys can no longer be assigned private-only or other forbidden scopes; validation is strengthened on both creation and updates.
  • Cache invalidation is now restricted to system-authenticated access only.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marble-app Ready Ready Preview, Comment Jun 17, 2026 6:28pm
marble-web Ready Ready Preview, Comment Jun 17, 2026 6:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: add7cede-4736-4fc8-9720-c9f97f134625

📥 Commits

Reviewing files that changed from the base of the PR and between a3eb89e and 90f9533.

📒 Files selected for processing (7)
  • apps/api/src/middleware/scope-authorization.ts
  • apps/cms/src/app/api/keys/[id]/route.ts
  • apps/cms/src/app/api/keys/route.ts
  • apps/cms/src/utils/keys.ts
  • packages/db/prisma/migrations/20260617144443_add_field_api_key_scopes/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/utils/src/constants/api-key.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/db/prisma/schema.prisma
  • apps/cms/src/utils/keys.ts
  • apps/api/src/middleware/scope-authorization.ts

Walkthrough

Adds fields_read, fields_write, and posts_read_drafts to the ApiScope DB enum and centralizes all scope constants in @marble/utils. Introduces a new scopeAuthorization Hono middleware that enforces per-resource scopes and blocks draft post reads for non-private keys. Removes /cache/invalidate from the API-key v1 router, and adds write-scope guards to CMS key creation and update routes.

Changes

API Key Scope Enforcement

Layer / File(s) Summary
Scope constants, types, and DB enum
packages/utils/src/constants/api-key.ts, packages/utils/package.json, packages/db/prisma/schema.prisma, packages/db/prisma/migrations/...migration.sql
Adds API_KEY_READ_SCOPES, API_KEY_WRITE_SCOPES, API_KEY_DRAFT_READ_SCOPES, API_KEY_PRIVATE_ONLY_SCOPES, API_KEY_SCOPES, ApiScope, default scope sets, and API_KEY_SCOPE_BY_RESOURCE mapping to @marble/utils under a new ./api-key-scopes subpath export; extends the Prisma ApiScope enum and migration with posts_read_drafts, fields_read, and fields_write.
CMS keys utility refactor and scope-filtering helpers
apps/cms/src/utils/keys.ts
Replaces locally defined scope arrays and ApiScope type with re-exports from @marble/utils; adds getPublicKeyWriteScopes() and getPublicKeyForbiddenScopes() helpers to filter scopes by category.
CMS API key creation and update write-scope guards
apps/cms/src/app/api/keys/route.ts, apps/cms/src/app/api/keys/[id]/route.ts
Adds HTTP 400 validation in POST /api/keys and PATCH /api/keys/[id] that rejects public API keys carrying forbidden/write scopes, returning the offending scope list in error details.
API context: apiKeyScopes type and keyAuthorization propagation
apps/api/src/types/env.ts, apps/api/src/middleware/key-authorization.ts
Adds optional apiKeyScopes?: ApiScope[] to ApiKeyVariables and extends keyAuthorization to store the verified key's scopes array in the Hono context.
scopeAuthorization middleware implementation
apps/api/src/middleware/scope-authorization.ts
Implements the middleware factory: reads apiKeyScopes from context, blocks draft/all post reads for non-private keys with 403, derives route-specific scope requirements via API_KEY_SCOPE_BY_RESOURCE and request method, and enforces presence of required scope before calling next().
API app wiring: middleware insertion, route removal, fields constant
apps/api/src/app.ts, apps/api/src/lib/constants.ts
Inserts scopeAuthorization() into the apiKeyV1 middleware chain, removes the /cache/invalidate route from API-key v1 routing, updates imports, and adds "fields" to the ROUTES array.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant keyAuthorization
  participant scopeAuthorization
  participant RouteHandler

  Client->>keyAuthorization: API request (method + path + API key)
  keyAuthorization->>keyAuthorization: verify key, set apiKeyScopes and apiKeyType in context
  keyAuthorization->>scopeAuthorization: next()
  scopeAuthorization->>scopeAuthorization: isDraftPostRead(method, pathname, status)?
  alt draft/all posts and non-private key
    scopeAuthorization-->>Client: 403 JSON "Private key required for draft access"
  else other route
    scopeAuthorization->>scopeAuthorization: derive requiredScope via API_KEY_SCOPE_BY_RESOURCE
    scopeAuthorization->>scopeAuthorization: hasScope(apiKeyScopes, requiredScope)?
    alt scope missing
      scopeAuthorization-->>Client: 403 JSON "Missing scope: {requiredScope}"
    else scope present
      scopeAuthorization->>RouteHandler: next()
      RouteHandler-->>Client: 200 response
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • usemarble/marble#258: Adds the CMS UI and API CRUD endpoints for API keys; this PR extends the same CMS key endpoints with draft/forbidden-scope logic and shared scope utilities.
  • usemarble/marble#280: Introduces the API-key v1 request pipeline; this PR extends that pipeline with scope propagation and enforcement middleware in the same /v1/* routing.
  • usemarble/marble#309: Modifies cache invalidation in apps/api/src/routes/invalidate.ts; this PR removes that route from API-key v1 routing while preserving it under system auth.

Suggested reviewers

  • prateekbisht23

🐇 Hop hop, new scopes abound,
fields_read and fields_write found!
posts_read_drafts for private keys strong,
scopeAuthorization guards all night long.
Public keys can't sneak in writes,
Every resource now knows its rights. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: enforce API key scopes' accurately summarizes the main change—adding scope-based authorization enforcement for API keys across v1 routes.
Description check ✅ Passed The description covers all required template sections: a clear description of changes, motivation/context with justification, comprehensive testing steps, and marked types of changes (bug fix, new feature, breaking change).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enforce-api-scopes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-jobs 90f9533 Commit Preview URL

Branch Preview URL
Jun 17 2026, 06:29 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-api 90f9533 Commit Preview URL

Branch Preview URL
Jun 17 2026, 06:28 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-mcp 90f9533 Commit Preview URL

Branch Preview URL
Jun 17 2026, 06:28 PM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/utils/src/constants/api-key.ts (1)

24-37: ⚡ Quick win

Derive API_KEY_SCOPES from read/write arrays to avoid drift.

Keeping a third manual list duplicates source-of-truth and can desync from API_KEY_READ_SCOPES/API_KEY_WRITE_SCOPES.

Suggested refactor
-export const API_KEY_SCOPES = [
-  "posts_read",
-  "posts_write",
-  "authors_read",
-  "authors_write",
-  "categories_read",
-  "categories_write",
-  "tags_read",
-  "tags_write",
-  "media_read",
-  "media_write",
-  "fields_read",
-  "fields_write",
-] as const;
+export const API_KEY_SCOPES = [
+  ...API_KEY_READ_SCOPES,
+  ...API_KEY_WRITE_SCOPES,
+] as const;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/constants/api-key.ts` around lines 24 - 37, The
API_KEY_SCOPES constant is manually defined as a hardcoded array which
duplicates data already existing in API_KEY_READ_SCOPES and API_KEY_WRITE_SCOPES
arrays, risking drift between them. Instead of maintaining API_KEY_SCOPES as a
separate manual list, derive it by combining the API_KEY_READ_SCOPES and
API_KEY_WRITE_SCOPES arrays using the spread operator to ensure a single source
of truth for all available scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/utils/src/constants/api-key.ts`:
- Around line 24-37: The API_KEY_SCOPES constant is manually defined as a
hardcoded array which duplicates data already existing in API_KEY_READ_SCOPES
and API_KEY_WRITE_SCOPES arrays, risking drift between them. Instead of
maintaining API_KEY_SCOPES as a separate manual list, derive it by combining the
API_KEY_READ_SCOPES and API_KEY_WRITE_SCOPES arrays using the spread operator to
ensure a single source of truth for all available scopes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12bfb9da-814d-42b3-aad9-861f8cf52090

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe4912 and 3d4fdc0.

📒 Files selected for processing (14)
  • apps/api/src/app.ts
  • apps/api/src/lib/constants.ts
  • apps/api/src/middleware/key-authorization.ts
  • apps/api/src/middleware/scope-authorization.ts
  • apps/api/src/routes/invalidate.ts
  • apps/api/src/types/env.ts
  • apps/cms/src/app/api/keys/[id]/route.ts
  • apps/cms/src/app/api/keys/route.ts
  • apps/cms/src/utils/keys.ts
  • packages/db/prisma/migrations/20260617144443_add_field_api_key_scopes/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/utils/package.json
  • packages/utils/src/constants/api-key.ts
  • packages/utils/src/types/api-key.ts
💤 Files with no reviewable changes (1)
  • apps/api/src/routes/invalidate.ts

@vercel vercel Bot temporarily deployed to Preview – marble-app June 17, 2026 17:50 Inactive
@taqh taqh marked this pull request as ready for review June 17, 2026 17:51
@taqh

taqh commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@taqh taqh merged commit 84f2a64 into main Jun 17, 2026
13 checks passed
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