feat: allow API keys to log into admin panel with limited scope#160
feat: allow API keys to log into admin panel with limited scope#160mcowger merged 10 commits intomcowger:mainfrom
Conversation
Lets an api_keys holder log in to the admin panel using their secret and
see a scoped view of their own activity, with self-service for comment
edits, secret rotation, and per-key trace capture. The ADMIN_KEY continues
to grant full access.
- New `authenticate` / `requireAdmin` preHandlers attach a `Principal`
(admin | limited) to every management request. Limited users are
resolved from the in-memory config.keys map via secret match.
- Scoped routes (usage, logs, summary, concurrency, errors, debug logs)
force-inject `apiKey = principal.keyName` with exact-match semantics
for limited users. Admin behavior is unchanged.
- Admin-only routes (providers, models, keys, quotas, mcp, config,
system-logs, metrics, performance, restart, oauth, logging, test) are
gated by `requireAdmin`.
- DebugManager refactored to `enabledGlobal` + `enabledKeys: Set<string>`;
capture decision is per-key. An `AsyncLocalStorage` request context
carries the active keyName through the inference pipeline so
`DebugManager` resolves it automatically — no per-call-site plumbing.
- Cooldowns: per-provider clear now rejects limited users whose
`allowedProviders` list excludes that provider; clear-all is admin-only.
- Self-service endpoints: `GET /self/me`, `POST /self/rotate`,
`PATCH /self/comment`, `POST /self/debug/toggle`. Rotating the secret
preserves all historical data because scoping is keyed on the stable
key name, not the secret.
- Verify endpoint now returns `{ role, keyName?, allowedProviders?,
allowedModels?, quotaName?, comment? }` so the frontend can render a
role-appropriate UI without follow-up calls.
- Audit log lines for cooldown clears, rotations, and trace toggles.
- Migration 0027/0030 adds `api_key` column to `inference_errors` and
`debug_logs` with requestId-joined backfill, plus indexes on
`request_usage(api_key, start_time)`, `inference_errors(api_key)`,
`debug_logs(api_key)`.
- AuthContext exposes `principal`, `isAdmin`, `isLimited`. Login page
relabeled to accept either credential form.
- `ProtectedRoute` learns `requireAdmin` and redirects limited users off
admin-only pages. Admin-only routes (providers, models, keys, config,
mcp, quotas, system-logs) gated.
- Sidebar renders conditional sections: limited users see Dashboard,
Logs, Traces, Errors, My Key. Identity chip at the bottom shows key
name and role.
- New `MyKey` page: view key metadata, edit comment, toggle per-key trace
capture, rotate secret (shows new secret once).
- Cooldown-clear buttons on the dashboard show a blast-radius warning
modal; "Clear All" hidden from limited users.
- Debug/Errors pages hide bulk-delete and global filter from limited
users and show a scope indicator in the header.
- Logs page hides the apiKey filter for limited users (server
force-scopes regardless).
- Admin-auth test updated to assert the expanded verify response shape
and adds a case for api-key-secret login producing a limited principal.
- The `api_keys.secret` column is still present (encrypted at rest when
ENCRYPTION_KEY is set). Dropping it requires a code-level backfill to
guarantee `secretHash` for all rows regardless of encryption config;
deferred to a separate PR.
- _principal: replace plain secret `===` with constant-time hash compare
(SHA-256 + timingSafeEqual), and walk the full keys list so the
rejection path doesn't leak a count-of-keys-before-match timing signal.
- debug: restore the `enabled` field in the limited-user GET /debug
response as OR of global + per-key, so older frontend callers keep
working.
- usage-storage: saveError now prefers request-context keyName (seeded
by v1 auth middleware via AsyncLocalStorage) over the last-resort DB
lookup, reducing null api_key rows when request_usage insertion lags
behind the error path.
- error responses: make every 403 from a scoped route return the same
{ error: { message, type, code } } shape instead of a bare string.
- usage: compute scopedKeyName once for the concurrency timeline query
instead of calling it twice on the same request.
- MyKey rotate: after the server returns the new secret, call login()
to refresh localStorage and the AuthContext principal so subsequent
requests use the new secret (otherwise the old secret is evicted on
the first 401, locking the user out of the modal showing the secret).
- Logs: gate the "Delete All" button behind isAdmin (previously visible
to limited users, backend rejected the call).
- AuthContext: add a monotonic verify sequence so a slow mount-time
verify promise can't clobber a newer login()/rotate() principal if
they resolve out of order.
- minor: drop redundant `isLimited && principal?.role === 'limited'`
checks in Debug/Errors/Logs in favor of `isLimited && principal?.keyName`.
Intentional non-fixes:
- Migration journal timestamps (0027/0030): hand-set round-number values
follow existing repo precedent (0021_add_secret_hash uses
1774000000000; 0023_add_secret_hash + 0024 in migrations_pg similar).
Values are monotonically after all existing entries so migration
ordering is correct. Regenerating via `bunx drizzle-kit generate`
requires up-to-date schema snapshots and a running environment we
don't have here.
- Migration 0027 correlated subquery backfill: Plexus instances are
typically small; one-shot UPDATEs run during startup migration are
fine for expected table sizes. Batching adds significant migration
complexity for unclear benefit; deferred.
Introduces an "Overall" tab (prepended to the Dashboard for limited principals only) that rolls identity, allowed providers/models, quota status, and usage totals onto a single page. Backed by a new `/v0/management/self/quota` endpoint that mirrors the admin quota-status shape and handles the no-quota-assigned case gracefully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clear summary, provider, and model data when the time range changes so switching ranges shows "Loading…" instead of rendering the prior range's numbers under the new label. Distinguish quota fetch failures from "no quota assigned" with a dedicated error state, so quota-gated users aren't told their key is unrestricted when the request actually failed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SQLite: 0027_green_emma_frost (api_key cols on debug_logs, inference_errors) PG: 0031_last_wallow (same, sequenced after 0030_acoustic_katie_power)
|
Thanks for this PR — the overall approach is solid and the self-service UX for limited users is well thought out. I've rebased the branch onto main and regenerated the migrations properly (SQLite Two things need to be addressed before this can merge: 1. Events SSE endpoint leaks all keys' activity to limited usersFile: The endpoint sits inside the This breaks the isolation guarantee that is the whole point of limited-principal scoping. An adversarial key holder could passively monitor the usage patterns, provider availability, and error rates of every other key. Fix: Call 2. Cooldown clear is accessible to all limited users regardless of
|
…aring cooldowns The management events SSE stream broadcast every key's started/updated/completed records to any authenticated principal, leaking cross-key request activity to limited users. Scope the stream by apiKey for limited principals; admins are unaffected. Cooldown clear endpoints were gated by an allowedProviders check that fell through to "allow" when the list was empty, letting any limited user force a provider retry. Move both clear endpoints behind requireAdmin — limited users retain read-only access to GET /cooldowns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@mcowger all ready for ya! |
|
@claude review this please, with a valley girl style. |
|
Claude finished @mcowger's task in 2m 29s —— View job Valley Girl Code Review™ ✨Like, omigod, I am SO done reading all 36 files. Here's my full take bestie:
Overall Vibe ✨Okay so like, this PR is giving MAJOR energy and the concept is totally cute? Limited users being able to see their own stuff is SO self-service-core and I am here for it. But bestie, 7,356 additions and 355 deletions across 36 files is literally giving me anxiety. That is a LOT of glow-up for one PR. Like, we could have totally split this into smaller looks and it would have been SO much more reviewable. Just saying. The Security Tea ☕ (This is, like, the most important part???)Issue 1: The SSE Gossip Leak — Oh. My. God. The original mcowger called this out and darkspadez said it's fixed. I cannot verify the fix is perfect from here since the actual PR diff isn't in this checkout, but the fix pattern mcowger described — filtering by Issue 2: The Cooldown Power Trip — Okay so the base code here is literally 23 lines and it's giving "open bar to everyone" vibes — no restriction whatsoever on the DELETE endpoints. mcowger's review said the PR added an The correct fix (making Architecture Glow-Up 💅The The new Frontend Notes 💄
Minor Notes That Are Still Giving Me Concern 🤔
The Verdict 💋This PR is mostly giving what it's supposed to give. The concept is solid, the scoping design is thoughtful, and the self-service UX is a real improvement. The two security issues mcowger flagged were the real tea here, and they appear to have been addressed. Before merging I'd just wanna triple-check:
Like, once those three things are verified? This is SO ready to ship. For sure. ✨ |
Dropped the stale 0027_green_emma_frost (SQLite) and 0031_last_wallow (PG) artifacts and let drizzle-kit regenerate clean outputs: SQLite → 0027_clear_switch PG → 0031_colossal_sauron
main had already claimed SQLite 0027 (thin_norrin_radd) and PG 0031 (eminent_alex_power), so the PR's schema additions now land at: SQLite → 0028_dapper_pestilence PG → 0032_fast_lethal_legion Also includes main's 0027/0031 files so the migration chain is complete.
Replace PR-branch migration numbers (SQLite 0027/0028, PG 0030/0031) with the correct next-available indices (SQLite 0029, PG 0033) now that main has advanced past those slots via PR mcowger#160. Also sync the postgres user-quota-definitions enum values (monthly, cost) that were missing from the fork's schema files.
… with limited scope # Conflicts: # packages/backend/drizzle/migrations/meta/_journal.json # packages/backend/drizzle/migrations_pg/meta/_journal.json
The following PR allows for users to login with their api key and view their logs, statistics, and enable/disable trace. Also allows them to rotate their key.