Rotate administrator keys with safe re-enable and exact retry

This commit is contained in:
Quintine committed 2026-09-30 00:01:16 +10:00
1 parent 9cf8249398
commit 55b47ce039
20 files changed
+3072 -104

No files matched your search

+65 -17
View File
@@ -6,26 +6,54 @@ Open **Administration → Accounts** as an enabled owner. Operators, viewers and
Choose a name, owner/operator/viewer role and operational reason, then review the exact account ID, name, role and reason before confirming. The browser generates a new UUID and 256-bit personal access key with Web Crypto. It saves the exact request in this tab before sending it. The server stores only its SHA-256 hash and returns account metadata, **not the key**. After confirmation, save the one-time displayed key privately and explicitly clear its tab record. Refresh restores an unconfirmed request or the confirmed key until it is cleared. If the response is uncertain, retry the same saved request; do not generate a replacement request or discard the only key copy. Do not place keys in reasons, screenshots, issue reports or shared logs.
## Change or disable access
## Change roles, disable access or rotate a key
Choose an enabled account other than your own. Review its exact name, ID,
current role, requested new role or disable, and operational reason before
sending. A role change uses the current account timestamp; a stale target
is rejected and must be reviewed again. Disabling blocks the key and
permanently invalidates current sessions; this page cannot re-enable the
account. **Do not manually re-enable its database row:** without a separate
key rotation, its old access key would become usable again. Provision a new
account instead. Another owner must perform a self-demotion or self-disable.
The database refuses to remove the final enabled owner. Existing game rooms,
seats and recovery kits are not modified.
Choose another account and review its exact name, ID, current role and
enabled state, target action, account timestamp and operational reason.
Role changes and disabling require an enabled target. A stale target is
rejected and must be reviewed again. Disabling blocks the existing access
key and permanently invalidates current sessions. Another owner must
perform a self-demotion or self-disable. The database refuses to remove the
final enabled owner. Existing game rooms, seats and recovery kits are not
modified.
Each operation has an exact saved identifier and request fingerprint. Concurrent or repeated identical requests resolve to one account change and one actor-attributed audit record; a different request cannot reuse that identifier. Live owner role, session expiry/generation and target version are rechecked at the database write, not inferred from the rendered page. An uncertain role or disable response can be retried from this tab; a conflicting or revoked request remains blocked for review. Tab records are scoped to the original owner account.
**Rotate key** works for another enabled or disabled account. The browser
generates a fresh target-bound 256-bit key, saves the exact request in this
tab, and asks for confirmation of the target and session impact. Rotation
replaces the old hash, increments the session generation, revokes existing
sessions and enables the account while retaining its name and role. A
disabled account is re-enabled **only with the new key**. Previously
retired key hashes are kept privately so later A→B→A rotations cannot
revive an old credential. Never use manual SQL to re-enable an account
without rotating its key. Rotation of your own account is unavailable.
On success, save the one-time displayed key privately before clearing its
tab record. The server never returns the key. An uncertain response must
be retried with the same saved request; discarding it may lose the only
copy. A retry confirms a historical operation, not that its key still
works: another owner may have rotated or disabled the account afterward.
The page compares the latest visible directory row with the saved
operation and warns when it changed or is not on the current page.
Even matching metadata is only a last-read observation; verify sign-in
before distributing a key.
Action history shows successful account operations with safe role changes and an operational reason for authorized readers. External CLI provisioning or revocation may still have no human actor attribution. Rejected attempts are not yet part of that history. No browser account operation grants access to a playing seat or reveals saved game data.
Each operation has an exact saved identifier and request fingerprint.
Concurrent or repeated identical requests resolve to one account change
and one actor-attributed audit record; a different request cannot reuse
that identifier. Live owner role, session expiry/generation and target
version are rechecked at the database write, not inferred from the
rendered page. An uncertain response can be retried from this tab; a
conflicting or revoked request remains blocked for review. Tab records
are scoped to the original owner account.
Action history shows successful account operations with safe role/enabled
changes and an operational reason for authorized readers. External CLI
provisioning or revocation may still have no human actor attribution.
Rejected attempts are not yet part of that history. No browser account
operation grants access to a playing seat or reveals saved game data.
## Verification and remaining work
Implementation: [account query and transactions](../db/admin-accounts.ts), [request contract](../lib/admin-accounts.ts), [route](../app/api/admin/accounts/route.ts), [page](../app/admin/accounts/page.tsx) and [additive operation receipt](../drizzle/0019_admin_accounts.sql). Keep the original owner-protection and session-revocation triggers in migration `0008`.
Implementation: [account query and transactions](../db/admin-accounts.ts), [request contract](../lib/admin-accounts.ts), [route](../app/api/admin/accounts/route.ts), [page](../app/admin/accounts/page.tsx), [original receipt](../drizzle/0019_admin_accounts.sql) and [additive rotation receipt/key history](../drizzle/0020_admin_key_rotation.sql). Keep the original owner-protection and session-revocation triggers in migration `0008`.
```sh
npm test -- admin-accounts admin-audit
@@ -42,9 +70,29 @@ confirmation. Independent security and persistence reviews resolved
unchanged-role audit no-ops, missing point-of-risk copy and a logout-all
timestamp rollback.
The rotation follow-up passes typecheck, lint and 5,787/5,787 offline
cases, the app build, Drizzle migration check and 55/55 HTTP tests against
the supported local development server. An isolated built-worker D1 store
with its own disposable owner/target completed authenticated **network**
provision, rotation, exact replay, disable, fresh-key re-enable, old
key/session rejection, A→B→A refusal, safe audit and foreign-origin
denial. No production account, room or credential was changed.
An actual phone-width owner page exposed the enabled-target review and
confirmation with no key before submission; a separate local-only
completed-record fixture showed the superseded-key warning without
sending a mutation. Independent security review found the historical
replay presentation risk, now corrected by last-read directory
comparison; persistence review reported no scoped finding.
The full 55-case suite against `wrangler dev` of the production build did
**not** pass: the local workerd process intermittently returned 503
mid-request under that suite. A single oversized request returned 413
and later requests succeeded after draining its body, but that does not
resolve the broader worker-restart observation. Development-server
success and focused built-worker mutations are not deployed acceptance.
Use an isolated owner and disposable target for authenticated end-to-end
account changes; never use a person's normal owner key in a destructive QA
flow. Client/browser fixtures alone do not prove a signed-in **network**
HTTP mutation or production deployment. Key rotation, re-enabling a
disabled account, failed-attempt audit, complete administration and live
acceptance remain unfinished.
HTTP mutation or production deployment. Failed-attempt audit, complete
administration and live acceptance remain unfinished.