Record administrator failed attempts without storing credentials
Rejected administrator authentication and authorization attempts are now recorded durably in a new additive admin_attempts table with a fixed reason (invalid_key_format, unknown_or_disabled_key, missing_session, unknown_or_expired_session, role_denied), the attempt kind, the attempted role and an already-known account id. No key, key hash, session token, session hash, request body, origin, address or user agent is stored, and the unknown/disabled distinction is deliberately collapsed so the lookup cannot become an account-enumeration oracle. GET /api/admin/operations?attempts=1 returns seven-day per-reason counters plus the twenty most recent attempts, newest first, to a live owner only, with no-store and the same query guard as the other reads. The operations page adds "List failed attempts" with the counters and the bounded sample, truncating each account id to eight characters. Recording is best-effort inside the rejection path, so a rejected request keeps its own answer even when the table is unavailable, and the history never locks out, disables or revokes anything by itself. Evidence: tests/admin-attempts.test.ts asserts every recorded reason, the kind/role/account fields, that successful sign-ins and privileged reads record nothing, that no credential text appears anywhere in the page, the reporting window, the twenty-row bound and the owner-only recheck after demotion, and that a dropped attempts table leaves the original rejection answer unchanged. The anonymous HTTP boundary denies ?attempts=1, and the panel was verified in the real browser against a genuine recorded rejection. The shared AdminError moved to db/admin-error.ts so the recorder and the access module no longer form a value-import cycle. Offline suite 7,801/7,801 with typecheck and lint clean. No game, seat, saved state or deployment changes.
This commit is contained in:
1 parent
3bebc68cd5
commit
8bea4c1a0c
15 files changed
+3090
-21
No files matched your search
@@ -0,0 +1,51 @@
|
||||
# Administrator failed-attempt history
|
||||
|
||||
Added 7 October 2026 as owner-only, read-only administration scope. It records
|
||||
rejected administrator authentication and authorization attempts in a durable,
|
||||
secret-free form so an operator can notice repeated failures without learning
|
||||
anything private about the person who made them.
|
||||
|
||||
## What is recorded
|
||||
|
||||
| Kind | Reason | When |
|
||||
| --- | --- | --- |
|
||||
| `login` | `invalid_key_format` | The presented key is not a string or does not match the printed key shape. |
|
||||
| `login` | `unknown_or_disabled_key` | No enabled account matches the key hash, or the credential changed between the read and the session write. |
|
||||
| `session` | `missing_session` | No session cookie, or a token that is not 64 hex characters. |
|
||||
| `session` | `unknown_or_expired_session` | The session is unknown, revoked, expired, generation-stale or belongs to a disabled account. |
|
||||
| `role` | `role_denied` | A live session whose role is not in the requested set. |
|
||||
|
||||
The unknown/disabled distinction is deliberately collapsed into one reason: the
|
||||
lookup cannot separate them without becoming an account-enumeration oracle.
|
||||
|
||||
Only the reason, the kind, the attempted role and an already-known account id
|
||||
are stored. **No key, key hash, session token, session hash, request body,
|
||||
origin, address or user agent is written.** A successful sign-in and a
|
||||
successful privileged request record nothing.
|
||||
|
||||
## Reading it
|
||||
|
||||
`GET /api/admin/operations?attempts=1` requires a live owner session, checks the
|
||||
`X-Dune-Admin-Id` header against that session, and answers `no-store`. Any other
|
||||
query combination is rejected with `400`, and an anonymous or non-owner request
|
||||
is rejected with `401`/`403` before any row is read. The same request without a
|
||||
query returns the aggregate sample, `?integrity=1` runs the manual SQLite check
|
||||
and `?stalled=1` samples stalled rooms.
|
||||
|
||||
The response carries the sample time, the seven-day reporting window, the total
|
||||
inside that window, per-reason counters and at most `ADMIN_ATTEMPT_LIMIT`
|
||||
(twenty) of the most recent attempts, newest first. The operations page renders
|
||||
both, truncating each account id to eight characters.
|
||||
|
||||
## Privacy and safety
|
||||
|
||||
Recording is **best-effort**: `recordAdminAttempt` swallows its own failure, so
|
||||
a rejected request keeps its original answer even when the attempts table is
|
||||
unavailable. The write happens inside the same rejection path that already
|
||||
decided the outcome, so it cannot turn a rejection into an approval, and the
|
||||
read path repeats live owner authority inside its own batch.
|
||||
|
||||
The history is diagnostic only. Nothing in it locks out an account, disables a
|
||||
key, revokes a session or repairs a room automatically; those stay explicit
|
||||
owner actions with their own audit. It also does not rate-limit or block
|
||||
requests — it reports them.
|
||||
+10
-2
@@ -9,13 +9,13 @@ recovery and persistence systems where suitable.
|
||||
|
||||
| Area | Required behavior | Stage |
|
||||
| --- | --- | --- |
|
||||
| Administrator access | Personal access keys, server-enforced owner/operator/viewer roles, persistent eight-hour sessions and individual/all-session sign-out. [Owner-only account management](ADMIN_ACCOUNTS.md) adds a paginated directory, browser-created one-time keys, role changes, disabling and fresh-key rotation/re-enable with session revocation, live authority and exact retries. A room host is not a site administrator. Failed-attempt history and further operational permissions remain. | Prototyped, partial |
|
||||
| Administrator access | Personal access keys, server-enforced owner/operator/viewer roles, persistent eight-hour sessions and individual/all-session sign-out. [Owner-only account management](ADMIN_ACCOUNTS.md) adds a paginated directory, browser-created one-time keys, role changes, disabling and fresh-key rotation/re-enable with session revocation, live authority and exact retries. A room host is not a site administrator. [Failed-attempt history](ADMIN_ATTEMPTS.md) records rejected sign-ins and role denials without storing any credential; further operational permissions remain. | Prototyped, partial |
|
||||
| Room directory | Search room codes/public player names, filter status/rules/availability, sort and paginate; show host, roster, modules, game change time, pause/join flags and bounded decision ownership. Detailed setup/shared-window ownership remains. | Prototyped |
|
||||
| Create and configure | [Create a lobby](ADMIN_ROOM_CREATION.md) with an explicitly owned new host seat, Basic/Advanced preview and initial AI configuration; invite humans and use ordinary lobby controls/voluntary host-seat handover. [Neutral lobby configuration](ADMIN_LOBBY_CONFIGURATION.md) adds rules/modules, AI seats and host assignment to an existing human without private access. Reserved human seats remain. | Prototyped, partial |
|
||||
| Lifecycle and removal | [Pause/resume and joining locks](ADMIN_ROOM_CONTROLS.md), [administrative close/reopen](ADMIN_ROOM_CLOSURE.md) and [recoverable removal/restoration](ADMIN_ROOM_REMOVAL.md) have controls, durable audit, exact retries and player/AI enforcement. [Archive/unarchive](ADMIN_ROOM_ARCHIVE.md) has separate directory filters and exact retries. Permanent deletion and bulk actions remain missing. | Prototyped, partial |
|
||||
| Participant support | [Participant AI](ADMIN_PARTICIPANT_AI.md) enables an existing difficulty on an eligible human seat in a paused game, retaining access/takeback and private custody. [Discussion controls](ADMIN_DISCUSSION.md) mute/unmute new sends while retaining history and gameplay. Participant removal, access revocation, assisted recovery and further replacement remain. Existing lobby controls can reassign the host. | Prototyped, partial |
|
||||
| Saved-game operations | [Owner-only room backups](ADMIN_BACKUPS.md) capture/list/download a size-limited, version-fenced room snapshot without changing play or access; import validation, safe checkpoint restore and interrupted-work diagnosis/resume remain missing. | Prototyped, partial |
|
||||
| Operations and audit | [Action history](ADMIN_ACTION_HISTORY.md) searches existing durable operations with safe settings and role-limited reasons. The owner-only [operations page](../app/admin/operations/page.tsx) samples build revision, room/seat flags, JSON syntax failures and backup/export counts without exposing game contents. [Stalled-decision diagnosis](ADMIN_STALLED_DECISIONS.md) adds a bounded, read-only sample of live rooms whose public pending interaction has been idle longest, with aggregate counters. An explicit owner action runs read-only SQLite quick/foreign-key checks and returns only pass/fail, never corrupt row details. Failed-attempt records, disk capacity and maintenance controls remain. | Prototyped, partial |
|
||||
| Operations and audit | [Action history](ADMIN_ACTION_HISTORY.md) searches existing durable operations with safe settings and role-limited reasons. The owner-only [operations page](../app/admin/operations/page.tsx) samples build revision, room/seat flags, JSON syntax failures and backup/export counts without exposing game contents. [Stalled-decision diagnosis](ADMIN_STALLED_DECISIONS.md) adds a bounded, read-only sample of live rooms whose public pending interaction has been idle longest, with aggregate counters. An explicit owner action runs read-only SQLite quick/foreign-key checks and returns only pass/fail, never corrupt row details. [Failed-attempt history](ADMIN_ATTEMPTS.md) records rejected authentication and authorization attempts with a fixed reason, the attempted role and an already-known account id — never a key, token, body or address — and exposes seven-day counters plus a bounded recent sample to the owner. Disk capacity and maintenance controls remain. | Prototyped, partial |
|
||||
|
||||
Track each area through Missing, Prototyped, Integrated, Verified and Polished.
|
||||
As implementation begins, link its controls, server actions, persistence,
|
||||
@@ -84,6 +84,14 @@ The check can take time; a failed check calls for a protected snapshot and
|
||||
private diagnosis, not automatic repair. It does not prove disk capacity,
|
||||
backup recoverability or valid game rules.
|
||||
|
||||
[Failed-attempt history](ADMIN_ATTEMPTS.md) adds **List failed attempts**: seven-day
|
||||
per-reason counters and the twenty most recent rejected administrator requests —
|
||||
malformed, unknown or disabled keys, missing, unknown, expired or revoked
|
||||
sessions, and role-denied actions. Recording is best-effort inside the rejection
|
||||
path and stores no credential; the read repeats live owner authority and
|
||||
answers `no-store`. The history is diagnostic: it never locks out, disables or
|
||||
revokes anything by itself.
|
||||
|
||||
[Stalled-decision diagnosis](ADMIN_STALLED_DECISIONS.md) adds **List stalled
|
||||
decisions**: a bounded, read-only sample of at most ten live rooms whose public
|
||||
pending interaction — a decision, response, Truthtrance or phase opening — has
|
||||
|
||||
+10
-1
@@ -74,6 +74,15 @@ eligibility and Tleilaxu income repeat scope stay gated exactly as recorded in
|
||||
[TLEILAXU_AMBASSADOR_RULES.md](TLEILAXU_AMBASSADOR_RULES.md#implementation-decision-boundary);
|
||||
no ordinary revival allowance, price, phase or income path changed.
|
||||
|
||||
**In progress — administrator failed-attempt history.** Rejected administrator
|
||||
authentication and authorization attempts are now recorded durably with a fixed
|
||||
reason, the attempted role and an already-known account id — never a key, token,
|
||||
body or address — and the owner-only operations page lists seven-day per-reason
|
||||
counters plus the twenty most recent entries. Recording is best-effort inside
|
||||
the rejection path, so a rejected request keeps its own answer, and the history
|
||||
never locks out, disables or revokes anything by itself. See
|
||||
[ADMIN_ATTEMPTS.md](ADMIN_ATTEMPTS.md).
|
||||
|
||||
**In progress — administrator operations.** The owner-only operations page now
|
||||
also diagnoses [stalled decisions](ADMIN_STALLED_DECISIONS.md): a bounded,
|
||||
read-only sample of at most ten live rooms whose public pending interaction — a
|
||||
@@ -82,7 +91,7 @@ decision, response, Truthtrance or phase opening — has been idle longest, plus
|
||||
Paused, closed, removed and archived rooms are excluded and no private game
|
||||
content is selected; `GET /api/admin/operations?stalled=1` stays owner-only,
|
||||
`no-store` and read-only, and it repairs nothing. Failed-attempt records, disk
|
||||
capacity and maintenance controls remain.
|
||||
Disk capacity and maintenance controls remain.
|
||||
|
||||
|
||||
**Mixed E1/E2 and paired E3 Nexus — bounded Development Verified.**
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 7 October 2026 — administrator failed-attempt history
|
||||
|
||||
Rejected administrator authentication and authorization attempts are now
|
||||
recorded durably in a new additive `admin_attempts` table with a fixed reason
|
||||
(`invalid_key_format`, `unknown_or_disabled_key`, `missing_session`,
|
||||
`unknown_or_expired_session`, `role_denied`), the attempt kind, the attempted
|
||||
role and an already-known account id. No key, key hash, session token, session
|
||||
hash, request body, origin, address or user agent is stored, and the
|
||||
unknown/disabled distinction is deliberately collapsed so the lookup cannot
|
||||
become an account-enumeration oracle.
|
||||
|
||||
`GET /api/admin/operations?attempts=1` returns seven-day per-reason counters
|
||||
plus the twenty most recent attempts, newest first, to a live owner only, with
|
||||
`no-store` and the same query guard as the other reads. The operations page adds
|
||||
**List failed attempts** with the counters and the bounded sample, truncating
|
||||
each account id to eight characters. Recording is best-effort inside the
|
||||
rejection path, so a rejected request keeps its own answer even when the table
|
||||
is unavailable; the history never locks out, disables or revokes anything by
|
||||
itself.
|
||||
|
||||
Evidence: `tests/admin-attempts.test.ts` asserts every recorded reason, the
|
||||
kind/role/account fields, that successful sign-ins and privileged reads record
|
||||
nothing, that no credential text appears anywhere in the page, the reporting
|
||||
window, the twenty-row bound and owner-only recheck after demotion, and that a
|
||||
dropped attempts table leaves the original rejection answer unchanged. The
|
||||
anonymous HTTP boundary also denies `?attempts=1`. The shared `AdminError` moved
|
||||
to `db/admin-error.ts` so the recorder and the access module no longer form a
|
||||
value-import cycle. No game, seat, saved state or deployment changes.
|
||||
|
||||
## 7 October 2026 — Sneak Attack composes the Bene Gesserit arrival reactions
|
||||
|
||||
A positive Moritani Sneak Attack now resolves its committed Terror entry first
|
||||
|
||||
Reference in new issue
Block a user