Residual of #7727, found by re-testing it. #7727 is genuinely fixed for admins — PATCH /api/v1/data/sys_api_key/{id} {revoked:true} went from 405 OBJECT_API_METHOD_NOT_ALLOWED to 200, the key stops authenticating on the very next request, and the console's Setup → API Keys → Revoke row action now fires a 200 and toasts "API key revoked" instead of erroring. The method gate and the ADR-0092 column whitelist both landed correctly.
What did not land is the persona the item names first — "a signed-in member (key owner)". The declared row actions revoke_api_key / restore_api_key still render in that member's own My Keys grid, and still error for them.
Symptom
A member mints a personal API key, then cannot revoke it:
PATCH /api/v1/data/sys_api_key/{their own id} {revoked:true} → 403 PERMISSION_DENIED
- the row still reads
revoked: false
- the key still returns 200 on
GET /api/v1/data/showcase_task
Reproduced with two independent personas.
Reproduction
- As admin:
POST /api/v1/auth/admin/create-user {"email":…,"password":…,"mustChangePassword":false}
- Sign in as that member.
POST /api/v1/keys {"name":"k"} → 201; capture data.id and data.key.
GET /api/v1/data/showcase_task with x-api-key: <key> → 200.
- As the same member:
PATCH /api/v1/data/sys_api_key/{id} {"revoked":true}.
Expected 200, and the key stops authenticating.
Actual 403 {"code":"PERMISSION_DENIED"}, row unchanged, key still 200.
Root cause
#7769 opened the method gate (apiMethods now ['get','list','update']) and the ADR-0092 D2 column whitelist (revoked), but the object-CRUD layer is unchanged. The platform member_default set grants only allowRead on the BETTER_AUTH_MANAGED_OBJECTS list (packages/plugins/plugin-security/src/objects/default-permission-sets.ts), so update on sys_api_key resolves only for admin_full_access.
Confirmed with the platform's own explain route, as that member:
GET /api/v1/security/explain?object=sys_api_key&operation=update
→ allowed: false; object_crud DENIES —
"No resolved permission set grants update on sys_api_key"
(member_default does carry a sys_api_key_self RLS carve-out, so the row is visible to its owner — there is just no allowEdit to go with it.)
Why it is worth closing rather than deferring
A personal API key "acts as you — treat it like a password" (the console's own copy on the mint screen). The owner is the person who discovers it leaked, and right now their only remedy is to find an admin. It is also a dead affordance of the exact shape #7727 filed, one layer up: the button renders for the persona that cannot use it.
Suggested shape (not prescriptive)
Give member_default an owner-scoped update grant on sys_api_key narrowed to the whitelisted column, so the existing sys_api_key_self RLS carve-out decides which rows and the column whitelist decides which fields. The gate: extend the revoke test to run as the key's owner, not only as admin — the current pins exercise the admin path, which is why this half stayed hidden behind #7727's fix.
Not affected
Cross-owner revocation is correctly refused (a member PATCHing the admin's key → 403, row unchanged), non-revoked column writes are still refused with a located message, create/delete/batch stay closed (405), and show-once semantics are intact.
Source
Found by the platform checklist retest of identity-auth.api-key-ui-lifecycle (framework 279ee48a), re-running #7727's own reproduction.
Residual of #7727, found by re-testing it. #7727 is genuinely fixed for admins —
PATCH /api/v1/data/sys_api_key/{id} {revoked:true}went from405 OBJECT_API_METHOD_NOT_ALLOWEDto 200, the key stops authenticating on the very next request, and the console's Setup → API Keys → Revoke row action now fires a 200 and toasts "API key revoked" instead of erroring. The method gate and the ADR-0092 column whitelist both landed correctly.What did not land is the persona the item names first — "a signed-in member (key owner)". The declared row actions
revoke_api_key/restore_api_keystill render in that member's own My Keys grid, and still error for them.Symptom
A member mints a personal API key, then cannot revoke it:
PATCH /api/v1/data/sys_api_key/{their own id} {revoked:true}→ 403 PERMISSION_DENIEDrevoked: falseGET /api/v1/data/showcase_taskReproduced with two independent personas.
Reproduction
POST /api/v1/auth/admin/create-user {"email":…,"password":…,"mustChangePassword":false}POST /api/v1/keys {"name":"k"}→ 201; capturedata.idanddata.key.GET /api/v1/data/showcase_taskwithx-api-key: <key>→ 200.PATCH /api/v1/data/sys_api_key/{id} {"revoked":true}.Expected 200, and the key stops authenticating.
Actual
403 {"code":"PERMISSION_DENIED"}, row unchanged, key still 200.Root cause
#7769 opened the method gate (
apiMethodsnow['get','list','update']) and the ADR-0092 D2 column whitelist (revoked), but the object-CRUD layer is unchanged. The platformmember_defaultset grants onlyallowReadon theBETTER_AUTH_MANAGED_OBJECTSlist (packages/plugins/plugin-security/src/objects/default-permission-sets.ts), soupdateonsys_api_keyresolves only foradmin_full_access.Confirmed with the platform's own explain route, as that member:
(
member_defaultdoes carry asys_api_key_selfRLS carve-out, so the row is visible to its owner — there is just noallowEditto go with it.)Why it is worth closing rather than deferring
A personal API key "acts as you — treat it like a password" (the console's own copy on the mint screen). The owner is the person who discovers it leaked, and right now their only remedy is to find an admin. It is also a dead affordance of the exact shape #7727 filed, one layer up: the button renders for the persona that cannot use it.
Suggested shape (not prescriptive)
Give
member_defaultan owner-scopedupdategrant onsys_api_keynarrowed to the whitelisted column, so the existingsys_api_key_selfRLS carve-out decides which rows and the column whitelist decides which fields. The gate: extend the revoke test to run as the key's owner, not only as admin — the current pins exercise the admin path, which is why this half stayed hidden behind #7727's fix.Not affected
Cross-owner revocation is correctly refused (a member PATCHing the admin's key → 403, row unchanged), non-
revokedcolumn writes are still refused with a located message,create/delete/batchstay closed (405), and show-once semantics are intact.Source
Found by the platform checklist retest of
identity-auth.api-key-ui-lifecycle(framework279ee48a), re-running #7727's own reproduction.