wmantly 0e955abc73 Standardize the resource modal: tabs, footer, linkable URL, Children tab, site-slug group prefixing (#120)
* Add Resource audit fields (created/updated by/on) and site-slug group prefixing

Resource had no created_by/created_on/updated_by/updated_on fields at all,
unlike proxy's Host and jump-host's ApiToken which already track this --
needed for the upcoming resource-modal footer. @simpleworkjs/orm has no
auto-timestamp hook, so these are set explicitly in the directory-admin
route handlers on every create/update.

Also: when a host/service resource is created, its two auto-created LDAP
groups (<slug>_access/_admin) now get prefixed with the nearest ancestor
site's slug (via a new Resource.findAncestorSiteSlug walk), so groups from
different sites don't collide/look identical. Falls back to today's
unprefixed naming when a resource has no site ancestor.

Included the checked-in dev inventory.sqlite's ALTER TABLE for the new
columns, since @simpleworkjs/orm's sync() only creates missing tables, never
alters existing ones -- the raw model change alone would have broken every
Resource read/write against this file with "no such column: created_by".

* Migrate Resource modal onto app.modal's tabs/footer/URL, add Children tab

The Directory's resource modal was a separate, hand-rolled, always-in-DOM
Bootstrap modal, independent of the shared app.modal singleton -- migrating
it onto app.modal (now published with tabs/footer/url support in
@simpleworkjs/frontend 0.2.6) is the pilot for standardizing entity modals
across the stack.

- General/Details/Associated LDAP Groups/Children tabs, replacing the old
  single long form (Details keeps every kind-conditional container
  unchanged; toggleFormFields() didn't need to change at all).
- Footer shows created/updated by/on (via the new Resource audit fields)
  and the Save button; Groups/Children tabs are hidden in add-mode since
  they need an existing resource id.
- New Children tab lists a resource's existing children (reusing the
  already-loaded edges/resourcesById data, no new endpoint) and an "Add
  Child Resource" button that reuses openAddModal's existing preset-parent
  support. Folded the pre-existing generic "Relationships (Graph Edges)"
  section in underneath, under an "advanced" subheading, rather than
  dropping it or giving it a 5th tab of its own.
- GET /directory/:slug (mirroring the existing /users/:uid precedent) plus
  a client-side app.modal.deepLinkSlug() check makes a resource's modal
  linkable and directly loadable.
- Converted the groups/edges lists from jq-repeat to plain manual DOM
  rendering: jq-repeat's MutationObserver-based scope (re)registration for
  an element that's destroyed and recreated on every modal open runs
  asynchronously, so populating synchronously right after open() (as
  refreshGroupsUI/refreshEdgesUI must) raced it -- on the second and later
  opens, the old scope's destroy() ran after the new data was pushed onto
  it, silently discarding it. Manual rendering (matching the new Children
  tab) sidesteps the race entirely.
- The #res-name/#res-kind auto-slug handler is now bound via
  app.modal.on() (delegated) instead of directly -- a direct bind would
  have silently stopped firing after the first Add/Edit, since the modal
  body is rebuilt from scratch on every open().

Verified live against the running dev stack: tabs/footer/groups/children
all render and populate correctly (including on a second open, confirming
the jq-repeat race fix), the address bar updates to /directory/{slug} and
reverts on close, browser Back closes the modal via popstate without a
page reload, and a resource created under a Site gets correctly
site-slug-prefixed LDAP groups.
2026-07-28 17:42:05 -04:00

SSO Manager

A self-hosted OpenID Connect provider with a bundled OpenLDAP directory and a web management UI — for home labs and small businesses that want their own identity provider instead of a hosted one.

It gives you one place to manage your users and groups, one login (OIDC) that your modern apps can use, and one LDAP directory your older or odder apps can bind to directly. Everything runs on your own hardware; there is no phone-home, no hosted control plane, and no per-user pricing.

Setting up the whole stack (this SSO + the theta42/proxy in front of it) with one command? Skip to theta-env — its setup.sh wires the two together and generates the config for you.

Documentation: https://theta42.github.io/sso-manager-node/

Screenshots

Dashboard Users
Dashboard User list
Groups OAuth Apps
Groups OAuth clients
Sites & Replication
Sites

Features

  • OpenID Connect / OAuth 2.0 provider — issue your own access, refresh, and ID tokens; protect your apps with standard OIDC login. Discovery document at /.well-known/openid-configuration.
  • Bundled OpenLDAP directory — users, groups, POSIX accounts (posixAccount/inetOrgPerson), SSH public keys, and sudo roles, with memberOf + referential-integrity overlays. This is your single source of truth for identity, not a sidecar.
  • Web management UI — manage users, groups, and OAuth clients from a browser; invite and password-reset flows over email; user self-service for profile and API tokens.
  • Direct LDAP binds — Linux hosts (PAM/SSSD login, LDAP-backed sudo rules, SSH public keys via openssh-lpk) and LDAP-native apps (Gitea, Emby, and anything else that speaks LDAP) use LDAPS (636) or StartTLS against the same directory, so you don't maintain a second user database for them.
  • Personal access tokens — any user can mint a long-lived bearer token to drive the management API from scripts or CI, scoped to their own permissions.
  • All-in-one Docker image — app + OpenLDAP + Redis in one container, or run the pieces separately against your own LDAP/Redis via app_* env config.
  • Multi-Site Support (Geo-Location Scaling) — built-in support for N-Way Multi-Master OpenLDAP replication across physical sites for HA and low latency.

Why this over the alternatives

Tools like Keycloak, Authentik, Authelia, or Zitadel are OIDC providers, but LDAP is either a paid feature, a federation target you have to run separately, or absent. If your stack already has apps that speak LDAP directly (or you just want one real directory as the source of truth), you end up running two identity systems and keeping them in sync.

SSO Manager bundles the OpenLDAP directory with the OIDC provider, so OIDC apps and LDAP apps read from the same users and groups. The trade-off is scope: it is intentionally small and self-hosted, not an enterprise IAM suite — no fancy workflow engine, no hosted SaaS. If you want a lightweight, self-contained identity provider with a real LDAP backend, that is the niche.

Quick start

Three ways to run it, in order of how much it sets up for you:

theta-env composes this SSO Manager with the theta42/proxy (an OIDC-protected reverse proxy) and generates all the config from a single setup.env — you enter your domain once and it fills in the LDAP DNs, hostnames, OAuth issuer, and random secrets consistently:

git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp setup.env.example setup.env   # set CFG_DOMAIN to your domain
./setup.sh                       # generates ./config/, builds + bootstraps + starts both

See the theta-env README for the full first-run flow, DNS/port requirements, and backups.

2. Standalone, in Docker

The all-in-one image bundles the app, OpenLDAP, and Redis. Copy the example secrets file, fill in your values, and build:

git clone https://github.com/theta42/sso-manager-node.git
cd sso-manager-node
mkdir -p config && chmod 700 config
cp secrets.js.example config/sso-secrets.js
$EDITOR config/sso-secrets.js     # set ldap.bindPassword, oauth.jwtSecret, ...
docker compose up -d --build

The web UI comes up at http://localhost:3001. To kick the tires with no config file at all, the entrypoint falls back to safe defaults (dc=example,dc=com, admin password admin, an auto-generated JWT) — fine for a local test, not for production.

Your domain is entered once, as the LDAP base DN (stack.ldapBaseDn); the other LDAP DNs and the OAuth issuer derive from it and must stay consistent. See DEPLOYMENT.md for the full config reference, the app_* env vars, LDAPS/TLS, and backups.

3. Bare metal on Debian/Ubuntu

An automated installer installs Node.js, Redis, and (on first run) OpenLDAP — configuring the directory (modules, overlays, schema, the SSO groups) and seeding /etc/sso-manager/secrets.js with a generated admin password and JWT secret — then deploys the app to /opt/theta42/sso-manager and starts a systemd service:

wget -O - https://raw.githubusercontent.com/theta42/sso-manager-node/master/install.sh | sudo bash

That's it — LDAP and the app are both live afterward. Edit /etc/sso-manager/secrets.js (org name, SMTP, a non-default base DN, ...) and restart the service to customize. It's idempotent and safe to re-run — re-running it updates the app in place (never touching LDAP or the secrets file again) and prints the version you're updating from and to (e.g. Updated v1.1.13 -> v1.1.14), or Already up to date if there's nothing new. Full details, including env var overrides (LDAP_BASE_DN, SKIP_LDAP, ...), in DEPLOYMENT.md under Method 2: Bare metal.

Architecture

┌─────────────┐
│  Browser /  │
│  OIDC apps  │
└──────┬──────┘
       │ HTTP/HTTPS
       ▼
┌────────────────────────┐      ┌─────────────┐
│  Express SSO Manager   │◄────►│   Redis     │
│  - OIDC provider       │      │ - sessions  │
│  - web UI (:3001)      │      │ - models    │
│  - management API      │      └─────────────┘
└────────┬───────────────┘
         │ ldapi/ldap (localhost)
         ▼
┌────────────────────────┐
│  OpenLDAP (slapd)      │
│  - users / groups      │
│  - LDAPS :636          │─── Linux hosts + LDAP apps bind directly
│  - StartTLS :389       │
└────────────────────────┘

Documentation

The nitty LDAP details (overlay setup, the custom theta42Person schema, the required groups, LDAPS/TLS, direct-bind service accounts) live in:

  • DEPLOYMENT.md — Docker + bare metal, the config layers, the app_* env reference, LDAPS/TLS, backups, troubleshooting.
  • API.md — the management API.
  • docs/ (GitHub Pages) — the same content broken into deployment, configuration, OAuth/OIDC, and LDAP.
  • CHANGELOG.md — what changed in each release.
  • All of the above is also readable from the running app itself at /docs — no internet access required.

If you are pointing the app at your own existing LDAP server, see LDAP requirements in DEPLOYMENT.md — the directory needs the pw-sha2, ppolicy, memberof, and refint modules plus a small custom schema. The bundled Docker image and install.sh set all of that up for you. Required groups: app_sso_admin (full admin), app_sso_oauth_admin (manage OAuth clients only), app_sso_invite (invitation management) — see DEPLOYMENT.md for the full setup.

Development

cd nodejs
npm install
npm run dev      # nodemon auto-reload
npm test         # jest test suite

License

MIT — see LICENSE.

S
Description
LDAP GUI and API manager for use with SSO.
Readme MIT 58 MiB
Languages
JavaScript 63.6%
EJS 31%
Shell 5.2%
CSS 0.2%