Accounts & Groups tabs under Users & access — app-owned groups alongside directory groups #550

Closed
opened 2026-09-14 18:14:21 +00:00 by Cordy · 3 comments
Owner

Follow-on from #533, approved with mockup admin-accounts-groups-mockup.html (rev 1) on 2026-09-14. The Access tab's account lists can grow very large and deserve their own surface, and Cairn gains app-owned groups so admins can group users without (or alongside) an IdP.

Approved design

Tab layout: Users & access becomes Access · Accounts · Groups · Seats (#admin/users/accounts, #admin/users/groups, shared admtab idiom).

  • Access slims to identity wiring and health: auth mode, issuer, admin group, admin-client chip, signed-in/active counters. The Administrators section is removed as redundant — admins are visible on Accounts, especially with the role filter (Nikola, mockup review).
  • Accounts hosts the two lists moved from Access, behaviour unchanged from v0.6.196: Local accounts (create / reset / role / remove, 50-cap meter) and Directory accounts (read-only rows, Cairn-admin grant; group-granted admin shows its source and disables Revoke with a tooltip). One filter box over both sections that also filters by role (e.g. admins only) and by group (typing a group name shows the accounts assigned to it). Directory section paginated ("Load more") — never render everything (the Nextcloud users-page lesson).
  • Groups mirrors that shape:
    • App-owned groups: created and managed in Cairn, members can be local or directory accounts. Chip-style member editing, bulk add with per-name success/failure reporting, rename, delete. Usable everywhere a directory group works: space membership, share targets, peering receiver lists, admin group.
    • Directory groups: OpenCloud-style lock treatment — visible, countable, "View members" drill-in, but read-only with one line saying where to edit them. Degraded state without the IdP admin client: names seen in sign-in tokens only, counts shown as —, warn notebox pointing at the admin-client config. In pure local mode the directory sections don't render at all.
    • Used by column: counts of shares / spaces / peering receiver lists / admin-group role referencing the group, so an admin knows what breaks before deleting or renaming.

Approved decisions

  1. Single namespace: app and directory groups share one name space. Creating an app group whose name Cairn knows as a directory group is refused (not warned) — shares and spaces address groups by name; ambiguity would be a security bug.
  2. Internal ids: app groups carry an internal id so rename does not rewrite every share/space reference.
  3. Delete fails closed: deleting a referenced app group leaves references inert (a share/receiver list addressed to it matches nobody) rather than mutating other subsystems' state; the confirm dialog lists what stops working.
  4. Administrators section on Access: removed (see above).

Competitor grounding (research 2026-09-14)

  • Nextcloud: all-users view loads everything (30s–2min waits reported); LDAP groups greyed out with no explanation → our paging + explicit lock copy.
  • OpenCloud: external groups locked with a lock symbol, local groups editable → adopted.
  • ownCloud oCIS: shipped with no group UI (API only); the later extension is praised for bulk member-add with per-name reporting → adopted; criticised for missing descriptions/nesting; claim-based group assignment is a requested feature (ocis#7341) — Cairn already has claims groups via livegroups, the merged view is the differentiator.

Implementation sketch

  • internal/auth/groupstore.go: GroupStore at /data/.cairn/groups.json (statestore-bound like local-users): id, name, members, created. CRUD + membership lookup.
  • WithAppGroups(inner, store) provider wrapper merging app-group membership into User.Groups, placed under WithAdminFlag so an app group can serve as admin group.
  • internal/api/groups.go: GET/POST /api/v1/admin/groups, DELETE/rename /{id}, POST/DELETE members; directory-group section in the same GET payload (admin-client listing where possible, else seen-names fallback with a partial flag); references counts.
  • Audit verbs: group-create, group-remove, group-rename, group-member-add, group-member-remove (registry group "users").
  • Frontend: tab split, moved sections, filters, groups UI + dialogs per mockup, i18n ×4, #389 fence gate on index.html.

Waves: 1 backend core (TDD), 2 frontend, docs ride along. Ship train per wave, live-verify on files-bao.

Follow-on from #533, approved with mockup `admin-accounts-groups-mockup.html` (rev 1) on 2026-09-14. The Access tab's account lists can grow very large and deserve their own surface, and Cairn gains **app-owned groups** so admins can group users without (or alongside) an IdP. ## Approved design **Tab layout:** Users & access becomes Access · **Accounts** · **Groups** · Seats (`#admin/users/accounts`, `#admin/users/groups`, shared admtab idiom). - **Access** slims to identity wiring and health: auth mode, issuer, admin group, admin-client chip, signed-in/active counters. **The Administrators section is removed as redundant** — admins are visible on Accounts, especially with the role filter (Nikola, mockup review). - **Accounts** hosts the two lists moved from Access, behaviour unchanged from v0.6.196: Local accounts (create / reset / role / remove, 50-cap meter) and Directory accounts (read-only rows, Cairn-admin grant; group-granted admin shows its source and disables Revoke with a tooltip). One filter box over both sections that also filters **by role** (e.g. admins only) and **by group** (typing a group name shows the accounts assigned to it). Directory section paginated ("Load more") — never render everything (the Nextcloud users-page lesson). - **Groups** mirrors that shape: - *App-owned groups*: created and managed in Cairn, members can be local or directory accounts. Chip-style member editing, bulk add with per-name success/failure reporting, rename, delete. Usable everywhere a directory group works: space membership, share targets, peering receiver lists, admin group. - *Directory groups*: OpenCloud-style lock treatment — visible, countable, "View members" drill-in, but read-only with one line saying where to edit them. Degraded state without the IdP admin client: names seen in sign-in tokens only, counts shown as —, warn notebox pointing at the admin-client config. In pure local mode the directory sections don't render at all. - *Used by* column: counts of shares / spaces / peering receiver lists / admin-group role referencing the group, so an admin knows what breaks before deleting or renaming. ## Approved decisions 1. **Single namespace:** app and directory groups share one name space. Creating an app group whose name Cairn knows as a directory group is **refused** (not warned) — shares and spaces address groups by name; ambiguity would be a security bug. 2. **Internal ids:** app groups carry an internal id so rename does not rewrite every share/space reference. 3. **Delete fails closed:** deleting a referenced app group leaves references inert (a share/receiver list addressed to it matches nobody) rather than mutating other subsystems' state; the confirm dialog lists what stops working. 4. Administrators section on Access: removed (see above). ## Competitor grounding (research 2026-09-14) - Nextcloud: all-users view loads everything (30s–2min waits reported); LDAP groups greyed out with no explanation → our paging + explicit lock copy. - OpenCloud: external groups locked with a lock symbol, local groups editable → adopted. - ownCloud oCIS: shipped with no group UI (API only); the later extension is praised for bulk member-add with per-name reporting → adopted; criticised for missing descriptions/nesting; claim-based group assignment is a requested feature (ocis#7341) — Cairn already has claims groups via livegroups, the merged view is the differentiator. ## Implementation sketch - `internal/auth/groupstore.go`: GroupStore at `/data/.cairn/groups.json` (statestore-bound like local-users): id, name, members, created. CRUD + membership lookup. - `WithAppGroups(inner, store)` provider wrapper merging app-group membership into `User.Groups`, placed **under** WithAdminFlag so an app group can serve as admin group. - `internal/api/groups.go`: GET/POST `/api/v1/admin/groups`, DELETE/rename `/{id}`, POST/DELETE members; directory-group section in the same GET payload (admin-client listing where possible, else seen-names fallback with a `partial` flag); references counts. - Audit verbs: `group-create`, `group-remove`, `group-rename`, `group-member-add`, `group-member-remove` (registry group "users"). - Frontend: tab split, moved sections, filters, groups UI + dialogs per mockup, i18n ×4, #389 fence gate on index.html. Waves: 1 backend core (TDD), 2 frontend, docs ride along. Ship train per wave, live-verify on files-bao.
Author
Owner

Shipped: v0.6.199 (backend) + v0.6.200 (UI), PR #551, live on both dogfoods.

Backend (v0.6.199, red witnessed on the runner before implementation):

  • auth.GroupStore at /data/.cairn/groups.json — statestore-bound like local-users, internal ids (decision 2), case-insensitive single namespace, idempotent membership ops, sorted listings.
  • WithAppGroups merges membership into User.Groups on every authentication, under the admin flag and above live groups — so an app group can be the admin group, and a live IdP refresh never drops app groups.
  • /api/v1/admin/groups: CRUD + rename, bulk member add with per-name {added, invalid} reporting (the oCIS lesson), directory listing (spaces.Manager.ListGroups via the IdP admin client; known-names fallback flagged dirSource: "seen"), per-group directory member drill-in. Creating or renaming onto a known directory name → 409 (decision 1). Delete fails closed (decision 3) — references in peering lists and the admin group simply stop matching; nothing else is mutated.
  • Peering group resolution consults app groups first, then the IdP: group-based peering allow-lists now work with no IdP client at all when the group is app-owned. Audit verbs group-create/-rename/-remove/-member-add/-member-remove (registry group users).

Frontend (v0.6.200): Access · Accounts · Groups · Seats. Access slimmed to identity wiring + counters, Administrators section removed (decision 4). Accounts: both lists moved intact, one filter (name substring / admin / app-group name — typing a group shows its members), account rows show their app groups, directory list pages 25 at a time. Groups: app-owned CRUD with chip member editing, comma-separated bulk add, reference counts on rows and in the delete confirm; directory groups read-only with live listing where the admin client exists, and the honest "names seen so far" note where it doesn't; hidden entirely in pure local mode. Deep links #admin/users/accounts and #admin/users/groups. i18n ×4, #389 fence gate green.

Verification so far: full go test ./... green; both pods booted clean on v0.6.200; fence gate + node --check on the shipped page. The signed-in admin views need a human session (redeploy cleared sessions) — Nikola's checklist:

  1. #admin/users — four tabs; Access shows wiring rows only, no Administrators list.
  2. Accounts — both lists present, filter by a name, by admin, and by a group name once one exists; create/reset/role/remove still work.
  3. Groups — create project-rhine with a member; check the members chip editor, bulk add with a junk name (per-name report), rename, delete confirm showing references; directory section shows keycloak groups with View members (files-bao has the admin client).
  4. Peering: add an app group to a receiver list on one side — should resolve without warnings.

Known follow-ups (not blocking): the peering "group lists need the IdP admin client" copy predates app groups and could soften; space names appear among directory groups (they ARE Keycloak groups) — a "space" badge would help; docs handbook page for groups rides with the #533 no-IdP page.

**Shipped: v0.6.199 (backend) + v0.6.200 (UI), PR #551, live on both dogfoods.** **Backend (v0.6.199, red witnessed on the runner before implementation):** - `auth.GroupStore` at `/data/.cairn/groups.json` — statestore-bound like local-users, internal ids (decision 2), case-insensitive single namespace, idempotent membership ops, sorted listings. - `WithAppGroups` merges membership into `User.Groups` on every authentication, under the admin flag and above live groups — so an app group can be the admin group, and a live IdP refresh never drops app groups. - `/api/v1/admin/groups`: CRUD + rename, bulk member add with per-name `{added, invalid}` reporting (the oCIS lesson), directory listing (`spaces.Manager.ListGroups` via the IdP admin client; known-names fallback flagged `dirSource: "seen"`), per-group directory member drill-in. Creating or renaming onto a known directory name → 409 (decision 1). Delete fails closed (decision 3) — references in peering lists and the admin group simply stop matching; nothing else is mutated. - Peering group resolution consults app groups first, then the IdP: **group-based peering allow-lists now work with no IdP client at all** when the group is app-owned. Audit verbs `group-create/-rename/-remove/-member-add/-member-remove` (registry group `users`). **Frontend (v0.6.200):** Access · **Accounts** · **Groups** · Seats. Access slimmed to identity wiring + counters, Administrators section removed (decision 4). Accounts: both lists moved intact, one filter (name substring / `admin` / app-group name — typing a group shows its members), account rows show their app groups, directory list pages 25 at a time. Groups: app-owned CRUD with chip member editing, comma-separated bulk add, reference counts on rows and in the delete confirm; directory groups read-only with live listing where the admin client exists, and the honest "names seen so far" note where it doesn't; hidden entirely in pure local mode. Deep links `#admin/users/accounts` and `#admin/users/groups`. i18n ×4, #389 fence gate green. **Verification so far:** full `go test ./...` green; both pods booted clean on v0.6.200; fence gate + `node --check` on the shipped page. The signed-in admin views need a human session (redeploy cleared sessions) — **Nikola's checklist:** 1. `#admin/users` — four tabs; Access shows wiring rows only, no Administrators list. 2. Accounts — both lists present, filter by a name, by `admin`, and by a group name once one exists; create/reset/role/remove still work. 3. Groups — create `project-rhine` with a member; check the members chip editor, bulk add with a junk name (per-name report), rename, delete confirm showing references; directory section shows keycloak groups with View members (files-bao has the admin client). 4. Peering: add an app group to a receiver list on one side — should resolve without warnings. Known follow-ups (not blocking): the peering "group lists need the IdP admin client" copy predates app groups and could soften; space names appear among directory groups (they ARE Keycloak groups) — a "space" badge would help; docs handbook page for groups rides with the #533 no-IdP page.
Author
Owner

Fix round + docs shipped.

v0.6.201 (PR #553, live on both dogfoods, clean boot with groups.json in the 19 statestore files): all five dogfood findings from Nikola's screenshots — Group/Members/Used-by columns with headers on both tables, View members now toggles instead of appending, New group / Add local user at mockup size beside the filter, surface-background inputs, inline duplicate-name message in the dialogs instead of the hidden 409 — plus the two ship-note follow-ups: Space marker on directory groups that are Cairn spaces (owner attribute + -ro twins, flagged through spaces.ListGroups with a test) and the peering receiver copy rewritten now that app-owned groups resolve without the IdP admin client. Deeper polish is parked in #552 (v0.7).

Docs run (PR #554, merged): the handbook was current through v0.6.178 — now aligned through v0.6.201. New pages local-accounts.md (the no-IdP story, Manuel's #533 request) and groups.md; users-access.md rewritten for four tabs; peering.md, index.md, README, ARCHITECTURE (§5.1 + new §5.5), encryption.md (#528 custody lifecycle) and file-browser.md (#389 phone section) all updated.

Remaining on this issue: nothing backend/UI — Nikola's live pass on v0.6.201 (columns, toggle, inline 409, Space markers) decides whether this closes.

**Fix round + docs shipped.** **v0.6.201** (PR #553, live on both dogfoods, clean boot with `groups.json` in the 19 statestore files): all five dogfood findings from Nikola's screenshots — Group/Members/Used-by columns with headers on both tables, View members now toggles instead of appending, New group / Add local user at mockup size beside the filter, surface-background inputs, inline duplicate-name message in the dialogs instead of the hidden 409 — plus the two ship-note follow-ups: **Space** marker on directory groups that are Cairn spaces (owner attribute + `-ro` twins, flagged through `spaces.ListGroups` with a test) and the peering receiver copy rewritten now that app-owned groups resolve without the IdP admin client. Deeper polish is parked in **#552** (v0.7). **Docs run** (PR #554, merged): the handbook was current through v0.6.178 — now aligned through v0.6.201. New pages `local-accounts.md` (the no-IdP story, Manuel's #533 request) and `groups.md`; `users-access.md` rewritten for four tabs; `peering.md`, `index.md`, README, ARCHITECTURE (§5.1 + new §5.5), `encryption.md` (#528 custody lifecycle) and `file-browser.md` (#389 phone section) all updated. Remaining on this issue: nothing backend/UI — Nikola's live pass on v0.6.201 (columns, toggle, inline 409, Space markers) decides whether this closes.
Author
Owner

Closing — feature complete and live.

Shipped across v0.6.199 (backend) → v0.6.206 (last fidelity round), all live on both dogfoods:

  • v0.6.199auth.GroupStore, WithAppGroups, full /api/v1/admin/groups CRUD, single-namespace 409, fail-closed delete, five audit verbs, group-based peering allow-lists resolving without an IdP admin client.
  • v0.6.200 — Access · Accounts · Groups · Seats; Access slimmed to identity wiring; Administrators section removed.
  • v0.6.201 — dogfood round 1: column headers, View-members toggle, toolbar sizing, inline duplicate-name message, Space markers on directory groups, peering copy rewritten.
  • v0.6.202 — house-style table headers, lock treatment, pagination with 25/50/100 page size, delete dialog per mockup.
  • v0.6.203 — lock beside the heading, mockup member panel with dashed directory chips, danger-button style de-scoped so it applies globally.
  • v0.6.206 — initial-circle avatars, one badge family, "Cairn role" header.

Docs: docs/handbook/groups.md and local-accounts.md written, users-access.md rewritten for four tabs (PR #554).

Remaining polish is tracked in #552 — live collision preview while typing, and an empty/degraded-state review against mockup variant D. The Used-by share count is tracked in #529, where it belongs: the column can only gain a real number once sharing can address a group.

**Closing — feature complete and live.** Shipped across v0.6.199 (backend) → v0.6.206 (last fidelity round), all live on both dogfoods: - **v0.6.199** — `auth.GroupStore`, `WithAppGroups`, full `/api/v1/admin/groups` CRUD, single-namespace 409, fail-closed delete, five audit verbs, group-based peering allow-lists resolving without an IdP admin client. - **v0.6.200** — Access · Accounts · Groups · Seats; Access slimmed to identity wiring; Administrators section removed. - **v0.6.201** — dogfood round 1: column headers, View-members toggle, toolbar sizing, inline duplicate-name message, Space markers on directory groups, peering copy rewritten. - **v0.6.202** — house-style table headers, lock treatment, pagination with 25/50/100 page size, delete dialog per mockup. - **v0.6.203** — lock beside the heading, mockup member panel with dashed directory chips, danger-button style de-scoped so it applies globally. - **v0.6.206** — initial-circle avatars, one badge family, "Cairn role" header. Docs: `docs/handbook/groups.md` and `local-accounts.md` written, `users-access.md` rewritten for four tabs (PR #554). Remaining polish is tracked in **#552** — live collision preview while typing, and an empty/degraded-state review against mockup variant D. The Used-by share count is tracked in **#529**, where it belongs: the column can only gain a real number once sharing can address a group.
Cordy closed this issue 2026-09-16 01:22:17 +00:00
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#550
No description provided.