Commit Graph

201 Commits

Author SHA1 Message Date
wmantly d8b6f6e7a3 app.api.delete: accept the (url, data, callback) form formAJAX uses
formAJAX always passes the serialized form as the second argument, so a
DELETE-method form (proxy's host/DNS rows) landed its callback in the
data slot and never ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:15:07 -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 b076498219 Merge pull request #103 from theta42/release/v1.4.0
Release 1.4.0
2026-07-25 16:41:49 -04:00
wmantly fc0d9104d0 Release 1.4.0: shared @simpleworkjs/* packages; fix discovery envelope drift + client_secret_hash leak
Rewire onto @simpleworkjs/directory-schema, /ldap, and /app-stack. The
directory discovery API now returns the {results} envelope via explicit
/resources, /resources/:slug, /graph, /me handlers and routes every read
through projectResource/projectResources, which unconditionally strips
client_secret_hash (and any /secret|password|privatekey/i key) and reduces
metadata to a public allowlist for non-admins — closing the leak where the ORM
serialized metadata wholesale. The dead routes/api_discovery.js (mounted after
the 404 catcher) is removed; ?group= now returns 200 instead of 404. user_ldap
+ group_ldap take escapeFilter/escapeDN + makeClient/withClient from the shared
ldap package (posix/write-side stays app-local; cert validation unchanged).
build_info unified to {buildVersion,buildHash,buildYear}; ldapts ^8.1.8. New
tests/discovery.test.js locks in the envelope + no-secrets guarantees. Lockfile
regenerated from the registry (no file:/link:).

Co-Authored-By: Claude <noreply@anthropic.com>
v1.4.0
2026-07-25 16:39:10 -04:00
wmantly 82da47cef7 Merge pull request #102 from theta42/docs/jump-host-xref
docs: cross-link the SSH jump host as a directory consumer
2026-07-23 16:23:54 -04:00
wmantly 39779f51dc docs: cross-link the SSH jump host as a directory consumer
- directory.md: new "Consumers of the directory" section explaining how
  the jump host reads the inventory (groups x host resources) to route
  SSH, and pointing at directory_spec.md §9 for planned consumers
- index.md: mention the jump host under Directory & Inventory and in
  Related projects

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:18:31 -04:00
wmantly d9a3cb6044 Merge pull request #101 from theta42/release/v1.3.2
Release 1.3.2 — fix OAuth client API client_id serialization (bootstrap-breaking)
v1.3.2
2026-07-23 16:09:37 -04:00
wmantly 0ee6825a01 fix: OAuth client API returned client_id: undefined; unknown id 500'd
The ORM Model.toJSON() serializes only schema fields, so the mapped
client_id/scopes/redirect_uris/... that OAuthClient.get() attaches to
the wrapped Resource were stripped from GET /api/oauth/client[/:id]
responses. client_id came back undefined; the theta-env bootstrap then
POSTed /api/oauth/client/undefined/rotate and got a 500, aborting stack
bring-up whenever proxy-secrets.js lacked a usable secret.

- OAuthClient.get() now emits an explicit public toJSON (client_id, name,
  slug, scopes, redirect_uris, allowed_groups, token_lifetime, is_valid),
  deliberately omitting client_secret_hash so it can't leak over the API.
- OAuthClient.get() null-guards Resource.get() (which returns null, not
  throws) and returns a clean 404 for an unknown/undefined id instead of
  crashing on r.kind.
- Regression tests: list/get expose client_id + hide the secret hash, the
  list-then-rotate bootstrap path, and unknown-id -> 4xx not 500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:06:55 -04:00
wmantly 14b6ed5ae0 Merge pull request #97 from theta42/spec/directory-consumers
Directory spec: readiness review of the five planned consumers
2026-07-23 03:37:00 -04:00
wmantly fd98854628 spec: readiness review of the five planned directory consumers
Records what each planned consumer of the directory data needs — end-user
catalog + access requests, SSH jump host, firewall rule generation/drift
testing, local DNS/mDNS, and host access control — what the current
model/API already covers, and the concrete gaps.

Verdict: the graph model is sufficient for all five. Gaps are one new
model (AccessRequest), service-token auth for the read API, documented
metadata conventions (portMappings, sshPort, dnsNames, icon, ...), and
cheap change detection (updated_on/etag). Also flags a real issue found
while auditing: /api/discovery/resources exposes full metadata —
including OAuth client_secret_hash — to any authenticated user; a
metadata privacy projection tops the work list.

Also updates the spec's stale status line (it still said "no code yet").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 03:33:19 -04:00
wmantly 3babf18fe4 Merge pull request #96 from theta42/release/v1.3.1
Release 1.3.1
v1.3.1
2026-07-23 03:29:01 -04:00
wmantly d78f1dfabf Release 1.3.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 03:24:23 -04:00
wmantly c3c206b830 Merge pull request #95 from theta42/docs/directory-doc
Surface the Directory documentation — register in-app and link
2026-07-23 03:20:05 -04:00
wmantly 0a21dce0d7 docs: surface the Directory doc — register in-app, link from UI and site
docs/directory.md existed but was orphaned: not in the /docs registry,
not linked anywhere. Now:

- registered as /docs/directory ("Directory & Inventory")
- help icon on the Directory page header links to it (same pattern as
  users/groups/profile pages)
- linked from the docs site index feature list
- extended with the shared slug conventions (site_<name>, host_<hostname>),
  the automatic registration story (theta-env stack seeding, ldap-client
  Linux host enrollment), and the admin + read-only API surface (the
  read-only graph routes live at /api/discovery, not /api/directory).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 03:17:50 -04:00
wmantly 5940880d9b Merge pull request #94 from theta42/docs/ldap-not-legacy
docs: direct LDAP binds are first-class, not "legacy"
2026-07-23 02:57:20 -04:00
wmantly 17fcf2fed0 docs: direct LDAP binds are first-class, not "legacy"
Linux hosts are a primary consumer of the directory — PAM/SSSD login,
LDAP-backed sudo rules (sudoRole), and SSH public keys (openssh-lpk) —
which is exactly what the custom schemas exist for. Describe LDAPS /
StartTLS consumers as "Linux hosts and LDAP-native apps" instead of
"legacy apps" across README, DEPLOYMENT, docs, and the Dockerfile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:55:11 -04:00
wmantly ec76054e41 Merge pull request #93 from theta42/release/v1.3.0
Release 1.3.0
v1.3.0
2026-07-23 02:25:27 -04:00
wmantly 5c0fc4f016 Release 1.3.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:22:46 -04:00
wmantly 43dae2a3eb Merge branch 'simpleworkjs' (superseded by tested ORM-port fixes on this branch)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:21:50 -04:00
wmantly 20c0a48199 ui: remove mobile phone field from user form
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:20:59 -04:00
wmantly 12da7140c2 test: dockerized test suite (openldap + redis + test-runner)
docker-compose.test.yml spins up the all-in-one OpenLDAP image, a
standalone Redis, and a test-runner that seeds the test user and runs
jest against them. globalSetup honors REDIS_URL; tests/setup.js
initializes the ORM and flushes test Redis keys before the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:20:59 -04:00
wmantly dfd5f46095 feat: OAuth client management API at /api/oauth/client
CRUD + secret rotation for OAuth clients (app_sso_oauth_admin group),
backed by the Resource model. Normalizes form-style string inputs
(newline-separated redirect_uris/allowed_groups, space-separated
scopes, bracketed token_lifetime fields).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:20:59 -04:00
wmantly 5dcc75195c fix: complete ORM port — token/oauth-client API mismatches, use published orm 0.2.8
- Use published @simpleworkjs/orm ^0.2.8 (fixes redis adapter write path)
  and model-redis ^1.6.0 instead of a local file: link that broke docker
  npm ci with a misleading "no lockfile" error.
- OtpToken.issue/verify: replace nonexistent find()/listDetail() with
  list({where}).
- routes/auth.js: ImpersonationToken.listDetail() -> list({where}).
- routes/token.js: drop listDetail() call; 404 on missing token instead
  of returning {results: null} with 200 (orm get() returns null, does
  not throw like model-redis Table.get did).
- OAuthClient: Resource has no is_valid column, so every client read as
  disabled and all /oauth/authorize requests 400'd — validity now lives
  in metadata (absent = valid). Also generate a unique slug on create
  (Resource.slug is required+unique) and use Resource.get() for lookup.
- User.login: 401 cleanly when neither uid nor username is supplied.
- models/index.js: log ORM init and surface init failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:20:49 -04:00
wmantly 12a99b550c Bump @simpleworkjs/orm to 0.2.8 to fix redis bugs 2026-07-22 23:24:41 -04:00
wmantly 4ce5a5f492 Monkey-patch RedisAdapter 2026-07-22 22:41:57 -04:00
wmantly c84141b2f3 Restore package-lock.json 2026-07-22 22:37:51 -04:00
wmantly f76d93d840 Fix User.login credentials check and flush Redis before tests 2026-07-22 22:37:45 -04:00
wmantly e910a492ba Fix Token API compatibility 2026-07-22 22:30:26 -04:00
wmantly 1ef868e23c Fix ORM API usages in Token subclasses 2026-07-22 22:26:26 -04:00
wmantly e11e39c23a fix: initialize ORM in tests 2026-07-22 22:17:23 -04:00
wmantly b91089ad4c fix: use published @simpleworkjs/orm instead of local path 2026-07-22 22:11:11 -04:00
wmantly 95ae50a924 fix: generate package-lock.json with node 20 for CI 2026-07-22 22:06:31 -04:00
wmantly 0076784fae fix: sync package-lock.json version 2026-07-22 22:03:53 -04:00
wmantly c4d7a1a8e9 feat: actionable metrics, LDAP log parsing, UI updates v1.2.1 2026-07-22 21:58:06 -04:00
wmantly a100f755ce Merge pull request #91 from theta42/release/v1.1.18
Release v1.1.18: fix Sites page crash, refresh screenshots
v1.1.18
2026-07-21 02:22:24 -04:00
wmantly 81ad538e50 Release 1.1.18: fix Sites page crash, refresh screenshots
The Sites & Replication page (added in the prior multi-master LDAP
release) 500'd on every load: views/sites.ejs included nonexistent
partials 'header'/'footer' instead of this app's actual 'top'/'bottom'.
Fixed to match every other view.

Also refreshed all README screenshots against the current UI and added
a new Sites & Replication screenshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 02:03:55 -04:00
wmantly 6ce36b4a14 Merge pull request #90 from theta42/feature/multi-master-ldap-location
feat: N-Way Multi-Master LDAP & User Location
v1.2.0
2026-07-21 00:14:22 -04:00
wmantly cda76d3889 docs: Add Why and When for replication and update README features 2026-07-21 00:10:25 -04:00
wmantly 2c11226793 feat: Add documentation and Sites status dashboard page 2026-07-21 00:06:59 -04:00
wmantly 80d88b083c feat: N-Way Multi-Master LDAP replication and Location property 2026-07-20 23:56:13 -04:00
wmantly b4fa824609 feat: configurable LDAPS hostname (ldapsHost/ldapsPort) and extensive docs (#89)
Add conf.ldap.ldapsHost / conf.ldap.ldapsPort so the /integrations page
can advertise an internal-only LDAPS hostname separate from the public
OAuth issuer. This avoids forcing admins to port-forward 636 publicly.

- routes/index.js derives LDAPS URL from ldapsHost/ldapsPort with issuer fallback
- integrations.ejs adds a contextual help panel explaining TLS hostname
  validation, the public-issuer default, and recommended internal-DNS /
  Docker-internal alternatives
- conf/base.js, secrets.js.example, DEPLOYMENT.md, docs/configuration.md,
  and docs/ldap.md document and expose the new options
- Add tests/integrations.test.js for default and custom ldapsHost behavior
- Bump version to 1.1.17

Co-authored-by: Claude <noreply@anthropic.com>
v1.1.17
2026-07-19 01:13:43 -04:00
wmantly 5a8030fd7d chore(release): public-release readiness and security fixes for 1.1.16
🤖 Generated with [Claude Code](https://claude.com/claude-code)
v1.1.16
2026-07-18 23:14:38 -04:00
wmantly cf80c966eb security: swap sanitizer to xss and harden logging
- Replace isomorphic-dompurify with xss to avoid ESM-only transitive
  dependencies (jsdom/htmlparser2) that break the existing Jest test suite.
- Sanitize rendered docs and Terms-of-Service HTML via xss() in routes/docs.js
  and routes/index.js.
- Remove full-object new-user logging from models/user_ldap.js and reduce
  login-path error output to error.name/error.message only.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 23:03:33 -04:00
wmantly 07819a6254 security: sanitize markdown output and reduce PII logging
- Add isomorphic-dompurify to sanitize rendered docs HTML and Terms of Service
- Remove addLdapUser full-object logging that included password hashes
- Log only error name/message on auth/login failures instead of full LDAP error objects

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:56:35 -04:00
wmantly 1b3e842006 ci: set app_oauth__jwtSecret for test runs
routes/oauth.js now validates jwtSecret at module load time, so CI must
provide a non-placeholder value for the test runner.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:16:30 -04:00
wmantly efe3e514b0 chore(release): public-release readiness and security fixes for 1.1.16
Security:
- Escape user-supplied values in LDAP filters and DNs (group_ldap.js, user_ldap.js)
- Replace Math.random() token/UUID/OTP generation with crypto.randomUUID / crypto.randomInt
- Refuse startup when oauth.jwtSecret is missing or placeholder

Fixes:
- Correct from-address template rendering in email.js

Packaging:
- Remove private flag and bump version to 1.1.16

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:08:11 -04:00
wmantly 37f2ece172 Merge pull request #87 from theta42/release-1.1.15
Bump version to 1.1.15
v1.1.15
2026-07-18 01:20:13 -04:00
wmantly b77704089b Bump version to 1.1.15; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:18:05 -04:00
wmantly 114d8c86ca Merge pull request #86 from theta42/install-script-rework
Rewrite install.sh as a git-clone installer, add a one-line install
2026-07-18 01:17:18 -04:00
wmantly 3e87ad86ab Rewrite install.sh as a git-clone installer, add a one-line install
Replaces the old flag-driven, copy-based installer with an idempotent
git-clone-and-symlink installer matching theta42/proxy's ops/install.sh
pattern, so `wget -O - .../install.sh | sudo bash` works the same way
for both apps:

- Installs to /opt/theta42/sso-manager (was /opt/sso-manager, and the
  repo had to already be checked out locally -- now it clones itself).
- First run only: bootstraps OpenLDAP (modules, overlays, schema,
  directory tree, SSO groups -- ops/ldap-setup.sh) with a generated
  admin password + JWT secret, and seeds /etc/sso-manager/secrets.js
  (was /opt/sso-manager/conf/secrets.js, hand-filled from CLI flags).
  Later runs never touch LDAP or the secrets file again.
- ops/systemd/sso-manager.service now points at the new install path
  and sets CONF_SECRETS=/etc/sso-manager/secrets.js (requires
  @simpleworkjs/conf >= 1.2.0, already the pinned version) instead of
  the app needing a config file inside the repo checkout.
- Prints the version it's updating from/to (or "Already up to date")
  on every run, instead of updating silently.

Two real bugs found and fixed while testing this end-to-end in a clean
container:
- The debconf `slapd/domain` value was computed as
  `${LDAP_BASE_DN#dc=}` ("example,dc=com" for "dc=example,dc=com")
  instead of a proper dotted domain -- slapd's postinst hangs
  indefinitely on a malformed domain instead of failing cleanly.
  Fixed to derive it the same way the secrets file already did
  ("example.com").
- ops/ldap-setup.sh's ppolicy-overlay checks used an LDAP substring
  filter, `(olcOverlay=*ppolicy*)`, against an attribute that doesn't
  support substring matching -- it silently matched nothing even when
  the overlay was correctly configured (stored as "{0}ppolicy"),
  so the final verification always reported failure and `set -e`
  aborted the installer after LDAP was set up but before the app was.
  Fixed to filter on `(objectClass=olcOverlayConfig)` and let the
  existing DN-based grep narrow it down, matching the pattern already
  used by every other check in that script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:13:51 -04:00