fix: require an explicit workspace for admin credential issuance#520
Conversation
The four ADMIN_TOKEN-gated token/enrollment routes defaulted an omitted `workspace` to the communal `default` workspace, and `uploads admin invite create` hardcoded the same fallback. A forgotten field therefore issued a credential against the shared tenant instead of erroring. That is the accidental-co-tenancy path: enrollment redemption mints a token without creating an org membership (`redeemEnrollment`), so the recipient got files:read/files:write on `default`'s objects while appearing in no member list and in no account UI. - apps/api: `POST/GET/DELETE /admin/tokens` and `POST /admin/enrollments` now 400 `workspace_required` when the field is absent or blank. Malformed slugs keep the distinct `invalid_workspace` code so "you forgot it" doesn't read as "you typo'd it". Naming `default` explicitly still works. - CLI: `uploads admin invite create` requires `--workspace`. - /console: the workspace field no longer prefills `default`, and the invite handler refuses to submit a blank one rather than falling back. - Docs: admin-tokens.md documented the old default; ops.md's example now uses a named workspace. Prod state at time of change: no accidental co-tenancy exists — `default` has one member (the operator), every other workspace has one member except the team org, and there are zero pending invites and zero enrollment rows. This closes the path before it is used, rather than cleaning up after it. Verification: pnpm test 3115/3115, pnpm typecheck clean, changed files format-clean. Refs #505
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (2)
🚫 Excluded labels (none allowed) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-auth | fbfd0cc | Commit Preview URL Branch Preview URL |
Jul 24 2026, 07:10 PM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-api | fbfd0cc | Commit Preview URL Branch Preview URL |
Jul 24 2026, 07:11 PM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-web | fbfd0cc | Commit Preview URL Branch Preview URL |
Jul 24 2026, 07:11 PM |
Closes the one path that could put unrelated users in the same workspace without anyone choosing it.
The problem
Four
ADMIN_TOKEN-gated routes defaulted an omittedworkspaceto the communaldefaultworkspace, anduploads admin invite createhardcoded the same fallback:workspacewas omittedPOST /admin/tokensdefaultPOST /admin/enrollmentsdefaultGET /admin/tokensdefault's tokensDELETE /admin/tokensdefaultuploads admin invite create--workspacedefaulted todefault/consoleinvite formdefault, and blank fell back to itSo an operator inviting someone "to their workspace" and forgetting the field handed out
files:read/files:writeon the shared tenant instead of getting an error.What makes this worse than a mis-scoped grant: enrollment redemption mints a token without creating an org membership (
redeemEnrollment,apps/api/src/auth-db.ts). The recipient could read and writedefault's objects while appearing in no member list and in no account UI — invisible co-tenancy.The
DELETEcase fails the other way: intending to revoke in workspace X, omitting it, and revoking indefaultinstead — or believing you revoked something when you hadn't.The fix
workspaceis required everywhere. Omitted or blank is now400 workspace_required; malformed slugs keep the distinctinvalid_workspacecode, so "you forgot the field" doesn't read as "you typo'd the slug". Namingdefaultexplicitly still works — this gates omission, not the value.The CLI change matters independently: because it hardcoded the string
"default"rather than reading the shared constant, it would have kept sendingdefaultexplicitly and sailed past the API guard.Production state
Checked before changing anything (aggregates only):
defaulthas one member — the operator.No accidental co-tenancy exists today. This closes the path before it gets used rather than cleaning up after it — which is also why there's no migration or backfill here.
Scope
Deliberately narrow. This does not retire the communal workspace, move objects, change any public URL, or touch the
communalflag / member-cap exemption / reserved slug. Those are the rest of the de-privileging cleanup discussed in #505.Verification
pnpm test— 3115/3115 (240 files)pnpm typecheck— cleanoxfmtNew
apps/api/src/routes/admin-workspace-required.test.tspins the rule per route (omitted and blank, both codes) so the default can't be reintroduced one handler at a time, plus a CLI test asserting the guard fires before any network call.Six tests in
routes-auth.test.tsrelied on the old default while testing scopes and email behavior; each now passes an explicit workspace. One of them (labeltoo long) would otherwise have kept passing for the wrong reason —workspace_requiredfires before label validation — so it needed the fix to keep testing what it claims to.Refs #505.