Skip to content

Users settings page (replaces standalone users app)#750

Open
cyberb wants to merge 38 commits into
masterfrom
users-settings-page
Open

Users settings page (replaces standalone users app)#750
cyberb wants to merge 38 commits into
masterfrom
users-settings-page

Conversation

@cyberb

@cyberb cyberb commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds a built-in Users settings page to the platform UI, replacing the need for the standalone users snap app (ldap-user-manager). Everything is managed directly against the platform's OpenLDAP via the Go backend.

Features:

  • Add / remove users with username + password.
  • Admin switch per user — toggles membership in the syncloud posix group (the platform's admin group). Guards against removing the last admin.
  • Email field — critical for OIDC claims. Never saved empty: defaults to <username>@<device-domain>, or a custom address that must be a valid email. Editable per existing user.
  • Arbitrary groups — create/remove posix groups and toggle per-user membership (the syncloud admin group is managed via the dedicated admin switch and excluded from the group list).
  • OIDC email claim enabled — uncommented mail: mail in the Authelia LDAP attribute mapping so apps that require the email claim can log in (previously the claim was never emitted).

Backend (backend/)

  • auth/ldap.go: new ListUsers, SetUserEmail, ListGroups, AddGroup, RemoveGroup, SetGroupMember, SetAdmin, ResolveEmail (default/validate, never empty). AddUser now takes an email. DomainProvider injected for the default-domain.
  • rest/backend.go: /rest/users, /rest/users/{add,remove,email,admin}, /rest/groups, /rest/groups/{add,remove,member} — all admin-secured.
  • ioc, CLI user add --email updated. Unit tests for email resolution.

Frontend (web/platform/)

  • New views/Users.vue, route, Settings tile, i18n keys across all 10 locales.
  • Dev stub (src/stub/api.js) implements the new endpoints so the page works without a backend (npm run dev).

Tests

  • Go: auth/rest/ioc pass. New ResolveEmail unit tests.
  • Web: jest (incl. locale key-parity) + lint + build pass.
  • e2e: specs/10-users.spec.ts — add user (default email), edit email, admin switch, group create + membership, removal.

Notes

cyberb added 30 commits June 12, 2026 22:26
… jest unit tests for users list/edit screens
…red endpoints

Mirror the backend's per-endpoint admin checks in the UI: routes whose
data comes only from AdminSecuredHandle endpoints (access, internalmemory,
storage, updates, backup, certificate, certificate/log, health) now carry
meta.admin and their Settings tiles render only for admins. Pages served
via SecuredHandle (network, support, logs, customproxy, system) and the
mixed read-secured/write-admin pages (activation, twofactor, locale) stay
visible to regular users.
Jest Settings.spec asserts the tile gating both ways (non-admin sees
locale/twofactor and the user-facing tiles, never the admin tiles; admin
sees all). Playwright 11-nonadmin-settings creates a non-admin user via the
platform CLI, logs in, and confirms locale and two-factor open while admin
tiles (storage, users) are absent.
The users app being replaced binds as the rootDN (dc=syncloud,dc=org) for
every operation. Collapse adminBind into rootBind so all writes use one
identity, matching that model. Removes the cn=admin path entirely, including
the pre-existing inline binds in IsAdmin and RemoveUser. No ACL migration is
needed since the rootDN bypasses ACLs on every existing device.
… user and group into own types

Move the User and Group structs, password complexity check, email default/
validation, and the users-app LDAP attribute map out of ldap.go into
dedicated type files with constructors, each with its own unit test that
exercises the type directly. Inject the three collaborators into the ldap
Service via ioc instead of package-level funcs.

AddUser now takes the admin flag and sets group membership in the same bind,
so the UserAdd handler makes a single service call instead of orchestrating
add-then-set-admin. REST request bodies for the user/group endpoints become
named types, one per file, instead of inline anonymous structs. cli user add
gains --admin.
Add meta.admin to these four routes and hide their entry points from regular
users: the customproxy and system Settings tiles, and the App Center nav
links (header, mobile menu, empty-apps link). System already holds reboot/
shutdown, so gating the page hides those buttons from non-admins.

Protect the backend calls behind them to match: restart, shutdown and
proxy_custom list/add/remove move from SecuredHandle to AdminSecuredHandle.
apps/available and app install/remove/upgrade were already AdminSecured;
activate stays FailIfActivated (runs pre-activation, before any admin exists).

Jest Settings.spec moves customproxy/system into the admin-tile set; the
non-admin Playwright spec asserts the tiles and App Center link are hidden.
Regular users only need two-factor, so gate the remaining user-facing
settings behind admin: activation, network, support, logs and locale now
carry meta.admin and their Settings tiles render only for admins.

Protect their backend calls to match: network/interfaces, logs, logs/send,
device/url and the timezone/time GETs move from SecuredHandle to
AdminSecuredHandle. What stays open to any logged-in user is exactly the
non-settings surface plus 2FA: /rest/user, apps/installed, app, proxy/image
and settings/2fa GET.

Settings.spec keeps only two-factor in the user-tile set; the non-admin
Playwright spec asserts every other tile is hidden and only two-factor opens.
Per-user OTP enrollment lives in the separate login app (web/login
LoginApp.vue totp_setup, served by the login service), not platform settings;
the platform two-factor page is only a device-wide admin enable/disable
toggle. So a regular user needs nothing under settings.

Gate the whole area instead of per-tile: /settings and /twofactor carry
meta.admin, the Settings nav links (header + mobile) render only for admins,
the two-factor tile and the settings/2fa GET become admin-only. Platform
login stays open since it is the shared Authelia SSO every app uses; a regular
user simply lands on their apps with no settings or app-center navigation.

Settings.spec asserts non-admins see no tiles and admins see all; the
non-admin Playwright spec asserts apps are visible but settings and app-center
navigation are absent.
… link

The #apps nav link is display:none on mobile (collapsed into the burger),
so toBeVisible failed on the mobile project. Assert the Applications heading
(viewport-agnostic, as other specs do) and check both desktop and mobile
admin nav links are absent.
…ap.AddRequest

Instead of returning an attribute map the caller has to assemble into an add
request (and tack the password hash on), UserBuilder.Build takes all the
params it needs (username, email, uid, password) and returns a complete
*ldap.AddRequest. AddUser just executes it with conn.Add.
cyberb added 8 commits June 16, 2026 19:45
…dHasher

Drop the member bool that toggled add vs remove. SetGroupMember/modifyMember
become AddGroupMember/RemoveGroupMember (+ private addMember/removeMember), the
REST endpoint splits into /rest/groups/member/add and /rest/groups/member/remove
with a shared GroupMemberRequest, and the UI/stub call the matching one. Each
handler now makes a single unambiguous service call.

Also extract makeSecret into a PasswordHasher type with a Hash method, injected
into the ldap Service (SetPassword, Reset) and into UserBuilder (the add
request's userPassword), with its own unit test.
…ap Service

Move the root dial+bind out of the Service's rootBind helper into an LdapClient
type with Connect (dial + root bind) and Disconnect (close), injected via ioc.
Every connection the Service opens now goes through s.ldapClient.Connect and is
closed via s.ldapClient.Disconnect, so connection lifecycle lives in one place.
The Service did user CRUD, group CRUD, membership and LDAP lifecycle all at
once. Split the CRUD out into two focused types sharing the injected LdapClient:

- GroupManager (independent): group list/add/remove, membership add/remove, and
  Members(). Depends only on LdapClient.
- UserManager (depends on GroupManager): user CRUD, SetAdmin, ListUsers and
  IsAdmin -- all of which read or write the syncloud admin group, so admin/group
  membership goes through GroupManager rather than duplicating member logic.

Service keeps only what is neither user nor group CRUD: Init/ApplyConfig/Reset
lifecycle plus Authenticate/AuthenticateUser credential checks.

REST handlers now call userManager / groupManager independently; the admin
middleware checker and cli user command resolve UserManager; login/activation
keep using Service. ioc wires the three types; full-container resolution is
covered by the existing public_api ioc test.
The leftover Service did two unrelated things: LDAP provisioning lifecycle
(Init/ApplyConfig/Reset) and a single credential check used by the cli login
command. Extract Authenticate into its own Authenticator type (the only
consumer, cmd/cli/login, now resolves it), and rename the remaining
lifecycle type to Initializer to say what it actually is. The unused zap
logger field drops out with Authenticate.
…outes

These are plain backend endpoints, not a settings resource, so /rest/settings/2fa
becomes /rest/2fa and likewise for timezone, time and health/{events,metrics}.
Frontend callers and stub updated to match. Also remove the stale totp/setup
comment.
…tic error asserts

Match the file and test file names to the Initializer type and give it an i
receiver (the retry-loop counters become attempt to free up i). Switch the
auth unit tests from assert.Nil/NotNil on errors to assert.NoError/Error so
failures print the actual error.
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.

1 participant