|
| 1 | +--- |
| 2 | +"@objectstack/platform-objects": patch |
| 3 | +"@objectstack/plugin-auth": patch |
| 4 | +--- |
| 5 | + |
| 6 | +fix(platform-objects,plugin-auth): let the API-key revoke/restore actions actually run (#7727) |
| 7 | + |
| 8 | +`sys_api_key` contradicted itself. It declared two row actions — |
| 9 | +`revoke_api_key` / `restore_api_key` — as `PATCH /api/v1/data/sys_api_key/{id}` |
| 10 | +with `bodyExtra: { revoked: true|false }`, while the same object set |
| 11 | +`enable.apiMethods = ['get', 'list']`. The declared PATCH was refused at the |
| 12 | +ADR-0049 method gate with `405 OBJECT_API_METHOD_NOT_ALLOWED` before any |
| 13 | +authorization ran, so **no product route revoked an API key**: the Setup → |
| 14 | +API Keys → Revoke button produced an error toast, the row still read |
| 15 | +`revoked = false`, and the key kept authenticating. A leaked key could only be |
| 16 | +retired by writing the row out of band. |
| 17 | + |
| 18 | +Enforcement of the flag was never the problem — the verifier filters |
| 19 | +`revoked: false` and re-checks the row, so a flipped bit takes effect on the |
| 20 | +very next `x-api-key` call. The missing piece was purely the write path, and it |
| 21 | +had **two** gates, not one: |
| 22 | + |
| 23 | +- **The method gate.** `enable.apiMethods` now carries `update`. `create` and |
| 24 | + `delete` stay off: minting is `POST /api/v1/keys` (the only path that ever |
| 25 | + returns the raw secret) and keys are retired by revoking, not deleting. |
| 26 | +- **The affordance reconciler.** ADR-0103's `reconcileManagedApiMethods` strips |
| 27 | + any write verb a `managedBy` object's resolved affordances do not grant — |
| 28 | + warning, not failing. So `apiMethods` alone would still have served 405 while |
| 29 | + the source read correctly. `userActions: { edit: true }` declares the |
| 30 | + affordance, exactly as `sys_user` does under ADR-0092 D4. |
| 31 | + |
| 32 | +**Opening the method does not open the columns.** `sys_api_key` stays |
| 33 | +`managedBy: 'better-auth'`, so ADR-0092 D2's identity write guard still |
| 34 | +fail-closed rejects user-context writes, and its per-object update whitelist |
| 35 | +remains the only opening. `revoked` is registered there and nothing else is: |
| 36 | +`key` stays unwritable (a rotated hash would mint a credential nobody holds), |
| 37 | +`user_id` stays unwritable (re-owning a key is privilege transfer), and |
| 38 | +`expires_at` stays on the mint path. A PATCH carrying only non-whitelisted |
| 39 | +columns is refused `403 PERMISSION_DENIED` rather than degrading into a |
| 40 | +timestamp touch, and a mixed patch applies `revoked` while stripping the rest. |
| 41 | +The guard itself is unchanged — no general weakening, and every other identity |
| 42 | +table keeps its default-deny. |
| 43 | + |
| 44 | +Per ADR-0092 D4's form-rendering constraint, the columns outside the whitelist |
| 45 | +(`name`, `prefix`, `user_id`, `scopes`, `expires_at`) are now `readonly`, so the |
| 46 | +edit form this affordance turns on cannot offer a write the server refuses — |
| 47 | +the declared-≠-enforced shape that caused the original defect. |
| 48 | + |
| 49 | +Nothing pinned any of this before: the existing tests exercise key *resolution* |
| 50 | +against a pre-revoked row and never call the route the actions declare, which is |
| 51 | +how a declared action and a method gate cancelled out unnoticed. The new |
| 52 | +`api-key-revoke-lifecycle` dogfood suite drives the real PATCH, asserts `200`, |
| 53 | +and then asserts the consequence — the key stops authenticating — because a 200 |
| 54 | +that leaves the key working is the defect wearing a success code. |
0 commit comments