16 Commits

Author SHA1 Message Date
wmantly 4a592f9795 Release 1.11.0: end-user catalog, access requests, nested groups
Closes the end-user half of the directory and adds nested LDAP groups.

The directory could describe the lab but could not tell anyone what they had
or how to reach it, and several of the paths meant to do so were silently
returning nothing:

  - GET /api/discovery/me resolved groups from req.user.groups, which does not
    exist (req.user carries memberOf), so it returned only isPublic resources
    for every human caller -- "My Services" was blank for everyone. The same
    read made isDirectoryAdmin() false for real admins.
  - The portal's "Discover More Services" called the admin-gated endpoint and
    swallowed the 403, so it never rendered for non-admins at all.
  - Services reported no address, because /me had reimplemented getMyAccess
    without its parent-walking resolution.

Adds the catalog at /, self-service access requests, and admin access
visibility (per-resource counts, and the reverse "what can this user reach").

Nested groups come in two halves. groupOfNames.member already accepts a group
DN, so nesting needs no schema -- what it needs is resolution, which no
released OpenLDAP performs. The all-in-one image therefore builds slapd from a
pinned master commit for the nestgroup overlay, and the app computes the
closure itself when pointed at a server without it. Both paths are covered.

member-values is deliberately left out of nestgroup-flags: it expands `member`
when reading a group, which destroys the distinction between "listed here" and
"reachable through a nested group" and is not recoverable afterwards.

Full suite green in both resolution modes: 215 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:22:08 -04:00
wmantly e9b808d1c2 Add cross-app super admin group; rename Executive page to Overview
- app_super_admin is a new cross-app LDAP group (also recognized by proxy
  and jump-host) that grants full admin here regardless of app_sso_admin
  membership: bypassed centrally in utils/permission.js's byGroup, folded
  into GET /api/user/me's isAdmin flag, and added to nav/forceLogin gates
  alongside app_sso_admin.
- Renamed the Executive page to Overview (route, view, API path
  /api/metrics/overview, nav label, docs), keeping /executive as a 301
  redirect alongside the existing /admin, /notifications, /dashboard
  legacy redirects.
2026-07-30 11:56:58 -04:00
wmantly 3a46680c8b Fix HTML-escaped loading indicator and missing success messages
Two regressions surfaced by a fresh production install:

- formAJAX's "loading" indicator passed a raw <div class="spinner-border">
  string to app.messages.action, which HTML-escapes its message by design
  (@simpleworkjs/frontend) -- so every form submit briefly showed the
  literal markup as text instead of a spinner. Replaced with plain text
  ("Saving…"), which needs no escaping workaround.

- POST /api/user/ (create) and PUT /api/user/password didn't include a
  `message` field, so the success toast/banner rendered with an empty
  body -- a green notification with nothing in it right after adding a
  user. Added messages matching the convention already used by every
  other route in this file (activate/deactivate, group membership, etc).

Verified live: created a user through the actual modal, confirmed the
POST response now carries a message, and confirmed app.messages.toast
renders plain text cleanly with no escaping artifacts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 12:52:09 -04:00
wmantly 208762f0d1 Unify the front-end UI shell across the theta42 apps
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).

Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
  for each of the current user user/me groups. sso-manager-node sends LDAP
  DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
  CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
  proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
  nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
  and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
  to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
  undefined `callback2` reference that threw on a non-function callback.

Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.

sso-manager-node specifics:
- val.js adopts the shared superset (adds the target/hostname rules and
  the password policy, and fixes the let-shadowed `message` that stopped
  custom rule messages from reaching validateMessage).
- GET /api/user/me now also reports isAdmin (membership in app_sso_admin).
- public/js/app.js: $.isFunction -> typeof (removed in jQuery 4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:57:34 -04:00
wmantly c4d7a1a8e9 feat: actionable metrics, LDAP log parsing, UI updates 2026-07-22 21:58:06 -04:00
wmantly 82f703f560 Add personal Unix group member management
Every account gets a personal posixGroup at creation (its primary GID
holder) but there was no way to manage its memberUid list -- add
add/remove endpoints and a profile-page UI (admin-only), reusing the
userSelect widget already built for the manager field.

Bumps to v1.1.9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 11:28:41 -04:00
wmantly 5d7c0bd594 Fix account-editing bugs from real-world feedback, add editable group membership
- Edit form's Mobile Phone field was effectively required (stray validate
  attribute) -- removed.
- Service account profiles always showed the literal filler name "Service
  Account" -- hidden now, since it's not meaningful. Required computing
  isServiceAccount in User.get(), not just listDetail().
- Fresh service accounts could look uncategorized (missing from the
  Service Accounts tab, wrong isServiceAccount) for up to 5 minutes after
  creation, due to a cache-staleness race in the create route -- the user
  gets cached via User.get() before the route marks it as a service
  account. Cleared and re-fetched after marking.
- memberOf came back as a bare string instead of a one-element array for
  users in exactly one group, causing client-side permission checks to
  iterate character-by-character and incorrectly deny access -- normalized
  alongside the existing manager normalization.
- Added editable group membership on the profile page ("My groups"),
  admin-only, using the existing per-group member endpoints.

Bumps to v1.1.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 11:00:50 -04:00
wmantly cdc5d1528c Unify service accounts to one kind, add manager field, make homeDirectory/loginShell editable
Removes the LDAP bind-only service account type in favor of a single
Unix/POSIX account model, surfaced in a new Users > Service Accounts tab.
Adds a multi-valued `manager` field to every account (defaults to the
creator, editable, and grants edit rights on the accounts a person manages
without needing app_sso_admin). homeDirectory and loginShell are now
editable from the profile edit form.

Bumps to v1.1.7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 00:32:19 -04:00
wmantly 3790e8001a Add Unix/POSIX service accounts, distinct from LDAP bind-only ones
The Integrations page's Service Accounts (bind-only, organizationalRole)
don't cover the other real use case: an account something actually runs
as on a Linux host -- a media manager, a torrent client, Emby -- with a
real uidNumber/gidNumber that owns files, and a group other accounts
join for write access (e.g. a `stuff_manager` group granting write
rights to a media library). That needs a real posixAccount, which the
bind-only model can't be.

- New well-known group `app_sso_service_account`, seeded the same way as
  app_sso_admin/app_sso_invite/app_sso_oauth_admin (docker-entrypoint.sh,
  ops/ldap-setup.sh). Not a permission gate -- a marker.
- "Add new user" form gets a "This is a service account" checkbox: swaps
  the person-shaped fields (first/last name, birthday, ToS agreement)
  for a single account-name field, since none of those make sense for a
  non-person account. On create, the route adds the user to
  app_sso_service_account.
- User.listDetail() annotates each user with isServiceAccount (checked
  against the marker group's member list once per call, not the memberof
  overlay's reverse attribute -- not reliably returned by every LDAP
  server this app might point at, confirmed against a real external
  directory during testing). Users page shows a "service" badge.
- Notification broadcasts (filter_type=all/all_active) exclude service
  accounts by default -- nobody reads mail as `stuff_manager`.
- Fixed a real, previously-unrelated bug this surfaced: addPosixAccount
  unconditionally set `mail: data.mail` in the LDAP entry even when
  undefined, and ldapts/slapd reject an attribute given an explicit
  undefined value ("no values for attribute type") rather than treating
  it as absent. This meant creating ANY user without an email already
  failed outright -- not something a service account (which commonly has
  no real mailbox) could route around. Made mail conditional, matching
  how mobile/sshPublicKey/dob already work.
- docs/ldap.md now explains both kinds of service account side by side
  and when to use which.

Verified end-to-end against a real external LDAP server (not a local
sandbox): created a service account with no email, confirmed it's
correctly flagged and excluded from broadcast recipient resolution,
confirmed a normal user is unaffected, confirmed the code degrades
gracefully if the marker group doesn't exist yet (pre-upgrade
deployments).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:52:50 -04:00
wmantly 5644bfa5ec Updated frontend 2026-07-01 11:10:09 -04:00
wmantly 2a6aaafa3d Added permission to block non admins from seeing all users 2024-02-19 10:42:02 -05:00
wmantly 5143162510 Better error message failed removing user 2020-12-30 15:27:06 -05:00
wmantly b8f632e644 user edit 2020-05-15 15:17:57 -04:00
wmantly 0889832efc groups and reset 2020-05-15 00:40:15 -04:00
wmantly 4d51a4ac9e rc1 2020-05-05 23:07:00 -04:00
wmantly 8dc0e946b1 beta 2020-05-03 18:22:51 -04:00