First-run setup for local mode (#51) #56

Merged
Cordy merged 5 commits from feat/first-run-setup into main 2026-07-29 04:50:24 +00:00
Owner

Bootstrap admin #1 for deployments without an IdP.

Flow: in local mode with no configured auth.users and an empty local-users store, cairnd generates a one-time token, prints it to the logs, and serves a branded /setup page. The page creates the first admin (username + password ≥10 chars), which is persisted to auth.localUsersPath (default /data/.cairn/local-users.json), flagged admin, and logged straight in via a cookie session. Setup closes permanently once the first user exists.

Security: the token is never exposed over HTTP (GET /setup/status returns only {pending}), so "whoever races first" can't claim admin on a network-reachable instance; constant-time token compare; POST /setup returns 409 once done.

Pieces:

  • internal/auth/localstore.go — atomic-JSON store of runtime-created users.
  • internal/auth/setup.go — the flow + endpoints; issues a session on success.
  • Local provider now merges store users with config users and sets User.Admin for store admins (so the bootstrap admin works over WebDAV/Basic too, not just the cookie).
  • Admin surfaces (License / Admin API / Settings) are registered in local mode so the bootstrap admin gets them even with no static auth.admins.
  • web/static/setup.html; config auth.localUsersPath.

OIDC/LDAP deployments use auth.adminGroups instead and are untouched (localUsers stays nil). New setup_test.go covers the store + full flow (pending → wrong token → bad input → success → closed → conflict). gofmt/vet/build/test + node --check green on the runner.

Bootstrap admin #1 for deployments without an IdP. **Flow**: in local mode with no configured `auth.users` and an empty local-users store, cairnd generates a one-time token, prints it to the logs, and serves a branded `/setup` page. The page creates the first admin (username + password ≥10 chars), which is persisted to `auth.localUsersPath` (default `/data/.cairn/local-users.json`), flagged admin, and logged straight in via a cookie session. Setup closes permanently once the first user exists. **Security**: the token is never exposed over HTTP (`GET /setup/status` returns only `{pending}`), so "whoever races first" can't claim admin on a network-reachable instance; constant-time token compare; `POST /setup` returns 409 once done. **Pieces**: - `internal/auth/localstore.go` — atomic-JSON store of runtime-created users. - `internal/auth/setup.go` — the flow + endpoints; issues a session on success. - `Local` provider now merges store users with config users and sets `User.Admin` for store admins (so the bootstrap admin works over WebDAV/Basic too, not just the cookie). - Admin surfaces (License / Admin API / Settings) are registered in local mode so the bootstrap admin gets them even with no static `auth.admins`. - `web/static/setup.html`; config `auth.localUsersPath`. OIDC/LDAP deployments use `auth.adminGroups` instead and are untouched (`localUsers` stays nil). New `setup_test.go` covers the store + full flow (pending → wrong token → bad input → success → closed → conflict). gofmt/vet/build/test + `node --check` green on the runner.
Cordy merged commit 1bac78d02e into main 2026-07-29 04:50:24 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#56
No description provided.