app-owned Admins and Users for non idP provided instances #533

Open
opened 2026-09-14 00:37:22 +00:00 by Cordy · 9 comments
Owner

— after talking with Manuel, we should have a Local Cairn admin available on deploy. On launch and at the setup wizard, ask the user to configure a local Cairn admin which is stored there. If no idP is provided during setup or conf file or environment (don’t remember anymore where we set this) then an additional tab should render under the admins dashboard called “Users & Groups” where local user can be managed up to 50 users total. There Users and groups and admins can be defined locally.
here is the conversation i had with Manuel about it:

[9/13/26, 7:39:03 PM] Manuel Novak: Aber AD, Keycloak, local User (inkl. Admin) müssen halt sitzen zu 100%
[9/13/26, 7:39:45 PM] Manuel Novak: Und eine Mischung aus lokalem Admin und wahlweise externen Diensten (AD/Keycloak)
[9/13/26, 7:39:52 PM] You: true, ich weis ich bin über das mit local user kurz drüber geballert. aber das war n kurzer run. and denke auch nur admin relevant.
[9/13/26, 7:40:15 PM] You: wollen wir app-owner user managment within admin dashboard of cairn?
[9/13/26, 7:40:27 PM] You: das ziel ist ja eigentlich keine app-owned user zu haben?
[9/13/26, 7:41:08 PM] Manuel Novak: Nur die lokale Liste mit Admins und unserem 25 Userlimit
[9/13/26, 7:41:22 PM] Manuel Novak: Mach einfach ne harte Grenze mit lokalen Nutzern
[9/13/26, 7:41:32 PM] You: 25 userlimit?
[9/13/26, 7:41:49 PM] You: ah damn dann muss ich aber vlt schauen ob ich das in ner json haben will oder lieber ne kleine db :')
[9/13/26, 7:42:19 PM] You: hätte gesagt, 1 admin user can be inside, that's that. keycloak ist ja auch n schmaller footprint
[9/13/26, 7:42:21 PM] You: +
[9/13/26, 7:42:28 PM] Manuel Novak: JSON lokal bis total 35User (10x Admins + 25 free User) is noch ok
[9/13/26, 7:42:41 PM] Manuel Novak: Nah ein Admin reicht nicht
[9/13/26, 7:42:47 PM] You: ich denke grad, wegen der PQ keys, will man die in der json habe :')
[9/13/26, 7:43:24 PM] Manuel Novak: Ist deren Problem dann und es muss nur dokumentiert sein, dass man lieber externe Dienste nutzen sollte
[9/13/26, 7:43:42 PM] Manuel Novak: Weil eben die JSON Plaintext is
[9/13/26, 7:44:47 PM] Manuel Novak: Im normalen Betrieb wird die JSON 1-3 Admins für den Notfall drin haben und alle anderen Admins sind im AD/Keycloak
[9/13/26, 7:45:37 PM] You: hm ja, gut, doch kann ich noch reinlunzen, aber dann entweder später heute als Phone UI. entweder heute später in der nacht oder morgen kommt der freeze
[9/13/26, 7:46:43 PM] Manuel Novak: Keine Hektik. Es muss sauber funktionieren
[9/13/26, 7:47:14 PM] You: yesss, also immernoch beta hier :D cute v0.8 erst für fully ready for release und last code freeze
[9/13/26, 7:47:29 PM] Manuel Novak: Wenn ein MA das Ding das erste Mal deployed und bei der Authentifizierung Probleme hat, wirkt das schlecht
[9/13/26, 7:47:38 PM] You: valid
[9/13/26, 7:48:13 PM] Manuel Novak: Ich hab mir Mokapi für AD mal angeschaut. Ich lass eine Instanz dagegen mal authen
[9/13/26, 7:48:20 PM] You: ich lasse es halt save noch in der v0.7 milestone, da code-review noch geballert wird, vulnerability hunting, und so weiter.
[9/13/26, 7:49:07 PM] You: so mit allem zusammen denke ich nicht das da v0.7 milestone geclosed wird in unter 2-3 wochen
[9/13/26, 7:50:45 PM] You: es flashed mich grad irwie weil ich irgendwie nichtmal selber erwartet habe heute "fertig" zu werden
[9/13/26, 7:51:11 PM] You: hab einfach geballert und bemerkt "oh ich hab nurnoch den phoneUI pass übrig, damn"
[9/13/26, 7:51:42 PM] Manuel Novak: Feinschliff is immer und wir müssen den kompletten UseCase von einem 0815 MA durchspielen und optimieren von:

  • Erster Kontakt Produkt
  • Preise
  • Installation
  • Features
  • Peering
  • Maintenance
  • Upgrades
    [9/13/26, 7:51:50 PM] You: ganz kurz.
    [9/13/26, 7:52:27 PM] You: die local users, + local admins. sollen die nur available sein wenn kein idP vorhanden ist? im sinne von Cairn was not served an idP
    [9/13/26, 7:52:45 PM] You: renders "Users and groups" tab aufm admin dashboard dann aber auch nur dann
    [9/13/26, 7:52:50 PM] Manuel Novak: Nah lokale Admins muss es IMMER geben können
    [9/13/26, 7:53:03 PM] You: admins werden konfiguriert on deployment.
    [9/13/26, 7:53:09 PM] Manuel Novak: Yes
    [9/13/26, 7:53:14 PM] You: initial launch will ask to set a password on webUI
    [9/13/26, 7:53:26 PM] Manuel Novak: Tönt gut
    [9/13/26, 7:53:27 PM] You: und da wollen wir 1-3 oder nur 1?
    [9/13/26, 7:54:04 PM] Manuel Novak: 1-3 aber die anderen 2x kann der initial Admin setzen ohne Wizard beim Deployment
    [9/13/26, 7:54:51 PM] Manuel Novak: Wenn es dann um AD/KC User geht --> die müssen auch promoted/demoted werden können
    [9/13/26, 7:55:06 PM] Manuel Novak: Die lokalen Admins sind wie gesagt nur Notfall Logins
    [9/13/26, 7:55:50 PM] Manuel Novak: Zusätzlich arbeiten die meisten MA mit ihrem Admin-AD Account der auch in der Admin Cairngruppe sein muss aber nicht lokal ist
    [9/13/26, 7:56:51 PM] Manuel Novak: Mit den lokalen Admin Accounts wird nur initial alles eingerichtet und gefixt im Fehlerfall aber DailyBusiness wird damit nicht gemacht
    [9/13/26, 7:58:04 PM] Manuel Novak: Zumindest ist es im Banken/Versicherung/kantonalen/militärischen Umfeld verboten nicht-zentral verwaltete Accounts mit erhöhten Rechten für alltägliche Aufgaben zu nutzen
— after talking with Manuel, we should have a Local Cairn admin available on deploy. On launch and at the setup wizard, ask the user to configure a local Cairn admin which is stored there. If no idP is provided during setup or conf file or environment (don’t remember anymore where we set this) then an additional tab should render under the admins dashboard called “Users & Groups” where local user can be managed up to 50 users total. There Users and groups and admins can be defined locally. here is the conversation i had with Manuel about it: [9/13/26, 7:39:03 PM] Manuel Novak: Aber AD, Keycloak, local User (inkl. Admin) müssen halt sitzen zu 100% [9/13/26, 7:39:45 PM] Manuel Novak: Und eine Mischung aus lokalem Admin und wahlweise externen Diensten (AD/Keycloak) [9/13/26, 7:39:52 PM] You: true, ich weis ich bin über das mit local user kurz drüber geballert. aber das war n kurzer run. and denke auch nur admin relevant. [9/13/26, 7:40:15 PM] You: wollen wir app-owner user managment within admin dashboard of cairn? [9/13/26, 7:40:27 PM] You: das ziel ist ja eigentlich keine app-owned user zu haben? [9/13/26, 7:41:08 PM] Manuel Novak: Nur die lokale Liste mit Admins und unserem 25 Userlimit [9/13/26, 7:41:22 PM] Manuel Novak: Mach einfach ne harte Grenze mit lokalen Nutzern [9/13/26, 7:41:32 PM] You: 25 userlimit? [9/13/26, 7:41:49 PM] You: ah damn dann muss ich aber vlt schauen ob ich das in ner json haben will oder lieber ne kleine db :') [9/13/26, 7:42:19 PM] You: hätte gesagt, 1 admin user can be inside, that's that. keycloak ist ja auch n schmaller footprint [9/13/26, 7:42:21 PM] You: + [9/13/26, 7:42:28 PM] Manuel Novak: JSON lokal bis total 35User (10x Admins + 25 free User) is noch ok [9/13/26, 7:42:41 PM] Manuel Novak: Nah ein Admin reicht nicht [9/13/26, 7:42:47 PM] You: ich denke grad, wegen der PQ keys, will man die in der json habe :') [9/13/26, 7:43:24 PM] Manuel Novak: Ist deren Problem dann und es muss nur dokumentiert sein, dass man lieber externe Dienste nutzen sollte [9/13/26, 7:43:42 PM] Manuel Novak: Weil eben die JSON Plaintext is [9/13/26, 7:44:47 PM] Manuel Novak: Im normalen Betrieb wird die JSON 1-3 Admins für den Notfall drin haben und alle anderen Admins sind im AD/Keycloak [9/13/26, 7:45:37 PM] You: hm ja, gut, doch kann ich noch reinlunzen, aber dann entweder später heute als Phone UI. entweder heute später in der nacht oder morgen kommt der freeze [9/13/26, 7:46:43 PM] Manuel Novak: Keine Hektik. Es muss sauber funktionieren [9/13/26, 7:47:14 PM] You: yesss, also immernoch beta hier :D cute v0.8 erst für fully ready for release und last code freeze [9/13/26, 7:47:29 PM] Manuel Novak: Wenn ein MA das Ding das erste Mal deployed und bei der Authentifizierung Probleme hat, wirkt das schlecht [9/13/26, 7:47:38 PM] You: valid [9/13/26, 7:48:13 PM] Manuel Novak: Ich hab mir Mokapi für AD mal angeschaut. Ich lass eine Instanz dagegen mal authen [9/13/26, 7:48:20 PM] You: ich lasse es halt save noch in der v0.7 milestone, da code-review noch geballert wird, vulnerability hunting, und so weiter. [9/13/26, 7:49:07 PM] You: so mit allem zusammen denke ich nicht das da v0.7 milestone geclosed wird in unter 2-3 wochen [9/13/26, 7:50:45 PM] You: es flashed mich grad irwie weil ich irgendwie nichtmal selber erwartet habe heute "fertig" zu werden [9/13/26, 7:51:11 PM] You: hab einfach geballert und bemerkt "oh ich hab nurnoch den phoneUI pass übrig, damn" [9/13/26, 7:51:42 PM] Manuel Novak: Feinschliff is immer und wir müssen den kompletten UseCase von einem 0815 MA durchspielen und optimieren von: - Erster Kontakt Produkt - Preise - Installation - Features - Peering - Maintenance - Upgrades [9/13/26, 7:51:50 PM] You: ganz kurz. [9/13/26, 7:52:27 PM] You: die local users, + local admins. sollen die nur available sein wenn kein idP vorhanden ist? im sinne von Cairn was not served an idP [9/13/26, 7:52:45 PM] You: renders "Users and groups" tab aufm admin dashboard dann aber auch nur dann [9/13/26, 7:52:50 PM] Manuel Novak: Nah lokale Admins muss es IMMER geben können [9/13/26, 7:53:03 PM] You: admins werden konfiguriert on deployment. [9/13/26, 7:53:09 PM] Manuel Novak: Yes [9/13/26, 7:53:14 PM] You: initial launch will ask to set a password on webUI [9/13/26, 7:53:26 PM] Manuel Novak: Tönt gut [9/13/26, 7:53:27 PM] You: und da wollen wir 1-3 oder nur 1? [9/13/26, 7:54:04 PM] Manuel Novak: 1-3 aber die anderen 2x kann der initial Admin setzen ohne Wizard beim Deployment [9/13/26, 7:54:51 PM] Manuel Novak: Wenn es dann um AD/KC User geht --> die müssen auch promoted/demoted werden können [9/13/26, 7:55:06 PM] Manuel Novak: Die lokalen Admins sind wie gesagt nur Notfall Logins [9/13/26, 7:55:50 PM] Manuel Novak: Zusätzlich arbeiten die meisten MA mit ihrem Admin-AD Account der auch in der Admin Cairngruppe sein muss aber nicht lokal ist [9/13/26, 7:56:51 PM] Manuel Novak: Mit den lokalen Admin Accounts wird nur initial alles eingerichtet und gefixt im Fehlerfall aber DailyBusiness wird damit nicht gemacht [9/13/26, 7:58:04 PM] Manuel Novak: Zumindest ist es im Banken/Versicherung/kantonalen/militärischen Umfeld verboten nicht-zentral verwaltete Accounts mit erhöhten Rechten für alltägliche Aufgaben zu nutzen
Author
Owner

Distilled the conversation with Manuel into requirements, checked against what already exists in the code.

Requirements as I read them:

  1. Local admins must ALWAYS be possible, in every auth mode — they are break-glass logins, not daily drivers ("mit den lokalen Admin Accounts wird nur initial alles eingerichtet und gefixt im Fehlerfall"). Normal operation: 1–3 local emergency admins, everything else in AD/Keycloak.
  2. First-run wizard asks the deployer to set ONE local admin password in the web UI; that admin can create up to 2 more local admins later from the dashboard (Manuel: "1-3 aber die anderen 2x kann der initial Admin setzen ohne Wizard").
  3. When no IdP is configured (auth mode "local"), the admin dashboard grows a Users & Groups topic: local users and groups managed there, with a hard cap.
  4. IdP users must be promotable/demotable to admin from the dashboard (admin set = local admins + IdP accounts in the admin group/list).
  5. JSON store is fine and documented as plaintext — docs steer real deployments to an external IdP ("es muss nur dokumentiert sein, dass man lieber externe Dienste nutzen sollte").

What already exists (more than we remembered): auth mode "local" is fully implemented (auth.NewLocal(users).WithStore(...), argon2id PHC via cairnd hash-password); LocalUsersPath defaults to /data/.cairn/local-users.json and already persists runtime-created users — the first-run setup admin from #51; CAIRN_ADMIN_* env-ensured local admin exists (#310); local-users.json is already in the #153 state-in-backend set, so it survives redeploys. The gaps: (a) in OIDC mode there is no local password login at all today (pwLogin is only wired when mode != "oidc") — requirement 1 needs a local-login path that coexists with the IdP redirect flow; (b) no Users & Groups admin UI, no runtime CRUD API, no caps; (c) no promote/demote surface (admins are a config allow-list); (d) local "groups" have no defined backing — presumably the existing spacestore (local spaces with write/read roles, IdP-less path) is the natural home rather than a second group system.

Two things I need from you before mockup/spec:

  • Cap conflict: Manuel's numbers are "10 admins + 25 users = 35 total"; the issue text says "up to 50 users total". Which is the hard limit?
  • Emergency local login UX in OIDC mode: a visible "Sign in with a local account" link on the login page, or a deliberately unadvertised route (e.g. /login?local) so end users never see it? Banking/military context in Manuel's messages suggests the quiet route, but that's a guess.

Also flagging for the spec: seat accounting (do local users consume licence seats the same as IdP users — presumably yes), per-user encryption custody for local users (keycloak-profile custody can't serve them; openbao and deployment custody can — worth a compatibility note), and the promote/demote audit trail (seat-release already writes audit entries; admin changes should too).

Distilled the conversation with Manuel into requirements, checked against what already exists in the code. **Requirements as I read them:** 1. **Local admins must ALWAYS be possible**, in every auth mode — they are break-glass logins, not daily drivers ("mit den lokalen Admin Accounts wird nur initial alles eingerichtet und gefixt im Fehlerfall"). Normal operation: 1–3 local emergency admins, everything else in AD/Keycloak. 2. **First-run wizard** asks the deployer to set ONE local admin password in the web UI; that admin can create up to 2 more local admins later from the dashboard (Manuel: "1-3 aber die anderen 2x kann der initial Admin setzen ohne Wizard"). 3. **When no IdP is configured** (auth mode "local"), the admin dashboard grows a **Users & Groups** topic: local users and groups managed there, with a hard cap. 4. **IdP users must be promotable/demotable to admin** from the dashboard (admin set = local admins + IdP accounts in the admin group/list). 5. **JSON store is fine and documented as plaintext** — docs steer real deployments to an external IdP ("es muss nur dokumentiert sein, dass man lieber externe Dienste nutzen sollte"). **What already exists** (more than we remembered): auth mode `"local"` is fully implemented (`auth.NewLocal(users).WithStore(...)`, argon2id PHC via `cairnd hash-password`); `LocalUsersPath` defaults to `/data/.cairn/local-users.json` and already persists *runtime-created* users — the first-run setup admin from #51; `CAIRN_ADMIN_*` env-ensured local admin exists (#310); `local-users.json` is already in the #153 state-in-backend set, so it survives redeploys. **The gaps:** (a) in OIDC mode there is no local password login at all today (`pwLogin` is only wired when mode != "oidc") — requirement 1 needs a local-login path that coexists with the IdP redirect flow; (b) no Users & Groups admin UI, no runtime CRUD API, no caps; (c) no promote/demote surface (admins are a config allow-list); (d) local "groups" have no defined backing — presumably the existing `spacestore` (local spaces with write/read roles, IdP-less path) is the natural home rather than a second group system. **Two things I need from you before mockup/spec:** - **Cap conflict:** Manuel's numbers are "10 admins + 25 users = 35 total"; the issue text says "up to 50 users total". Which is the hard limit? - **Emergency local login UX in OIDC mode:** a visible "Sign in with a local account" link on the login page, or a deliberately unadvertised route (e.g. `/login?local`) so end users never see it? Banking/military context in Manuel's messages suggests the quiet route, but that's a guess. Also flagging for the spec: seat accounting (do local users consume licence seats the same as IdP users — presumably yes), per-user encryption custody for local users (keycloak-profile custody can't serve them; openbao and deployment custody can — worth a compatibility note), and the promote/demote audit trail (seat-release already writes audit entries; admin changes should too).
Author
Owner

Decision (Nikola, 2026-09-14): the hard limit is 50 local users total (supersedes the 35 from the chat excerpt). Emergency-login UX question still open.

Decision (Nikola, 2026-09-14): the hard limit is **50 local users total** (supersedes the 35 from the chat excerpt). Emergency-login UX question still open.
Author
Owner

Decisions (Nikola, 2026-09-14):

  • Emergency login is a visible "Sign in with a local account" link on the login page, not a hidden route.
  • Minimum 1 admin is created at deployment through the existing first-run wizard (#51) — the deployer sees it and sets the password themself in the web UI.
  • Local user handling lives in the admin dashboard at #admin/users/access — either extending that topic or a new tab beside it: create users and admins, basic permissions, for the initial admin.
  • Explicit positioning, beyond break-glass: local accounts let evaluators test Cairn before integrating their IdP, and let home users run it permanently without any IdP — minimal-environment mode is a supported first-class path, not a degraded one.
  • Cap: 50 local users total (recorded earlier).

Next: competitor deep dive (how Nextcloud / OpenCloud / Seafile / Pydio and break-glass patterns elsewhere handle initial admin + local-login-beside-SSO + local user management), then a full user-flow mockup for review.

**Decisions (Nikola, 2026-09-14):** - Emergency login is a **visible "Sign in with a local account" link** on the login page, not a hidden route. - **Minimum 1 admin is created at deployment through the existing first-run wizard (#51)** — the deployer sees it and sets the password themself in the web UI. - Local user handling lives in the admin dashboard at **`#admin/users/access`** — either extending that topic or a new tab beside it: create users and admins, basic permissions, for the initial admin. - Explicit positioning, beyond break-glass: local accounts let evaluators **test Cairn before integrating their IdP**, and let **home users run it permanently without any IdP** — minimal-environment mode is a supported first-class path, not a degraded one. - Cap: 50 local users total (recorded earlier). Next: competitor deep dive (how Nextcloud / OpenCloud / Seafile / Pydio and break-glass patterns elsewhere handle initial admin + local-login-beside-SSO + local user management), then a full user-flow mockup for review.
Author
Owner

Competitor research + code recon — the design basis for the mockup.

How the field handles it:

Initial admin at deploy. Three patterns exist: env-var before first start (oCIS INITIAL_ADMIN_PASSWORD — set-once, changes silently ignored later, a documented footgun; Grafana GF_SECURITY_ADMIN_*); default credentials with forced change (Grafana admin/admin — universally criticised); and a browser wizard on first run (Portainer, Nextcloud installer). Portainer's wizard has the unclaimed-instance problem — anyone who reaches the fresh instance first becomes admin — which it patches with a 5-minute timeout. Cairn's existing #51 flow already beats all of these: /setup is a browser wizard, but guarded by a one-time token printed only to the operator's log, so "whoever races first" cannot claim the instance and there's no set-once env trap. We keep it and extend it; nothing to copy here.

Local login beside SSO. Nextcloud hides the local form behind /login?direct=1 — the support forums are full of admins who couldn't find it during an IdP outage, and their own security note warns the hidden route still exposes stale local passwords (hiding ≠ protection). Seafile keeps the classic form visible next to the SSO button. Grafana shows SSO buttons above the form and lets you disable_login_form — after which a broken IdP means editing config on disk to get back in. Nikola's visible-link decision matches the Seafile/Grafana-default pattern and dodges Nextcloud's documented confusion; the security answer to "visible = exposed" is few, admin-managed, argon2id-hashed local accounts + the existing verify limiter + audit, not hiding the door.

Local users beside a directory. oCIS bundles a minimal LDAP (IDM) it explicitly positions for "small environments up to a few hundred users" — precedent for our 50 cap and for treating built-in accounts as a first-class small-deployment mode, not a degraded one. Pydio keeps an internal user repo and auto-files externally-authenticated users into a separate group — mirrors how our seats list already tracks seen IdP users, which gives the promote/demote UI its user list for free.

Code recon — what exists (more than the issue assumed):

  • #admin/users/access already exists: admin topic users, tabs access|seats, pane #adm-users via loadUsers(). The new UI grows inside the existing Access tab — no new topic needed.
  • /setup (#51) is a real page (setup.html) + POST /setup: token check, username validation, ≥10-char password, store.Create(username, hash, admin=true), closes permanently once a user exists. Gated today on local mode only (pwLogin != nil + empty store + no config users).
  • LocalStore (local-users.json, statestore-backed so it survives redeploys per #153): storedUser{PasswordHash, Admin, CreatedAt}Create/lookup/Empty only. Needs List / Delete / SetPassword / SetAdmin + the 50 cap.
  • There is also the D11 storage-setup wizard (/storage-setup, same token mechanism) — the wizard chain becomes: storage → first admin → done.

Implementation shape this points to (the mockup follows it):

  1. Extend the #51 setup gate to run in every auth mode while the local store is empty → Manuel's "local admins must always be possible".
  2. OIDC login page: IdP button primary, plus a visible "Sign in with a local account" reveal → posts to pwLogin, which gets wired in OIDC mode via the existing auth.Multi pattern (app-passwords already stack this way).
  3. Access tab grows: local-accounts table + add/reset/promote/demote/remove, 50-cap meter, plaintext-JSON note steering production to an IdP; with an IdP present, a directory-accounts section (fed by the seats "seen users" list) for admin promote/demote.
  4. New audit verbs (user-create, user-remove, user-password-reset, admin-promote, admin-demote) — the #433 registry guard will enforce registration.
  5. Local users hit the licence seat accounting like everyone else (Admit already runs at login).

Sources: Nextcloud direct-login discussion, nextcloud-oidc-login README, OpenCloud external IdP docs, oCIS IDM service docs, Seafile SSO setup, Pydio manage users, Grafana admin+SSO thread, Grafana disable login form.

Mockup with the full flow follows for review.

**Competitor research + code recon — the design basis for the mockup.** **How the field handles it:** *Initial admin at deploy.* Three patterns exist: env-var before first start (oCIS `INITIAL_ADMIN_PASSWORD` — set-once, changes silently ignored later, a documented footgun; Grafana `GF_SECURITY_ADMIN_*`); default credentials with forced change (Grafana `admin/admin` — universally criticised); and a browser wizard on first run (Portainer, Nextcloud installer). Portainer's wizard has the unclaimed-instance problem — anyone who reaches the fresh instance first becomes admin — which it patches with a 5-minute timeout. **Cairn's existing #51 flow already beats all of these**: `/setup` is a browser wizard, but guarded by a one-time token printed only to the operator's log, so "whoever races first" cannot claim the instance and there's no set-once env trap. We keep it and extend it; nothing to copy here. *Local login beside SSO.* Nextcloud hides the local form behind `/login?direct=1` — the support forums are full of admins who couldn't find it during an IdP outage, and their own security note warns the hidden route still exposes stale local passwords (hiding ≠ protection). Seafile keeps the classic form visible next to the SSO button. Grafana shows SSO buttons above the form and lets you `disable_login_form` — after which a broken IdP means editing config on disk to get back in. Nikola's visible-link decision matches the Seafile/Grafana-default pattern and dodges Nextcloud's documented confusion; the security answer to "visible = exposed" is *few, admin-managed, argon2id-hashed local accounts + the existing verify limiter + audit*, not hiding the door. *Local users beside a directory.* oCIS bundles a minimal LDAP (IDM) it explicitly positions for "small environments up to a few hundred users" — precedent for our 50 cap and for treating built-in accounts as a first-class small-deployment mode, not a degraded one. Pydio keeps an internal user repo and auto-files externally-authenticated users into a separate group — mirrors how our seats list already tracks seen IdP users, which gives the promote/demote UI its user list for free. **Code recon — what exists (more than the issue assumed):** - `#admin/users/access` **already exists**: admin topic `users`, tabs `access|seats`, pane `#adm-users` via `loadUsers()`. The new UI grows inside the existing Access tab — no new topic needed. - `/setup` (#51) is a real page (`setup.html`) + `POST /setup`: token check, username validation, ≥10-char password, `store.Create(username, hash, admin=true)`, closes permanently once a user exists. Gated today on **local mode only** (`pwLogin != nil` + empty store + no config users). - `LocalStore` (`local-users.json`, statestore-backed so it survives redeploys per #153): `storedUser{PasswordHash, Admin, CreatedAt}` — **Create/lookup/Empty only**. Needs List / Delete / SetPassword / SetAdmin + the 50 cap. - There is also the D11 storage-setup wizard (`/storage-setup`, same token mechanism) — the wizard chain becomes: storage → first admin → done. **Implementation shape this points to** (the mockup follows it): 1. Extend the #51 setup gate to run in **every auth mode** while the local store is empty → Manuel's "local admins must always be possible". 2. OIDC login page: IdP button primary, plus a visible "Sign in with a local account" reveal → posts to `pwLogin`, which gets wired in OIDC mode via the existing `auth.Multi` pattern (app-passwords already stack this way). 3. Access tab grows: local-accounts table + add/reset/promote/demote/remove, 50-cap meter, plaintext-JSON note steering production to an IdP; with an IdP present, a directory-accounts section (fed by the seats "seen users" list) for admin promote/demote. 4. New audit verbs (`user-create`, `user-remove`, `user-password-reset`, `admin-promote`, `admin-demote`) — the #433 registry guard will enforce registration. 5. Local users hit the licence seat accounting like everyone else (Admit already runs at login). Sources: [Nextcloud direct-login discussion](https://help.nextcloud.com/t/login-as-local-user-despite-default-redirect-to-sso-oidc-provider/174466), [nextcloud-oidc-login README](https://github.com/pulsejet/nextcloud-oidc-login/blob/master/README.md), [OpenCloud external IdP docs](https://docs.opencloud.eu/docs/admin/configuration/authentication-and-user-management/external-idp/), [oCIS IDM service docs](https://doc.owncloud.com/ocis/latest/deployment/services/s-list/idm.html), [Seafile SSO setup](https://server.camp/docs/en/services/seafile/seafile-sso/), [Pydio manage users](https://docs.pydio.com/latest/admin-guide/connect-your-users/manage-users/), [Grafana admin+SSO thread](https://community.grafana.com/t/admin-user-with-sso-authentication/10437), [Grafana disable login form](https://community.grafana.com/t/disable-basic-password-login-form/120197). Mockup with the full flow follows for review.
Author
Owner

Wave 1 shipped — v0.6.194 (PR #545), live on both dogfoods.

Backend complete per the approved mockup: LocalStore CRUD + 50-cap + last-sign-in stamp, AdminStore for runtime directory-admin grants (both in the #153 state set — boot now shows files=18), local accounts + /setup in every auth mode, POST /auth/password live in OIDC via the Multi stack, WithAdminFlagRuntime union, the full /api/v1/admin/local-users + directory-admins + /api/v1/me/password API with last-admin/self guards, and five new audit verbs (the #433 guard caught my parameterized helper and forced literals at call sites — working as designed).

Live proof on files-bao (OIDC, empty local store): FIRST-RUN SETUP available — open /setup and paste this one-time token in the boot log. Both dogfoods are now claimable: grab the token from each pod's log (kubectl logs -n cairn deploy/cairn-openbao | grep FIRST-RUN) and create your break-glass admin at https://files-bao.c0rdyceps.ch/setup — that will also be the wave-2/3 test account. Until claimed, /setup waits harmlessly behind the token.

Wave 2 (login + setup pages) next, then wave 3 (Access tab UI).

**Wave 1 shipped — v0.6.194 (PR #545), live on both dogfoods.** Backend complete per the approved mockup: LocalStore CRUD + 50-cap + last-sign-in stamp, AdminStore for runtime directory-admin grants (both in the #153 state set — boot now shows `files=18`), local accounts + `/setup` in **every** auth mode, `POST /auth/password` live in OIDC via the Multi stack, `WithAdminFlagRuntime` union, the full `/api/v1/admin/local-users` + `directory-admins` + `/api/v1/me/password` API with last-admin/self guards, and five new audit verbs (the #433 guard caught my parameterized helper and forced literals at call sites — working as designed). **Live proof on files-bao (OIDC, empty local store):** `FIRST-RUN SETUP available — open /setup and paste this one-time token` in the boot log. Both dogfoods are now claimable: grab the token from each pod's log (`kubectl logs -n cairn deploy/cairn-openbao | grep FIRST-RUN`) and create your break-glass admin at `https://files-bao.c0rdyceps.ch/setup` — that will also be the wave-2/3 test account. Until claimed, `/setup` waits harmlessly behind the token. Wave 2 (login + setup pages) next, then wave 3 (Access tab UI).
Author
Owner

All three waves shipped — v0.6.194 / .195 / .196 (PRs #545, #546, #547), live on both dogfoods.

The full approved flow is now real:

  • Wave 1 (backend): local accounts in every auth mode, 50-cap store + runtime admin grants (state-in-backend), /setup gate widened, POST /auth/password in OIDC, full admin API with guards and five audited verbs.
  • Wave 2 (pages): visible "Sign in with a local account" beneath the SSO button, in-place reveal, /login#local deep link, back link; setup page with repeat-password and closes-permanently copy. (One flagged deviation: no storage→admin stepper — the page can't know whether the storage wizard ran.)
  • Wave 3 (Access tab): local-accounts section with cap meter, add/reset dialogs with generated show-once passwords, promote/demote/remove; directory-accounts section from the seats seen-list with Make/Remove admin; truthful page lead.

Your verification pass (both dogfoods are unclaimed — each restart mints a fresh token):

  1. kubectl logs -n cairn deploy/cairn-openbao | grep FIRST-RUN → open https://files-bao.c0rdyceps.ch/setup, paste the token, create your break-glass admin (you land signed in).
  2. Sign out → the login page should show the SSO button and the local-account link; sign in locally.
  3. #admin/users/access → your local account listed (1 of 50), add a test account with the generated password, reset it, promote/demote, remove; check the audit query shows user-create etc.
  4. Directory section: nikola-test (and any seen user) with Make admin.

Still open on this issue: docs handbook page "Running without an identity provider" (plaintext-JSON note, 1–3 emergency admins guidance — Manuel's requirement 5), phone-view polish of the new Access sections if your #389 pass finds issues, and Manuel's own run-through before closing. The self-service password-change UI (backend /api/v1/me/password exists) also has no surface in the user menu yet — small follow-up.

**All three waves shipped — v0.6.194 / .195 / .196 (PRs #545, #546, #547), live on both dogfoods.** The full approved flow is now real: - **Wave 1 (backend):** local accounts in every auth mode, 50-cap store + runtime admin grants (state-in-backend), `/setup` gate widened, `POST /auth/password` in OIDC, full admin API with guards and five audited verbs. - **Wave 2 (pages):** visible "Sign in with a local account" beneath the SSO button, in-place reveal, `/login#local` deep link, back link; setup page with repeat-password and closes-permanently copy. (One flagged deviation: no storage→admin stepper — the page can't know whether the storage wizard ran.) - **Wave 3 (Access tab):** local-accounts section with cap meter, add/reset dialogs with generated show-once passwords, promote/demote/remove; directory-accounts section from the seats seen-list with Make/Remove admin; truthful page lead. **Your verification pass** (both dogfoods are unclaimed — each restart mints a fresh token): 1. `kubectl logs -n cairn deploy/cairn-openbao | grep FIRST-RUN` → open `https://files-bao.c0rdyceps.ch/setup`, paste the token, create your break-glass admin (you land signed in). 2. Sign out → the login page should show the SSO button *and* the local-account link; sign in locally. 3. `#admin/users/access` → your local account listed (1 of 50), add a test account with the generated password, reset it, promote/demote, remove; check the audit query shows `user-create` etc. 4. Directory section: `nikola-test` (and any seen user) with Make admin. **Still open on this issue:** docs handbook page "Running without an identity provider" (plaintext-JSON note, 1–3 emergency admins guidance — Manuel's requirement 5), phone-view polish of the new Access sections if your #389 pass finds issues, and Manuel's own run-through before closing. The self-service password-change UI (backend `/api/v1/me/password` exists) also has no surface in the user menu yet — small follow-up.
Author
Owner

Stepper follow-up — v0.6.198… correction: v0.6.197 (PR #548), live.

Nikola's suspicion after seeing /setup on files-bao: the "Set up this Cairn" title + Storage ✓ → Administrator stepper from the mockup only appear on truly new instances. Reality check: the stepper had not been implemented at all — the flagged wave-2 deviation ("the page cannot know whether the storage wizard ran"). That call was wrong: main already knows, because storageSource == "bootstrap" means the D11 wizard wrote the pointer.

Fixed: /setup/status now carries storageWizard (TDD, witnessed red), and the setup page conditionally titles itself "Set up this Cairn" with the mockup's stepper only when the storage wizard actually ran first. Config-provisioned instances (both dogfoods) keep the plain first-run page — honest either way. So the behaviour Nikola expected is now the behaviour that exists, verifiable on the fresh wizard-deployed test instance planned in a few days.

Also confirmed live meanwhile: /setup on files-bao redirects to /login (setup claimed and closed permanently), the claimed admin survived the v0.6.196→197 redeploy via the state backend, and the login page shows the wave-2 SSO + local-account layout.

**Stepper follow-up — v0.6.198… correction: v0.6.197 (PR #548), live.** Nikola's suspicion after seeing `/setup` on files-bao: the "Set up this Cairn" title + Storage ✓ → Administrator stepper from the mockup only appear on truly new instances. Reality check: the stepper had **not been implemented at all** — the flagged wave-2 deviation ("the page cannot know whether the storage wizard ran"). That call was wrong: main already knows, because `storageSource == "bootstrap"` means the D11 wizard wrote the pointer. Fixed: `/setup/status` now carries `storageWizard` (TDD, witnessed red), and the setup page conditionally titles itself "Set up this Cairn" with the mockup's stepper **only when the storage wizard actually ran first**. Config-provisioned instances (both dogfoods) keep the plain first-run page — honest either way. So the behaviour Nikola *expected* is now the behaviour that *exists*, verifiable on the fresh wizard-deployed test instance planned in a few days. Also confirmed live meanwhile: `/setup` on files-bao redirects to `/login` (setup claimed and closed permanently), the claimed admin survived the v0.6.196→197 redeploy via the state backend, and the login page shows the wave-2 SSO + local-account layout.
Author
Owner

v0.6.198 shipped (PR #549) — login/setup mockup fidelity, per Nikola's screenshot comparison.

Root cause of the visual drift was a CSS cascade bug, not missing markup: .divider { display:flex } was declared after .hidden { display:none }, so at equal specificity the later rule won and the "or" divider stayed visible in the revealed local-account state. Fixed with a compound selector .divider.hidden { display:none }.

The same bug existed latently in setup.html — .steps { display:flex } after .hidden meant the v0.6.197 wizard stepper would have shown on every instance instead of only wizard-deployed ones (exactly backwards). Fixed the same way (.steps.hidden) before anyone saw it; the conditional logic itself was already correct.

Fidelity changes on both pages:

  • Placeholders replaced with bold labels above each field (mockup's .frow pattern, left-aligned), inputs on surface white with slightly taller padding. Setup's password requirement moved into a muted label suffix "(at least 10 characters)".
  • Blank band below "Local accounts are managed on this instance by its administrators." removed (#msg margin and card bottom padding tightened).
  • "← Back to single sign-on" untouched, as requested.

Live-verified on files-bao at /login#local: labelled fields, no divider, back link + note with tight spacing; and the default state still shows SSO button + divider + local-account link with form/back/note hidden. The setup-page changes ride along for the fresh test instance in a few days.

**v0.6.198 shipped (PR #549) — login/setup mockup fidelity, per Nikola's screenshot comparison.** Root cause of the visual drift was a CSS cascade bug, not missing markup: `.divider { display:flex }` was declared *after* `.hidden { display:none }`, so at equal specificity the later rule won and the "or" divider stayed visible in the revealed local-account state. Fixed with a compound selector `.divider.hidden { display:none }`. The same bug existed latently in setup.html — `.steps { display:flex }` after `.hidden` meant the v0.6.197 wizard stepper would have shown on **every** instance instead of only wizard-deployed ones (exactly backwards). Fixed the same way (`.steps.hidden`) before anyone saw it; the conditional logic itself was already correct. Fidelity changes on both pages: - Placeholders replaced with bold labels above each field (mockup's `.frow` pattern, left-aligned), inputs on surface white with slightly taller padding. Setup's password requirement moved into a muted label suffix "(at least 10 characters)". - Blank band below "Local accounts are managed on this instance by its administrators." removed (`#msg` margin and card bottom padding tightened). - "← Back to single sign-on" untouched, as requested. Live-verified on files-bao at `/login#local`: labelled fields, no divider, back link + note with tight spacing; and the default state still shows SSO button + divider + local-account link with form/back/note hidden. The setup-page changes ride along for the fresh test instance in a few days.
Author
Owner

v0.6.204 shipped (PR #557, live on both dogfoods) — the last build item on this issue is done.

The user menu gains Change password: current/new/repeat with inline validation (mismatch, under 10 characters, wrong current password), backed by the POST /api/v1/me/password endpoint that has existed since wave 1. On an SSO account the dialog answers with "Your password is managed by your identity provider, not by Cairn" instead of a raw 409. i18n ×4.

Also in this release, from Nikola's Accounts-tab dogfooding (#552 thread): creating a local account named like a seen directory account is now refused with an inline explanation — the two would share one identity, so a password sign-in would have reached the directory user's files (a security catch, not just UX; local re-creations and config admins are exempt); and group member adds gained autocomplete + validation via GET /api/v1/admin/groups/candidates.

What remains on #533 is verification, not code: Nikola's fresh-instance wizard pass (stepper, token flow, setup closing) on the upcoming test deployment, and Manuel's run-through of the no-IdP story (docs at docs/handbook/local-accounts.md). Once those pass, this can close.

**v0.6.204 shipped (PR #557, live on both dogfoods) — the last build item on this issue is done.** The user menu gains **Change password**: current/new/repeat with inline validation (mismatch, under 10 characters, wrong current password), backed by the `POST /api/v1/me/password` endpoint that has existed since wave 1. On an SSO account the dialog answers with "Your password is managed by your identity provider, not by Cairn" instead of a raw 409. i18n ×4. Also in this release, from Nikola's Accounts-tab dogfooding (#552 thread): creating a local account named like a **seen directory account is now refused with an inline explanation** — the two would share one identity, so a password sign-in would have reached the directory user's files (a security catch, not just UX; local re-creations and config admins are exempt); and group member adds gained **autocomplete + validation** via `GET /api/v1/admin/groups/candidates`. **What remains on #533 is verification, not code:** Nikola's fresh-instance wizard pass (stepper, token flow, setup closing) on the upcoming test deployment, and Manuel's run-through of the no-IdP story (docs at `docs/handbook/local-accounts.md`). Once those pass, this can close.
Sign in to join this conversation.
No labels
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#533
No description provided.