Skip to content

feat(admin): the user actions behind the admin gate - #13

Merged
raymondproguy merged 2 commits into
mainfrom
feat/console-admin-endpoints
Oct 12, 2026
Merged

raymondproguy merged 2 commits into
mainfrom
feat/console-admin-endpoints

Conversation

@raymondproguy

Copy link
Copy Markdown
Contributor

The console's Users screen needs the account actions the CLI reaches the database for directly (users unlock, oauth unlink): an operator who cannot reach the database has no path at all, so the read-only position in user_handlers.go gives way. Still absent on purpose: no password reset, no delete, nothing that reads PasswordHash.

Six endpoints, all behind AdminOnly, each audited with the operator's id in metadata and answering the account's state after the action:

POST /v1/admin/users (cryden.SignUp)
POST /v1/admin/users/{id}/unlock (ResetFailedAttempts)
POST /v1/admin/users/{userID}/suspend (LockAccount + revoke-all)
POST /v1/admin/users/{userID}/unsuspend (same op as unlock,
its own audit type)
POST /v1/admin/users/{userID}/revoke-sessions (RevokeAllForUser)
POST /v1/admin/users/{userID}/mfa/disable (totp.Delete + revoke)
DELETE /v1/admin/users/{userID}/oauth/{provider} (ListByUser + Unlink)

Suspend is LockAccount with a far-future deadline, not a new engine mechanism: a suspended sign-in is refused by the engine's own lockout path. MFA disable deletes the enrolment through the TOTP store rather than cryden.DisableTOTP, which requires the holder's password an operator does not have; the enrolment's removal revokes the account's sessions so access held through the factor does not outlive it. Unlock and unsuspend are one engine operation with two names, kept apart for the audit trail: lifting an operator-imposed suspension and clearing an engine-imposed lockout are different sentences in the history.

Admin-created accounts go through cryden.SignUp, so the password policy, breach check and rate limiter all run as they would at POST /v1/signup; the audit records both the engine's signup event and the operator's admin_user_created.

Every action answers AdminActionDTO: the account as it stands, so a console re-renders without a second GET, with detail carrying what is worth reporting (the revoked session count, the unlinked provider).

Tested with a real engine on memory stores behind the real gate: create (sign-up through the policy, duplicate 409), unlock clearing lockout and counter and the proof being a working login, suspend revoking live sessions and blocking the next login, unsuspend restoring sign-in, revoke-sessions emptying the live list, TOTP removal through the engine's own enrolment path and its 404 without one, OAuth unlink and its unknown-provider 404, malformed ids answering 404, and every action refusing a non-operator token. The Tier 6 SQLite 501 test extends to the new paths through its existing adminPaths table. openapi/spec.yaml documents each route and the AdminActionDTO schema; the embedded copy is regenerated.

The console's Users screen needs the account actions the CLI reaches
the database for directly (users unlock, oauth unlink): an operator
who cannot reach the database has no path at all, so the read-only
position in user_handlers.go gives way. Still absent on purpose: no
password reset, no delete, nothing that reads PasswordHash.

Six endpoints, all behind AdminOnly, each audited with the operator's
id in metadata and answering the account's state after the action:

  POST   /v1/admin/users                              (cryden.SignUp)
  POST /v1/admin/users/{id}/unlock                    (ResetFailedAttempts)
  POST /v1/admin/users/{userID}/suspend               (LockAccount + revoke-all)
  POST /v1/admin/users/{userID}/unsuspend             (same op as unlock,
                                                       its own audit type)
  POST /v1/admin/users/{userID}/revoke-sessions       (RevokeAllForUser)
  POST /v1/admin/users/{userID}/mfa/disable           (totp.Delete + revoke)
  DELETE /v1/admin/users/{userID}/oauth/{provider}    (ListByUser + Unlink)

Suspend is LockAccount with a far-future deadline, not a new engine
mechanism: a suspended sign-in is refused by the engine's own lockout
path. MFA disable deletes the enrolment through the TOTP store rather
than cryden.DisableTOTP, which requires the holder's password an
operator does not have; the enrolment's removal revokes the account's
sessions so access held through the factor does not outlive it. Unlock
and unsuspend are one engine operation with two names, kept apart for
the audit trail: lifting an operator-imposed suspension and clearing an
engine-imposed lockout are different sentences in the history.

Admin-created accounts go through cryden.SignUp, so the password
policy, breach check and rate limiter all run as they would at
POST /v1/signup; the audit records both the engine's signup event and
the operator's admin_user_created.

Every action answers AdminActionDTO: the account as it stands, so a
console re-renders without a second GET, with detail carrying what is
worth reporting (the revoked session count, the unlinked provider).

Tested with a real engine on memory stores behind the real gate: create
(sign-up through the policy, duplicate 409), unlock clearing lockout
and counter and the proof being a working login, suspend revoking live
sessions and blocking the next login, unsuspend restoring sign-in,
revoke-sessions emptying the live list, TOTP removal through the
engine's own enrolment path and its 404 without one, OAuth unlink and
its unknown-provider 404, malformed ids answering 404, and every
action refusing a non-operator token. The Tier 6 SQLite 501 test
extends to the new paths through its existing adminPaths table.
openapi/spec.yaml documents each route and the AdminActionDTO schema;
the embedded copy is regenerated.
@raymondproguy
raymondproguy merged commit 59f6f63 into main Oct 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant