Skip to content

Commit fd50e59

Browse files
claude[bot]os-zhuangclaude
authored
tests(identity): pin what a platform admin gets from every /admin/ route, both the 2xx side and the eight refusals that are by design (#11899)
* tests(identity): pin the platform admin's answer on every /admin/ route (#9482) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR * tests(identity): declare the by-design payloads with their entries (#9482) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR * tests(identity): re-grade the checklist item's admin clauses (#9482) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR --------- Co-authored-by: os-zhuang <jack@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 50369dc commit fd50e59

2 files changed

Lines changed: 741 additions & 5 deletions

File tree

docs/qa/platform-checklist/areas/identity-auth.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@
370370
"title": "Admin user-lifecycle operations (ban/unban, set-password, impersonate, create/set-role/remove, revoke-sessions) enforce, persist, and stay closed to non-admins",
371371
"since": "v16",
372372
"status": "active",
373-
"revision": 4,
373+
"revision": 5,
374374
"priority": "P1",
375375
"surface": "mixed",
376376
"personas": ["platform admin", "target user", "non-admin forger"],
@@ -381,8 +381,9 @@
381381
"a disposable target user with a known password (create one; do not ban a seeded persona other items depend on)"
382382
],
383383
"knownGaps": [
384-
"MEASURED 2026-08-18 (#9482): the better-auth-native /admin/ endpoints are unusable by an ObjectStack platform admin on a stock boot. The vendor's adminMiddleware gates on the legacy user.role === 'admin' scalar; ADR-0068 D2 stopped synthesizing it, so the seeded dev admin (role='user', positions=['user','platform_admin']) is refused 403 YOU_ARE_NOT_ALLOWED_TO_* by ban/unban/set-role/remove-user/impersonate/revoke-user-session(s)/list-users/get-user/list-user-sessions/update-user. That is why /admin/create-user and /admin/set-user-password exist as ObjectStack raw mounts carrying the ADR-0068 gate (admin-user-endpoints.ts says so in its header). A runner hand-driving the ban or impersonate STEPS will hit that 403 — it is the recorded state of the surface, not a fixture mistake. The sys_user Ban/Unban/Impersonate actions are type:'api' proxies onto those same routes, so the console affordances inherit it."
385-
]
384+
"SUPERSEDED 2026-08-24 (#9482) — the note below is kept because a runner may still meet it on an OLD build, but it no longer describes current main. The 2026-08-18 measurement said EVERY better-auth-native /admin/ endpoint refuses an ObjectStack platform admin. Since then #9970 re-mounted ban-user/unban-user with the ADR-0068 gate and #10352 re-authorized impersonate-user as a better-auth PLUGIN endpoint (consolidated onto hasPlatformAdminStanding by #11686). Re-measured on main 2026-08-24: the platform admin gets 200 from ban-user, unban-user, create-user, set-user-password, unlock-user, import-users, oauth2/toggle-disabled and impersonate-user. EIGHT routes still answer the platform admin 403 YOU_ARE_NOT_ALLOWED_TO_*, and that is now a RULED OUTCOME rather than a gap: remove-user, revoke-user-session, revoke-user-sessions, list-user-sessions, update-user, list-users and get-user (#9969, closed not_planned — no ObjectStack consumer, re-implement on demand), plus set-role (#9968, ruled B — the sys_user action was RETIRED by PR #11530 and the vendor route deliberately left vendor-gated, because its only effect is writing the legacy scalar ADR-0068 D2 retired). A runner who meets a 403 on any of those EIGHT is seeing the intended design, not a fixture mistake and not a bug to file.",
385+
"HISTORICAL (measured 2026-08-18, superseded by the note above): the better-auth-native /admin/ endpoints are unusable by an ObjectStack platform admin on a stock boot. The vendor's adminMiddleware gates on the legacy user.role === 'admin' scalar; ADR-0068 D2 stopped synthesizing it, so the seeded dev admin (role='user', positions=['user','platform_admin']) is refused 403 YOU_ARE_NOT_ALLOWED_TO_* by ban/unban/set-role/remove-user/impersonate/revoke-user-session(s)/list-users/get-user/list-user-sessions/update-user. That is why /admin/create-user and /admin/set-user-password exist as ObjectStack raw mounts carrying the ADR-0068 gate (admin-user-endpoints.ts says so in its header). A runner hand-driving the ban or impersonate STEPS on a build from before 2026-08-24 will hit that 403."
386+
]
386387
},
387388
"steps": [
388389
"GET /api/v1/auth/config and confirm features.admin is advertised (opt-in flag; it gates the sys_user ban/unban/set-password/impersonate actions)",
@@ -469,7 +470,7 @@
469470
],
470471
"automated": {
471472
"kind": "e2e",
472-
"ref": "packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts (clause 8 — the sys_audit_log shapes create-user / set-user-password / import-users leave) + packages/qa/dogfood/test/admin-credential-lifecycle.dogfood.test.ts (clauses 1 and 2, both sides: an explicit password supplied ALONGSIDE generatePassword:true is the one that signs in and no generated password comes back, with a generatePassword-only create as the contrast so the clause cannot be satisfied by generatePassword being inert; the created account carries its credential sys_account row; set-user-password rotates — the original password is proven to work BEFORE the rotation, the new one after, and the old one is refused 401 INVALID_EMAIL_OR_PASSWORD; a member's forged create-user and set-user-password are each 403 PERMISSION_DENIED and an anonymous call 401 UNAUTHENTICATED, each with the state read back — no user row left behind, the victim's real password still working, the hijack password not) + packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts (clause 9, REFUSAL half, over a population DERIVED from the running stack rather than listed: honoApp.routes for the 9 ObjectStack raw mounts registered ahead of better-auth's catch-all, UNION auth.api for the 24 endpoints the catch-all publishes — 31 routes at this configuration. The derived set and the suite's classification table are checked for exact agreement in BOTH directions, so a newly mounted /admin/ route fails the suite by name until someone records what a non-admin must get from it. Universal invariant over all 31: neither an anonymous caller nor a plain member ever receives a 2xx, the single classified exception being has-permission, whose 200 body is asserted to be the negative answer. Every route is fired with a payload valid enough to REACH the authorization check — measured: better-auth and the ObjectStack sso / unlock-user / oauth2 mounts validate the body FIRST, so an empty-body route-walk draws an identical 400 VALIDATION_ERROR for member and admin alike and asserts nothing about the gate). NOTE this item's clause-9 prose names SIX routes; the measured surface is 31 — do not tick 31 routes off a 6-route reading. STILL MANUAL: clause 7 (the console's impersonation indicator) — its oracle is a screenshot, which no dogfood test can drive. BLOCKED, NOT MERELY UNPINNED: clauses 0, 3, 4, 5, 6 and the ALLOWED side of clause 9's better-auth bucket are not automatable without a product decision. better-auth's admin plugin authorizes on the legacy user.role === 'admin' scalar (auth-manager.ts constructs admin() with schema only, so the vendor default adminRoles:['admin'] applies) while ADR-0068 D2 deliberately stopped synthesizing that scalar, contributing platform_admin to positions[] instead. Measured on the seeded dev admin: sys_user.role is 'user', positions is ['user','platform_admin'], and ban-user / unban-user / set-role / remove-user / impersonate-user / revoke-user-session(s) / list-users / get-user / list-user-sessions / update-user each answer the PLATFORM ADMIN 403 YOU_ARE_NOT_ALLOWED_TO_*. Confirmed by construction: writing role='admin' onto that same admin row flips ban-user, unban-user, list-users and impersonate-user to 200 in the same boot. Clause 9's refusal half is pinned and real; its allowed half is asserted NOWHERE on that bucket, because pinning today's admin-is-also-refused behaviour would turn the fix red and pinning the fixed behaviour would be red today. See #9482."
473+
"ref": "packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts (clause 8 — the sys_audit_log shapes create-user / set-user-password / import-users leave) + packages/qa/dogfood/test/admin-credential-lifecycle.dogfood.test.ts (clauses 1 and 2, both sides: an explicit password supplied ALONGSIDE generatePassword:true is the one that signs in and no generated password comes back, with a generatePassword-only create as the contrast so the clause cannot be satisfied by generatePassword being inert; the created account carries its credential sys_account row; set-user-password rotates — the original password is proven to work BEFORE the rotation, the new one after, and the old one is refused 401 INVALID_EMAIL_OR_PASSWORD; a member's forged create-user and set-user-password are each 403 PERMISSION_DENIED and an anonymous call 401 UNAUTHENTICATED, each with the state read back) + packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts (clause 9, REFUSAL half, over a population DERIVED from the running stack rather than listed: honoApp.routes for the ObjectStack raw mounts registered ahead of better-auth's catch-all, UNION auth.api for the endpoints the catch-all publishes — 31 routes at this configuration. The derived set and that suite's classification table are checked for exact agreement in BOTH directions. Universal invariant over all 31: neither an anonymous caller nor a plain member ever receives a 2xx, the single classified exception being has-permission, whose 200 body is asserted to be the negative answer. Every route is fired with a payload valid enough to REACH the authorization check) + packages/qa/dogfood/test/admin-platform-admin-standing.dogfood.test.ts (NEW 2026-08-24 — clause 0, clause 6 and the ALLOWED half of clause 9: what the PLATFORM ADMIN gets, which was previously asserted NOWHERE. ADMIN-IDENTITY CONTROL runs first and every later assertion refuses to run until it passes: the subject carries platform_admin in positions[] and isPlatformAdmin true, and sys_user.role is NOT 'admin' — so a 403 measured there can never mean 'the fixture was never an admin'. It deliberately does NOT copy remove-user-atomicity.test.ts's in-process role='admin' write, which is exactly why no earlier test observed this defect family. CLAUSE 0 + clause 9's allowed half: ban-user persists banned + ban_reason and unban-user reverses it; unlock-user, import-users and oauth2/toggle-disabled each answer 200 — every one re-read from STORAGE rather than believed from the response body. CLAUSE 6: impersonate-user answers 200 and sys_session.impersonated_by records the admin. REFUSED-BY-DESIGN: the EIGHT consumer-less routes are pinned at exactly 403 plus their exact vendor code (status AND code, ADR-0112 — a status-only pin stays green when only the code moves), each entry naming the card that ruled it, with a no-effect control proving the refused remove-user / update-user / set-role moved nothing. SWEEP over the same derived population: no route may refuse the platform admin unless it is a recorded by-design refusal, and every derived route must be classified — the classification check is the one a newly mounted route trips, and it runs first for that reason. The admitted and refused sides use SEPARATE expectation instruments, so a bug in one cannot pass the other.) NOTE this item's clause-9 prose names SIX routes; the measured surface is 31 — do not tick 31 routes off a 6-route reading. STILL MANUAL: clause 7 (the console's impersonation indicator) — its oracle is a screenshot, which no dogfood test can drive. ⛔ CLAUSES 3, 4 AND 5 ARE RULED, NOT UNPINNED COVERAGE DEBT: clause 3 (set-role flips gate outcomes), clause 4 (revoke-user-sessions kills a live session mid-flight) and clause 5 (remove-user's owner_id FK clear) each require the platform admin to COMPLETE the operation, and all three routes are among the eight that refuse him BY DESIGN — #9969 closed not_planned for the seven consumer-less routes, #9968 ruled B and retired the set_user_role action instead of re-implementing the route. What is pinned is the refusal, pinned as intended behaviour. Scoring these as missing automation would be scoring the absence of a route the maintainer declined to build. If one of them later acquires a real ObjectStack consumer and is re-mounted, admin-platform-admin-standing.dogfood.test.ts fails until its entry moves from the by-design list to the admitted list. See #9482, #9652, #9968, #9969."
473474
},
474475
"traps": ["wrong-persona", "shared-browser-tab"],
475476
"source": [
@@ -485,7 +486,13 @@
485486
{ "revision": 1, "date": "2026-08-07", "change": "new item: admin lifecycle operations with persistence, enforcement, attribution and both-sides gate checks", "ref": "claude/platform-test-checklist-ocwugl" },
486487
{ "revision": 2, "date": "2026-08-08", "change": "added admin/list-users, create-user (explicit-password-wins §E12 #3031/#3033, signs in), set-role (flips gate outcomes), remove-user, revoke-user-sessions (kills live session mid-flight), each non-admin-refused; plus the §A5 engine cascade exemption clause (delete sys_user → owned rows' owner_id set_null; owner-anchor guard does not veto the system-context cascade, #3023/#3048) (PENDING-GAPS §D + §G)", "ref": "claude/platform-test-checklist-ocwugl" },
487488
{ "revision": 3, "date": "2026-08-11", "change": "CORRECTION from run #7663: the revoke-user-sessions clause named get-session's status code as its oracle and expected 401. better-auth's no-session convention is HTTP 200 with a JSON null body (session-of-record.test.ts:165), so the literal 401 expectation misdescribes a CORRECT implementation and a status-only assertion would also pass against a live session's absence. Re-pointed the clause, the step and the negative at the authed-request oracle — a protected request the target could serve a moment earlier, refused on the very next call — with get-session's body kept as corroboration only. The session was provably gone in the run; only the oracle was wrong", "ref": "#7740" },
488-
{ "revision": 4, "date": "2026-08-18", "change": "clauses 1, 2 and the refusal half of clause 9 pinned (QA run #9401 scored this item partial on a pin reaching clause 8 only). The clause-9 pin DERIVES its /admin/ population from the running stack (honoApp.routes UNION auth.api = 31 routes, against the 6 the clause prose names) and fails on any newly mounted route until it is classified, which is what makes it catch a silently unguarded new route. automated.ref now separates what is MANUAL (clause 7, a screenshot oracle) from what is BLOCKED on a product decision (clauses 0, 3, 4, 5, 6 and clause 9's allowed side on the better-auth bucket): those routes refuse the platform admin himself, because ADR-0068 D2 stopped synthesizing the legacy role scalar the vendor's admin plugin authorizes on. knownGaps records the measurement so the next runner reads a 403 there as the surface's state, not as a fixture error", "ref": "#9482" }
489+
{ "revision": 4, "date": "2026-08-18", "change": "clauses 1, 2 and the refusal half of clause 9 pinned (QA run #9401 scored this item partial on a pin reaching clause 8 only). The clause-9 pin DERIVES its /admin/ population from the running stack (honoApp.routes UNION auth.api = 31 routes, against the 6 the clause prose names) and fails on any newly mounted route until it is classified, which is what makes it catch a silently unguarded new route. automated.ref now separates what is MANUAL (clause 7, a screenshot oracle) from what is BLOCKED on a product decision (clauses 0, 3, 4, 5, 6 and clause 9's allowed side on the better-auth bucket): those routes refuse the platform admin himself, because ADR-0068 D2 stopped synthesizing the legacy role scalar the vendor's admin plugin authorizes on. knownGaps records the measurement so the next runner reads a 403 there as the surface's state, not as a fixture error", "ref": "#9482" },
490+
{
491+
"revision": 5,
492+
"date": "2026-08-24",
493+
"change": "the ADMIN side pinned, and three clauses re-graded from 'blocked on a product decision' to 'ruled'. Revision 4 recorded that ban/unban/set-role/remove-user/impersonate/revoke-user-session(s)/list-users/get-user/list-user-sessions/update-user all refuse the platform admin; that is no longer true. Re-measured on main: #9970 re-mounted ban-user/unban-user with the ADR-0068 gate and #10352 (consolidated onto hasPlatformAdminStanding by #11686) re-authorized impersonate-user, so those answer 200 and clauses 0 and 6 are now pinned with their stored effects read back. The remaining EIGHT routes refuse the platform admin BY DESIGN — #9969 closed not_planned for seven consumer-less routes, #9968 ruled B for set-role — so clauses 3, 4 and 5 are not coverage debt but clauses whose operation the maintainer declined to build. The new pin asserts both sides with SEPARATE instruments, behind an admin-identity control proving the subject is a platform admin who does NOT carry the legacy role scalar",
494+
"ref": "#9482"
495+
}
489496
]
490497
},
491498
{

0 commit comments

Comments
 (0)