Skip to content

feat(auth): [app] FirstAdmin = local, first administrator created on the machine; -ru reports failed writes - #6

Merged
inkly merged 3 commits into
ReCasaOS:mainfrom
ppenguin:feat/first-admin-local
Oct 24, 2026
Merged

inkly merged 3 commits into
ReCasaOS:mainfrom
ppenguin:feat/first-admin-local

Conversation

@ppenguin

Copy link
Copy Markdown
Contributor

Closes ReCasaOS/CasaOS#45. Includes the follow-up asked on #4.

Commit
feat(auth): first administrator created on the machine (opt-in) [app] FirstAdmin = local (default web: unchanged)
fix(cli): -bootstrap-admin refuses before reading; -ru reports a write that failed #45 ask + #4 follow-up

FirstAdmin = local

  • GET /v1/users/status: no register key, first_admin: local (new field, web|local)
  • POST /v1/users/register: 403, also with a key handed out before the switch
  • casaos-user-service -bootstrap-admin: creates the first administrator, then exits
    • rules of the web registration: argon2id, ≥ 6 characters, role admin
    • credentials: $CREDENTIALS_DIRECTORY/{username,password} (systemd LoadCredential=) or two lines on stdin; never argv/env
    • an account exists → exit 1 before any credential is read

#4 follow-up

  • UpdateUserPassword, UpdateUserTOTP return their error
    • -ru: password not stored → exit 1, nothing printed as new password; 2FA not cleared → exit 1, says so
    • PUT /v1/users/current/password: 500 when the write failed (was 200)
  • DB DSN _pragma=busy_timeout(5000): -ru / -bootstrap-admin beside the running service wait instead of SQLITE_BUSY
  • nits: accounts inlined, CLI errors on stderr

Tests

  • local refuses the web (status, register incl. earlier key); CreateFirstAdmin once; credentials from stdin and systemd
  • bootstrap: refuses without reading when an account exists; creates
  • writes report errors (closed DB); busy_timeout = 5000
  • go test -race ./... green

🤖 Generated with Claude Code

ppenguin and others added 2 commits October 24, 2026 16:23
- was: an empty box hands a register key to anyone asking `GET /v1/users/status` → first visitor on the LAN becomes administrator
- `[app] FirstAdmin = local` (default `web`: unchanged):
  - `GET /v1/users/status`: no key, `first_admin: local`
  - `POST /v1/users/register`: 403
  - `casaos-user-service -bootstrap-admin`: creates the first administrator once (same rules as the web registration: argon2id, ≥ 6 characters), then exits
  - credentials from `$CREDENTIALS_DIRECTORY/{username,password}` (systemd `LoadCredential=`) or two lines on stdin; never arguments or environment
- `first_admin` (`web`|`local`) in the status for the dashboard
- tests: local refuses the web (status, register incl. an earlier key); bootstrap once; credentials from stdin and from systemd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e that failed

- `-bootstrap-admin` (`v1.BootstrapFirstAdmin`): an account exists → ErrAlreadyInitialised,
  exit 1, before any credential is read
- `UpdateUserPassword`, `UpdateUserTOTP` return their error
  - `-ru`: password not stored → exit 1, no password printed; 2FA not cleared → exit 1, says so
  - `PUT /v1/users/current/password`: 500 when the write failed (was: 200)
- DB `busy_timeout(5000)`: `-ru` / `-bootstrap-admin` beside the running service wait for its
  write instead of SQLITE_BUSY
- CLI errors on stderr; `accounts` closure inlined
- tests: bootstrap refuses without reading, bootstrap creates; writes report errors; busy_timeout

Follow-up of ReCasaOS#4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@inkly inkly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this matches #45 closely: the register path is refused before the key is looked at, bootstrap refuses before reading anything, and the -ru writes now report failures. Two small requests before merge:

  1. TestFirstAdminLocalRefusesTheWeb panics when run on its own (-run FirstAdmin): GetUserStatus logs the GPU lookup error through an uninitialised logger, and it only passes because event_listen_test sets the logger up first. Could you call logger.LogInitConsoleOnly() in the test (or in newRig)?
  2. In -ru, if the 2FA write fails after the password write succeeded, the new password is never printed, so the account is left with a password nobody knows. Could both go in one UPDATE, or could that branch print the password and say to run -ru again?

Optional: warn on unknown FirstAdmin values (a typo silently means web), refuse a terminal on stdin (the password is echoed), and close the DB in busy_test (cleanup fails on Windows).

We'll squash-merge with a plain commit message, so nothing to change in the commit trailers.

… and terminal guards

- `ResetUserCredentials`: password + the four 2FA columns in one UPDATE → never a new password
  nobody was told while 2FA stays on
- test rig: `logger.LogInitConsoleOnly()` (a test run on its own panicked in GetUserStatus)
- `[app] FirstAdmin` neither web nor local → warning at start (taken as web)
- `-bootstrap-admin` from a terminal without systemd credentials → refused (the password would echo);
  pipes and /dev/null are read
- busy_test closes the DB (cleanup on Windows)
- tests: reset writes both, reset error on a closed DB, FirstAdmin values, /dev/null is not a terminal
@ppenguin

Copy link
Copy Markdown
Contributor Author

Done in 2e84e1c:

  1. newRig: logger.LogInitConsoleOnly(); go test -run FirstAdmin ./route/ passes on its own
  2. -ru: ResetUserCredentials writes the password and the four 2FA columns in one UPDATE: both or neither

Optional, also done:

  • unknown [app] FirstAdmin → warning at start (taken as web)
  • -bootstrap-admin from a terminal without systemd credentials → refused (ErrTerminalInput); pipes and /dev/null are read (isatty)
  • busy_test closes the DB

@inkly inkly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all verified: one UPDATE for -ru (argon2id hash stored, 2FA fully cleared), a warning on an unknown FirstAdmin, a terminal refused while pipes and /dev/null are read, the rig logger, and the DB closed in busy_test. It also merges cleanly with #7. Optional nit: UpdateUserTOTP has no caller left outside the tests and could go. Approving. We'll squash-merge with a plain commit message.

@inkly
inkly merged commit ec70506 into ReCasaOS:main Oct 24, 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.

Proposal: first administrator created on the machine (UserService [app] FirstAdmin = local)

2 participants