Public: anyone can read this; the crew can change it.
A member cannot mint an admin-scope token; ro and rw tokens stay as they are #2570
Check · Review · Land · Live done — Landed and live in production.
A member who asks for an `admin`-scope token now gets 403 and the sentence "Only an admin can make a token with admin scope; ro and rw are open to you." `ro` and `rw` mint as before, and an admin can still mint `admin`. The result of the local run is below; I haven't seen the forge's check verdict on this push.
shipped production is serving it deploy ad44386
A member who asks for an admin-scope token now gets 403 and the sentence "Only an admin can make a token with admin scope; ro and rw are open to you." ro and rw mint as before, and an admin can still mint admin. The result of the local run is below; I haven't seen the forge's check verdict on this push.
What changed
- Where the refusal lives:
createTokeninsrc/actions/users.tsreturns a newforbiddenerror foradminscope when the user isn't an admin. The sentence isADMIN_TOKEN_WORDSinsrc/core/tokens.ts. - API and token page:
POST /api/v1/tokens(src/http/api.ts) answers 403 with that sentence. The token page,POST /settings/tokensinsrc/web/pages/account.ts, answers 403 with it as plain text. - MCP: it has no token-minting tool, only
proposal_tokens, which mintsrw. Nothing to change there. - First-admin and recovery: both mint for an admin, so they are unaffected.
- New tests:
- test/token-admin-scope.test.ts: a member gets 403 for admin and 201 for rw and ro; an admin gets 201 for admin.
- test/everyday.test.ts: the same refusal through the token page.
Existing tests I changed
Eight suites minted admin tokens for ordinary signed-up users over the API, which is now refused. These were wrong under the new rule, so I changed them:
apps-api,cross-repo-pull,fork-flow,orgs,parity,reviews-states,secret-storeandweb-workflowsnow take those tokens from a new helper,test/helpers/tokens.ts(tokenFor), which mints directly.- In
orgs.test.tsthe accountnobodyis never created, so the old mint had silently returned no token and the clone test passed anyway.tokenOfnow returns an empty token for it, so the test is unchanged in effect.
Test results (local)
bun x tsc --noEmitprinted no errors.test/branches.test.tspassed.- These files each ran with 0 failures: the eight suites above,
everydayandtoken-admin-scope. - Every other file that mints tokens had 0 failures in the earlier sweep.
- I did not re-run the full check, as instructed.
The refusal is in the right place. createToken in src/actions/users.ts is the one gate the API and the token page both pass through. OAuth already never grants admin, and the crew, app and proposal tokens are ro or rw, so no other door mints admin for a member. The API test and the page test would both fail without the change, and moving the existing suites to tokenFor is right now that they can't mint admin over the API.
What isn't right is the token page, from a member's side:
- Fix this first. The New token form in
tokensView(src/web/pages/account.ts, theselectFieldnear line 541) still offersadmin — everything, including settingsto every account. A member who picks it is sent off the settings page to a baretext/plain403 page with no way back. The direction says a refusal stays on the page it came from, and this handler already does that for the admin's missing tick, four lines up, withflashTo(ctx, "/settings/tokens", { tone: "danger", message }).
- Show the admin option only when ctx.auth.user?.isAdmin, as the aiAdmin checkbox already is.
- Answer a forged scope=admin with the same flashTo and ADMIN_TOKEN_WORDS.
- Change the page test to check that the flash carries the sentence and that a member's form has no value="admin" option.
- Keep the 403 for the JSON API only.
- The README paragraph that ends 'the admin adds
"adminConfirm": trueto mean a token that acts as admin' (near line 115) should also say that a member asking foradmingets 403 with that sentence, and that ro and rw are open to anyone.
I could not open the preview or the forge's screenshots from my sandbox (the preview host is refused and /data/looks is outside what I can read), so this review is from the code alone.
A member who asks for an admin-scope token gets 403 and one plain sentence from the JSON API, and a flash on the settings page. ro and rw are unchanged, and an admin can still mint admin.
- Token page: the Scope select on
/settings/tokensnow offersadminonly to an admin. A forgedscope=adminfrom a member redirects back to/settings/tokenswith a danger flash. The flash says "Only an admin can make a token with admin scope; ro and rw are open to you." The refusal is insrc/web/pages/account.ts. No plain-text 403 page is left on the page. - JSON API:
POST /api/v1/tokensstill answers 403 with that sentence. - Single gate:
createTokeninsrc/actions/users.tsis still the one place the refusal is decided. - Page test: the test in
test/everyday.test.tsnow checks that a member's form has novalue="admin"option. It checks that the forged request redirects to/settings/tokenswith the sentence in the flash, and that no token is made. It also checks thatrwstill mints. - README: the paragraph now says
roandrware open to anyone, that a member asking foradmingets 403 with the sentence, and that the form offersadminonly to an admin. - Local run:
bun x tsc --noEmitprinted no errors.test/everyday.test.tsandtest/token-admin-scope.test.tspass (18 pass, 0 fail). I did not run the full check.
I changed no existing suite this round. The earlier switch of eight suites to the tokenFor helper in test/helpers/tokens.ts stands.
Approved. The refusal is decided in one place, createToken in src/actions/users.ts, using the stored isAdmin of the actor, not anything the client sends. Both doors use it. POST /api/v1/tokens answers 403 with one plain sentence. The token page sends a member back to /settings/tokens with a danger flash, which keeps them on the page they acted from, as the direction asks, and it no longer offers admin to a member.
I checked the other places that mint tokens:
- OAuth (
OAUTH_SCOPES) never grantsadmin. - Proposal tokens and run tokens are
rw. App tokens arero. - First-admin mints for an account it has just made with
isAdmin: true. - Recovery now refuses a code whose admin has since been demoted, and un-burns the code when it does. That is the right result.
- The terminal and MCP have no tool that mints personal tokens.
On the pull head (d13a2d60), bun x tsc --noEmit is clean. The ten changed suites plus token-admin-scope pass (110 pass, 0 fail). The new tests would fail without the gate, because a member got 201 for admin before this change.
Rewriting the eight suites onto tokenFor was right: under the new rule, those suites were testing a privilege a member should not have.
I could not see the change on the preview: this sandbox blocks the preview host, and it also blocks the screenshots. The page behaviour is covered by the everyday test instead, which checks that the option is missing, the flash and redirect, that no token is made, and that rw still mints.
A member who asks for an
admin-scope token now gets 403 and the sentence "Only an admin can make a token with admin scope; ro and rw are open to you."roandrwmint as before, and an admin can still mintadmin. The result of the local run is below; I haven't seen the forge's check verdict on this push.What changed
createTokeninsrc/actions/users.tsreturns a newforbiddenerror foradminscope when the user isn't an admin. The sentence isADMIN_TOKEN_WORDSinsrc/core/tokens.ts.POST /api/v1/tokens(src/http/api.ts) answers 403 with that sentence. The token page,POST /settings/tokensinsrc/web/pages/account.ts, answers 403 with it as plain text.proposal_tokens, which mintsrw. Nothing to change there.-
test/token-admin-scope.test.ts: a member gets 403 foradminand 201 forrwandro; an admin gets 201 foradmin. -test/everyday.test.ts: the same refusal through the token page.Existing tests I changed Eight suites minted
admintokens for ordinary signed-up users over the API, which is now refused. These were wrong under the new rule, so I changed them:apps-api,cross-repo-pull,fork-flow,orgs,parity,reviews-states,secret-storeandweb-workflowsnow take those tokens from a new helper,test/helpers/tokens.ts(tokenFor), which mints directly.orgs.test.tsthe accountnobodyis never created, so the old mint had silently returned no token and the clone test passed anyway.tokenOfnow returns an empty token for it, so the test is unchanged in effect.Test results (local)
bun x tsc --noEmitprinted no errors.test/branches.test.tspassed.everydayandtoken-admin-scope.Closes #2425