Skip to content

Add scoped admin API keys - #1795

Merged
skyfallwastaken merged 3 commits into
mainfrom
scoped-admin-api-keys
Nov 8, 2026
Merged

skyfallwastaken merged 3 commits into
mainfrom
scoped-admin-api-keys

Conversation

@skyfallwastaken

@skyfallwastaken skyfallwastaken commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Summary of the problem

Every admin API key has full access to the whole admin API. A key made for a read-only tool can also convict users, change admin levels and approve deletions, so a leaked key does as much damage as possible.

Describe your changes

  • AdminApiKey now has scopes. There are 13 scopes such as users:read, heartbeats:read, bans:write and permissions:write, and each /api/admin/v1 endpoint requires exactly one. A key without it gets a 403 naming the missing scope. The owner's admin level still applies on top, so a scope never grants more than the owner has.
  • Controllers declare scopes with require_admin_api_key_scope. An action with no declared scope rejects keys, and a test fails if a new admin API route is added without one.
  • /api/admin/v1/check works with any scope and now returns the key's scopes and expiry. The legacy /api/v1/users/lookup_* and /api/v1/stats endpoints need users:read. OAuth admin tokens are unchanged.
  • A key can only create keys with scopes it has itself.
  • The new key form has a scope checklist and the index and detail pages show each key's scopes and expiry.
  • The migration gives every existing key all scopes and sets expires_at to 48 days later. Expired keys stop authenticating immediately and RevokeExpiredAdminApiKeysJob marks them revoked every 10 minutes.
  • AdminApiKeyExpiryNoticeJob emails each admin once with their expiring keys and when each will be revoked. Keys are marked as notified in the same transaction that enqueues the email and only one run can happen at a time, so it never sends twice. It is not on cron, so run it once after the migration:
AdminApiKeyExpiryNoticeJob.perform_later
class Api::Admin::V1::BansController < Api::Admin::V1::ApplicationController
  require_admin_api_key_scope "bans:read", only: :show
  require_admin_api_key_scope "bans:write", only: %i[create destroy]
end

Performance

In-process requests through the full Rails stack with a fully scoped key, 3 alternating rounds of 3000 requests per endpoint against main. Median ms.

Endpoint main This PR Queries per request
GET /api/admin/v1/check 1.42 1.50 2 (unchanged)
GET /api/admin/v1/user/info 5.52 5.47 11 (unchanged)
GET /api/v1/users/lookup_slack_uid 1.80 1.85 3 (unchanged)

The key lookup still uses the unique token_bidx index and the expiry check filters that one row. The scope check is in memory.

Screenshots / Media

AI Usage

  • Model: Claude Opus 5.5 (claude-opus-5-5)
  • Agent: Amp (thread)
  • Reasoning effort: Amp default for the mode
  • The human sent 6 messages: the initial request, asking to drop the cron entry for the email job so they can trigger it manually, asking for this PR, asking to simplify it, remove useless comments and tests and measure the performance impact, asking whether the revocation cron should go too and then asking to keep the revocation cron. Their critiques were dropping the email job's cron entry and the simplification request.

Each admin API endpoint now requires one AdminApiKey scope. Existing keys
keep every scope but expire 48 days after the migration, and
AdminApiKeyExpiryNoticeJob emails their owners once about it.
Copilot AI balanced review requested due to automatic review settings November 8, 2026 19:54

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@greptile-apps

greptile-apps Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The latest change appears safe to merge; the earlier request for revocation-job tests remains open.

Findings

  1. P2 Key revocation lacks tests ▶
Fix with agent prompt
### Issue 1
app/jobs/revoke_expired_admin_api_keys_job.rb:undefined-5
The PR deletes the only test for `RevokeExpiredAdminApiKeysJob` while changing how it selects keys. This violates the repository requirement to include tests for new or changed functionality. Before merging, restore coverage that checks expired keys are revoked and renamed, while future and permanent keys stay untouched.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds scoped admin API keys, expiry for existing keys and expiry notices.

Since the last review:

  • Admin API keys now need a matching scope for each action.
  • Admins can choose scopes when they create an API key.
  • Existing admin API keys expire after a 48-day transition.
  • Admins can get one email before their old keys expire.

Reviews (3) · Last reviewed commit: "Show the admin API key token preview wit..." · Reviewed by Greptile

Require heartbeats:read for user stats, which returns raw heartbeats.
Mark keys as notified in the same transaction that enqueues the expiry
email, and never run two notice jobs at once. Show expired or revoked
keys as dead. Trim the mailer, a test and redundant assertions.
Drop the usage card and its copy from the key page.
@skyfallwastaken
skyfallwastaken enabled auto-merge (squash) November 8, 2026 20:22
@skyfallwastaken
skyfallwastaken merged commit 5eec502 into main Nov 8, 2026
19 checks passed
@skyfallwastaken
skyfallwastaken deleted the scoped-admin-api-keys branch November 8, 2026 20:23
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