app-owned Admins and Users for non idP provided instances #533
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#533
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
— 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:
[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
Distilled the conversation with Manuel into requirements, checked against what already exists in the code.
Requirements as I read them:
What already exists (more than we remembered): auth mode
"local"is fully implemented (auth.NewLocal(users).WithStore(...), argon2id PHC viacairnd hash-password);LocalUsersPathdefaults to/data/.cairn/local-users.jsonand already persists runtime-created users — the first-run setup admin from #51;CAIRN_ADMIN_*env-ensured local admin exists (#310);local-users.jsonis 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 (pwLoginis 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 existingspacestore(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:
/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).
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.
Decisions (Nikola, 2026-09-14):
#admin/users/access— either extending that topic or a new tab beside it: create users and admins, basic permissions, for the initial admin.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.
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; GrafanaGF_SECURITY_ADMIN_*); default credentials with forced change (Grafanaadmin/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:/setupis 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 youdisable_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/accessalready exists: admin topicusers, tabsaccess|seats, pane#adm-usersvialoadUsers(). 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./storage-setup, same token mechanism) — the wizard chain becomes: storage → first admin → done.Implementation shape this points to (the mockup follows it):
pwLogin, which gets wired in OIDC mode via the existingauth.Multipattern (app-passwords already stack this way).user-create,user-remove,user-password-reset,admin-promote,admin-demote) — the #433 registry guard will enforce registration.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.
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 +/setupin every auth mode,POST /auth/passwordlive in OIDC via the Multi stack,WithAdminFlagRuntimeunion, the full/api/v1/admin/local-users+directory-admins+/api/v1/me/passwordAPI 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 tokenin 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 athttps://files-bao.c0rdyceps.ch/setup— that will also be the wave-2/3 test account. Until claimed,/setupwaits harmlessly behind the token.Wave 2 (login + setup pages) next, then wave 3 (Access tab UI).
All three waves shipped — v0.6.194 / .195 / .196 (PRs #545, #546, #547), live on both dogfoods.
The full approved flow is now real:
/setupgate widened,POST /auth/passwordin OIDC, full admin API with guards and five audited verbs./login#localdeep 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.)Your verification pass (both dogfoods are unclaimed — each restart mints a fresh token):
kubectl logs -n cairn deploy/cairn-openbao | grep FIRST-RUN→ openhttps://files-bao.c0rdyceps.ch/setup, paste the token, create your break-glass admin (you land signed in).#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 showsuser-createetc.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/passwordexists) also has no surface in the user menu yet — small follow-up.Stepper follow-up — v0.6.198… correction: v0.6.197 (PR #548), live.
Nikola's suspicion after seeing
/setupon 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, becausestorageSource == "bootstrap"means the D11 wizard wrote the pointer.Fixed:
/setup/statusnow carriesstorageWizard(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:
/setupon 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.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.hiddenmeant 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:
.frowpattern, left-aligned), inputs on surface white with slightly taller padding. Setup's password requirement moved into a muted label suffix "(at least 10 characters)".#msgmargin and card bottom padding tightened).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.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/passwordendpoint 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.