Compare commits

..

14 Commits

Author SHA1 Message Date
wmantly 71e8c09e8f Merge pull request #94 from theta42/release/v1.6.0
Release 1.6.0: bump jump-host to v1.4.0 (standalone mode)
2026-07-26 21:40:19 -04:00
wmantly acc61a4c3d Bump jump-host to v1.4.0 (standalone mode)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 21:39:57 -04:00
wmantly 67f62276e7 Merge pull request #93 from theta42/docs/screenshots-refresh
docs: refresh top-level screenshots, add jump-host dashboard
2026-07-26 16:32:06 -04:00
wmantly 6ef12408df docs: refresh top-level screenshots, add jump-host dashboard
sso-dashboard.png and proxy-hosts.png still showed the pre-unification
nav; refresh with the current shared shell and add a jump-host dashboard
screenshot alongside them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 16:29:51 -04:00
wmantly c3d232f7cc Merge pull request #92 from theta42/release/v1.5.0
Release 1.5.0: unified front-end UI shell across the three apps
2026-07-26 00:33:22 -04:00
wmantly 285cc4fbef Release 1.5.0: bump submodules to the unified-UI-shell tags
sso-manager-node v1.5.0, proxy v1.4.0, jump-host v1.3.0 — the three apps
now share one byte-identical front-end shell (top.ejs, bottom.ejs,
app-base.js) with per-app values in each repo's utils/ui.js, one nav-gating
model driven by /api/user/me, and jQuery 4 / EJS 3 across the board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:32:37 -04:00
wmantly 5299556057 Merge pull request #91 from theta42/release/v1.4.0
Release 1.4.0
2026-07-25 16:44:08 -04:00
wmantly cc03b3758c Release 1.4.0: bump submodules to the unified-release tags
sso-manager-node -> v1.4.0, proxy -> v1.3.0, jump-host -> v1.2.0. The three
apps now share @simpleworkjs/{oidc-client,directory-schema,ldap,app-stack}
(1.0.0, published under the simpleworkjs org) instead of byte-identical forks,
and the SSO directory discovery API no longer leaks client_secret_hash and
returns the {results} envelope (fixing jump-host bridging). No setup.sh change:
the new deps resolve from npm in each app's image build (npm ci stays clean).
CHANGELOG embeds the full per-app release notes. UI chrome unification is
deferred to a browser-verified session (see UI_UNIFICATION_HANDOFF.md).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-25 16:43:08 -04:00
wmantly 2d0496cfda Merge pull request #90 from theta42/release/v1.3.7
Release 1.3.7
2026-07-23 21:02:46 -04:00
wmantly 48df638ddd Release 1.3.7
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:02:22 -04:00
wmantly d6d2c7144a Merge pull request #89 from theta42/feature/jump-oidc
Provision jump-host web-UI SSO login (OIDC client)
2026-07-23 21:02:06 -04:00
wmantly 67374dc914 feat: provision jump-host web-UI SSO login (OIDC client); bump to v1.1.0
The jump host's web UI now authenticates via OIDC + a local admin
(jump-host v1.1.0). Wire that in the bundle:

- bootstrap mints a dedicated 'theta-jump' OAuth client (redirect
  https://<JUMP_HOST>/api/auth/oidc/callback) and writes a full oidc
  block + generated local admin password into config/jump-secrets.js,
  mirroring the proxy's OIDC provisioning
- an existing pre-OIDC jump-secrets.js (API token but no OIDC client) is
  regenerated so upgraders get SSO login
- bump jump-host submodule v1.0.x -> v1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:01:36 -04:00
wmantly a3b41c6775 Merge pull request #88 from theta42/docs/jump-host
docs: add the optional SSH jump host to the Pages site
2026-07-23 16:23:35 -04:00
wmantly b25fb56a0d docs: add the optional SSH jump host to the theta-env Pages site
- index: mention the jump host as an optional third component (intro,
  What-you-get, Related projects)
- quickstart: CFG_JUMP_HOST_ENABLED / CFG_JUMP_HOST / JUMP_SSH_PORT
- reframe "legacy LDAP clients" -> "direct LDAP clients" (Linux hosts are
  first-class consumers)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:17:14 -04:00
11 changed files with 269 additions and 20 deletions
+193 -1
View File
@@ -10,6 +10,197 @@ for what changed inside the apps it composes.
## [Unreleased] ## [Unreleased]
## [1.6.0] - 2026-07-26
### Bumped
- jump-host -> [v1.4.0](https://github.com/theta42/jump-host/releases/tag/v1.4.0)
Jump-host gains **standalone mode**: it can now run with no LDAP directory and
no SSO Manager at all, storing users and hosts itself via
`@simpleworkjs/orm` (Sequelize; SQLite by default, any Sequelize-supported
dialect). This is an app-internal capability, opt-in via
`standalone.enabled` in jump-host's own config — the bundled theta-env stack
is unaffected and continues to wire jump-host to the shared LDAP directory
and SSO Manager as before. Two bugs were also fixed in jump-host's SSH
server: an ephemeral listen port (`0`) was silently overridden back to the
default, and session listeners could miss a client's immediate `exec`/`shell`
request.
No `setup.sh`, compose, or config change on the theta-env side.
## [1.5.0] - 2026-07-26
### Bumped
- sso-manager-node -> [v1.5.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.5.0)
- proxy -> [v1.4.0](https://github.com/theta42/proxy/releases/tag/v1.4.0)
- jump-host -> [v1.3.0](https://github.com/theta42/jump-host/releases/tag/v1.3.0)
This release finishes the UI half of the unification that 1.4.0 deferred: the
three apps now share one front-end shell. `views/top.ejs`, `views/bottom.ejs`
and `public/lib/js/app-base.js` are byte-identical across sso-manager-node,
proxy and jump-host, and everything per-app moved into each repo's new
`nodejs/utils/ui.js` (nav items and the groups that may see them, footer links,
favicon, profile/logout targets, update-banner on/off). Nav gating is one model
everywhere — the shell reveals `.group-required-<cn>` from `GET /api/user/me`,
normalising sso's LDAP DNs and the OIDC clients' group CNs to the same shape,
with the clients' `isAdmin` flag exposed as a synthetic `admin` group. jQuery is
4.0.0 and EJS 3.1.10 in all three.
Five client-side bugs were fixed along the way, including two that broke real
flows: `app.api.delete` ignored the callback that `formAJAX` passes (so
DELETE-method forms — the proxy's host and DNS delete buttons — never refreshed),
and the login page threw on every logged-out visit while revealing its card.
No `setup.sh`, compose or config change: this is app-internal UI work. Verified
by driving a full stack of all three apps in a browser — every page renders
console-clean, nav gating is correct per role, and the OIDC login round trip
completes on both OIDC clients.
sso-manager-node 1.5.0:
### Changed
- **Unified the front-end UI shell across the three theta42 apps.** `views/top.ejs`, `views/bottom.ejs` and `public/lib/js/app-base.js` are now byte-identical in sso-manager-node, proxy and jump-host, so the apps look and behave the same and a shell change lands in one edit per repo instead of three divergent ones. Everything that differs between the apps moved into a new `nodejs/utils/ui.js`, exposed to every render as `ui` via `app.locals`: nav items and the groups that may see them, footer repo/license/docs/Terms links, favicon, the profile and post-logout targets, and whether the update banner exists at all.
- **One nav-gating model everywhere.** `app-base.js` reveals `.group-required-<cn>` elements for each group the current user is in, read from `GET /api/user/me`. sso-manager-node reports LDAP DNs in `memberOf` and the OIDC clients report CNs in `groups`; both normalise to CNs client-side, and the clients' effective-rights `isAdmin` flag is exposed as a synthetic `admin` group — so one gating model covers a group-based provider and boolean-admin clients without either app learning the other's response shape.
- **`GET /api/user/me` is fetched once per page load and cached** (`app.auth.loadUser`). The nav, per-view `forceLogin` and every group-gated element read that one promise instead of issuing their own request.
- `app.auth.isLoggedIn` is dual-mode: it returns a Promise **and** invokes an optional node-style callback, so the async and callback call styles both work against one shared `top.ejs`.
- `app.auth.forceLogin` no longer uses `$.holdReady` (removed in jQuery 4). An unauthenticated user is redirected to `/login?redirect=<path>`; group requirements are still enforced, and `logOut` now only clears the session, leaving the destination to the caller (`ui.logoutRedirect`).
- Dependency alignment across all three apps: `jquery` `^4.0.0` and `ejs` `^3.1.10`.
### Fixed
- **`app.api.delete` dropped its callback when called by `formAJAX`.** `formAJAX` always passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran. `delete` now accepts both `(url, callback)` and `(url, data, callback)`.
- **`app.api.post`/`put` referenced an undefined `callback2`** and threw when handed a non-function callback. Both are now dual-mode Promise/callback.
- **The login page's "reveal the card once we know you're logged out" branch threw** (`Cannot read properties of null`) whenever the logged-in check answered before the parser reached that element — which it always did without a stored token. It now runs on DOM ready.
- **`logInRedirect` on the legacy `/login/<path>` form kept only the path.** The OIDC provider routes an unauthenticated authorization request through `/login/oauth/authorize?client_id=…&state=…`; dropping the query there loses the entire authorization request. The suffix form now preserves its query string.
### Fixed (sso-manager-node)
- `public/lib/js/val.js` shadowed `message` with `let` inside `validateField`, so a custom rule's return value never reached `validateMessage` and the caller always saw the generic length message. Resolved by adopting the shared validator, which also brings the `target`/`hostname` rules and the real password policy (>= 8 chars, and either 12+ or 3 of 4 character classes) to this app.
- `public/js/app.js` used `$.isFunction`, removed in jQuery 4.
### Added (sso-manager-node)
- `GET /api/user/me` now also reports `isAdmin` (membership in `app_sso_admin`), the single effective-rights flag the shared UI shell gates the update banner on. Group-level gating still reads `memberOf`.
### Verified
- Browser-verified against a full theta-env stack (sso-manager + proxy + jump-host): every top-level page renders with a clean console; nav gating is correct for admin and non-admin; `forceLogin`'s onboarding and group gates fire; `val.js` blocks a weak password and accepts a strong one through a real form submit; the DELETE-method forms work; and the OIDC login round trip (authorize with PKCE -> login -> consent -> callback -> token fragment) completes on both OIDC clients.
proxy 1.4.0:
### Changed
- **Unified the front-end UI shell across the three theta42 apps.** `views/top.ejs`, `views/bottom.ejs` and `public/lib/js/app-base.js` are now byte-identical in sso-manager-node, proxy and jump-host, so the apps look and behave the same and a shell change lands in one edit per repo instead of three divergent ones. Everything that differs between the apps moved into a new `nodejs/utils/ui.js`, exposed to every render as `ui` via `app.locals`: nav items and the groups that may see them, footer repo/license/docs/Terms links, favicon, the profile and post-logout targets, and whether the update banner exists at all.
- **One nav-gating model everywhere.** `app-base.js` reveals `.group-required-<cn>` elements for each group the current user is in, read from `GET /api/user/me`. sso-manager-node reports LDAP DNs in `memberOf` and the OIDC clients report CNs in `groups`; both normalise to CNs client-side, and the clients' effective-rights `isAdmin` flag is exposed as a synthetic `admin` group — so one gating model covers a group-based provider and boolean-admin clients without either app learning the other's response shape.
- **`GET /api/user/me` is fetched once per page load and cached** (`app.auth.loadUser`). The nav, per-view `forceLogin` and every group-gated element read that one promise instead of issuing their own request.
- `app.auth.isLoggedIn` is dual-mode: it returns a Promise **and** invokes an optional node-style callback, so the async and callback call styles both work against one shared `top.ejs`.
- `app.auth.forceLogin` no longer uses `$.holdReady` (removed in jQuery 4). An unauthenticated user is redirected to `/login?redirect=<path>`; group requirements are still enforced, and `logOut` now only clears the session, leaving the destination to the caller (`ui.logoutRedirect`).
- Dependency alignment across all three apps: `jquery` `^4.0.0` and `ejs` `^3.1.10`.
### Fixed
- **`app.api.delete` dropped its callback when called by `formAJAX`.** `formAJAX` always passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran. `delete` now accepts both `(url, callback)` and `(url, data, callback)`.
- **`app.api.post`/`put` referenced an undefined `callback2`** and threw when handed a non-function callback. Both are now dual-mode Promise/callback.
- **The login page's "reveal the card once we know you're logged out" branch threw** (`Cannot read properties of null`) whenever the logged-in check answered before the parser reached that element — which it always did without a stored token. It now runs on DOM ready.
- **`logInRedirect` on the legacy `/login/<path>` form kept only the path.** The OIDC provider routes an unauthenticated authorization request through `/login/oauth/authorize?client_id=…&state=…`; dropping the query there loses the entire authorization request. The suffix form now preserves its query string.
### Added
- `.group-required { display: none }` in `public/css/styles.css`, the base rule the shared gating model reveals against.
- Admin-only nav items lost their inline `display: none` in favour of that class, and the brand link points at `/` instead of `#`.
### Verified
- Browser-verified against a full theta-env stack (sso-manager + proxy + jump-host): every top-level page renders with a clean console; nav gating is correct for admin and non-admin; `forceLogin`'s onboarding and group gates fire; `val.js` blocks a weak password and accepts a strong one through a real form submit; the DELETE-method forms work; and the OIDC login round trip (authorize with PKCE -> login -> consent -> callback -> token fragment) completes on both OIDC clients.
jump-host 1.3.0:
### Changed
- **Unified the front-end UI shell across the three theta42 apps.** `views/top.ejs`, `views/bottom.ejs` and `public/lib/js/app-base.js` are now byte-identical in sso-manager-node, proxy and jump-host, so the apps look and behave the same and a shell change lands in one edit per repo instead of three divergent ones. Everything that differs between the apps moved into a new `nodejs/utils/ui.js`, exposed to every render as `ui` via `app.locals`: nav items and the groups that may see them, footer repo/license/docs/Terms links, favicon, the profile and post-logout targets, and whether the update banner exists at all.
- **One nav-gating model everywhere.** `app-base.js` reveals `.group-required-<cn>` elements for each group the current user is in, read from `GET /api/user/me`. sso-manager-node reports LDAP DNs in `memberOf` and the OIDC clients report CNs in `groups`; both normalise to CNs client-side, and the clients' effective-rights `isAdmin` flag is exposed as a synthetic `admin` group — so one gating model covers a group-based provider and boolean-admin clients without either app learning the other's response shape.
- **`GET /api/user/me` is fetched once per page load and cached** (`app.auth.loadUser`). The nav, per-view `forceLogin` and every group-gated element read that one promise instead of issuing their own request.
- `app.auth.isLoggedIn` is dual-mode: it returns a Promise **and** invokes an optional node-style callback, so the async and callback call styles both work against one shared `top.ejs`.
- `app.auth.forceLogin` no longer uses `$.holdReady` (removed in jQuery 4). An unauthenticated user is redirected to `/login?redirect=<path>`; group requirements are still enforced, and `logOut` now only clears the session, leaving the destination to the caller (`ui.logoutRedirect`).
- Dependency alignment across all three apps: `jquery` `^4.0.0` and `ejs` `^3.1.10`.
### Fixed
- **`app.api.delete` dropped its callback when called by `formAJAX`.** `formAJAX` always passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran. `delete` now accepts both `(url, callback)` and `(url, data, callback)`.
- **`app.api.post`/`put` referenced an undefined `callback2`** and threw when handed a non-function callback. Both are now dual-mode Promise/callback.
- **The login page's "reveal the card once we know you're logged out" branch threw** (`Cannot read properties of null`) whenever the logged-in check answered before the parser reached that element — which it always did without a stored token. It now runs on DOM ready.
- **`logInRedirect` on the legacy `/login/<path>` form kept only the path.** The OIDC provider routes an unauthenticated authorization request through `/login/oauth/authorize?client_id=…&state=…`; dropping the query there loses the entire authorization request. The suffix form now preserves its query string.
### Added
- `.group-required { display: none }` in `public/css/styles.css`, the base rule the shared gating model reveals against.
- `#spa-shell` dropped its inline `margin-top`; `styles.css` already sets it and the shared shell adjusts it when a banner is shown.
### Verified
- Browser-verified against a full theta-env stack (sso-manager + proxy + jump-host): every top-level page renders with a clean console; nav gating is correct for admin and non-admin; `forceLogin`'s onboarding and group gates fire; `val.js` blocks a weak password and accepts a strong one through a real form submit; the DELETE-method forms work; and the OIDC login round trip (authorize with PKCE -> login -> consent -> callback -> token fragment) completes on both OIDC clients.
## [1.4.0] - 2026-07-25
### Bumped
- sso-manager-node -> [v1.4.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.4.0)
- proxy -> [v1.3.0](https://github.com/theta42/proxy/releases/tag/v1.3.0)
- jump-host -> [v1.2.0](https://github.com/theta42/jump-host/releases/tag/v1.2.0)
This release unifies the three theta42 apps onto shared `@simpleworkjs/*` packages
(`oidc-client`, `directory-schema`, `ldap`, `app-stack` — published under the
simpleworkjs org at 1.0.0), replacing each app's byte-identical forks of the same
code so they share one codebase and API schema. It also fixes a security
regression in the SSO directory discovery API (OAuth `client_secret_hash` leaked
to every authenticated caller) and the envelope drift that broke jump-host
bridging. The shared UI chrome (`top.ejs`/`bottom.ejs`, `app-base.js`, `val.js`)
is intentionally **not** unified in this release — that work is deferred to a
browser-verified session; see `UI_UNIFICATION_HANDOFF.md`. No `setup.sh` change:
the new `@simpleworkjs/*` deps resolve from npm inside each app's image build
(`npm ci` stays clean; no `file:`/`link:`).
sso-manager-node 1.4.0:
### Security
- **The directory discovery API leaked OAuth `client_secret_hash` (and any secret-ish metadata key) to every authenticated caller.** `Resource` doesn't override `toJSON`, so the ORM serialized `metadata` wholesale — including the `client_secret_hash` stored on `kind:'oauth'` resources — across `GET /api/discovery/resources`, `/graph`, `/me`, `/resources/:slug`, and the directory-admin `GET /api/directory-admin/resources`. Every discovery read endpoint and the admin list now route through `projectResource`/`projectResources` from `@simpleworkjs/directory-schema`, which unconditionally strips secret keys (anything matching `/secret|password|privatekey/i`, including `client_secret_hash`) and, for non-directory-admins, reduces metadata to a public allowlist. Admins never receive `client_secret_hash` either.
### Fixed
- **Directory discovery envelope drift.** `routes/discovery.js` (the `autoRouter(Resource)` mounted live at `app.js:87`) returned **bare arrays**, not the `{ results: [...] }` envelope the directory contract specifies — so jump-host's `data.results || []` collapsed every per-group query to `[]` and no user could bridge. Discovery is now served by explicit `/resources`, `/resources/:slug`, `/graph`, `/me` handlers that all return the `{ results }` envelope. The dead `routes/api_discovery.js` (mounted at `app.js:112`, *after* the 404 catcher) and its mount were removed.
- `GET /api/discovery/resources?group=<cn>` now returns 200 with `{ results: [...] }` instead of 404.
### Added
- `@simpleworkjs/directory-schema` — the directory contract: the `kind` enum, `Resource`/`ResourceEdge`/`ResourceGroup` field defs, the `{ results }` envelope, the security projection (`projectResource`/`projectResources`/`isDirectoryAdmin`), and the discovery client. `models/resource.js` imports the field defs; the discovery + directory-admin routes use the projection.
- `@simpleworkjs/ldap``models/user_ldap.js` and `models/group_ldap.js` now take `escapeFilter`/`escapeDN` and `makeClient`/`withClient` from the shared package (via local wrappers that pass `conf`); sso keeps its rich `User.get`/`Group.get`/`User.login`/`User.addSSHkey` (posix/write-side stays app-local). sso's `makeClient` passes no `tlsOptions`, so cert validation is unchanged.
- `@simpleworkjs/app-stack` — unified `build_info` (`{buildVersion, buildHash, buildYear}`) and the `static-modules` mounting helper. `utils/build_info.js` and the static-modules loop in `routes/index.js` use the shared helpers.
- New `tests/discovery.test.js` (jest + supertest, runs under the docker harness): locks in the `{ results }` envelope on `/resources`, `/graph`, `/me`, `/resources/:slug`, the `?group=` 200-regression, and the no-`client_secret_hash`/no-secret-key guarantee for every caller.
### Changed
- Dependency alignment: `ldapts` `^8.1.2``^8.1.8`. The new `@simpleworkjs/*` deps resolve from the npm registry (`^1.0.0`); no `file:`/`link:` entries in the lockfile, so `npm ci` is clean in docker builds.
- `build_info` export shape changed from `{commit, version}` to `{buildVersion, buildHash, buildYear}` (the shared shape used by all three apps).
proxy 1.3.0:
### Added
- `@simpleworkjs/oidc-client` — the OIDC client (session models, auth router, OIDC utils, safe-redirect, local-admin bootstrap). Deleted the local `utils/oidc.js`, `utils/safe_redirect.js`, `models/oidc_state.js`, `models/token.js`, `models/auth.js`, `routes/auth.js`; `models/index.js` wires the factory. The per-host SSO in `routes/host_auth.js` is unchanged but consumes the shared OIDC utils.
- `@simpleworkjs/ldap` — the ldapts client + RFC 4515/4514 escaping.
- `@simpleworkjs/app-stack` — unified `build_info` (`{buildVersion, buildHash, buildYear}`) and the `static-modules` mounting helper. `utils/build_info.js` and the static-modules loop in `routes/render.js` now use the shared helpers.
### Security
- **LDAP filter injection in `User.get`.** The user lookup built its search filter by interpolating `data.username` raw into `(&(objectClass=inetOrgPerson)(uid=<username>))`. A username containing `*`, `(`, `)`, `\`, or NUL could widen or alter the filter (e.g. `*` → match-all). The filter value is now passed through `escapeFilter` from `@simpleworkjs/ldap` (RFC 4515 escaping).
### Changed
- Dependency alignment: `model-redis` `^1.5``^1.6.0`, `ldapts` `^8.1.2``^8.1.8`. The four new `@simpleworkjs/*` deps resolve from the npm registry (`^1.0.0`); no `file:`/`link:` entries in the lockfile, so `npm ci` is clean in docker builds.
- `build_info` export shape changed from `{commit, version}` to `{buildVersion, buildHash, buildYear}` (the shared shape used by all three apps). The `/health` endpoint and footer now report `buildVersion`/`buildHash`.
jump-host 1.2.0:
### Added
- `@simpleworkjs/oidc-client` — the OIDC client (session models, auth router, OIDC utils, safe-redirect, local-admin bootstrap). Deleted the local `utils/oidc.js`, `utils/safe_redirect.js`, `models/oidc_state.js`, `models/token.js`, `models/auth.js`, `routes/auth.js`; `models/index.js` wires the factory and the local-admin bootstrap.
- `@simpleworkjs/directory-schema` — the sso↔jump-host directory contract. `utils/access.js` now fetches reachable hosts through the shared `createDirectoryClient` (`getResourcesByGroup`).
- `@simpleworkjs/ldap``models/user_ldap.js` is now a thin wrapper over `createLdapClient`, preserving this app's loose TLS default (`rejectUnauthorized: false`) and the exact export shape.
- `@simpleworkjs/app-stack` — unified `build_info` (`{buildVersion, buildHash, buildYear}`) and the `static-modules` mounting helper. `build_info` moved from `models/` to `utils/`; `routes/render.js` uses `mountStaticModules`.
### Fixed
- **Directory envelope drift was silently treated as "no reachable hosts".** `utils/access.js` previously read `data.results || []`, so if the SSO directory ever returned a bare array (envelope drift) every per-group query collapsed to `[]` and no user could bridge. The shared client now validates the `{ results }` envelope on every call and treats an envelope violation as a failed group fetch rather than silently returning `[]`.
### Changed
- Dependency alignment: `ldapts` `^8.1.2``^8.1.8`, `redis` `^4.7``^6.1.0` (the direct `redis` dep is unused — only `model-redis` is used, which already brings `redis` ^6.1.0). The new `@simpleworkjs/*` deps resolve from the npm registry (`^1.0.0`); no `file:`/`link:` entries in the lockfile, so `npm ci` is clean in docker builds.
- `build_info` export shape changed from `{commit, version}` to `{buildVersion, buildHash, buildYear}` (the shared shape used by all three apps). The `/health` endpoint and footer now report `buildVersion`/`buildHash`.
- `app-base.js` `forceLogin`/`logInRedirect` switched to the `?redirect=` query-param convention (matching the server-side `/login?redirect=` route).
## [1.3.7] - 2026-07-23
### Added
- The bootstrap now provisions the jump host's **web-UI SSO login** when the jump host is enabled: it mints a dedicated `theta-jump` OAuth client and writes a full `oidc` block (endpoints, client id/secret, callback) plus a generated local anti-lockout admin password into `./config/jump-secrets.js`. Matches how the proxy's OIDC client is provisioned. An existing pre-OIDC `jump-secrets.js` (API token but no OIDC client) is regenerated so upgraders get SSO login. Requires jump-host ≥ v1.1.0.
## [1.3.6] - 2026-07-23 ## [1.3.6] - 2026-07-23
### Bumped ### Bumped
@@ -399,7 +590,8 @@ First tagged release. Establishes the `vX.Y.Z` tag convention going forward.
- proxy -> [v1.1.0](https://github.com/theta42/proxy/releases/tag/v1.1.0) - proxy -> [v1.1.0](https://github.com/theta42/proxy/releases/tag/v1.1.0)
- sso-manager-node -> [v1.1.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.0) - sso-manager-node -> [v1.1.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.0)
[Unreleased]: https://github.com/theta42/theta-env/compare/v1.1.20...HEAD [Unreleased]: https://github.com/theta42/theta-env/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/theta42/theta-env/compare/v1.3.7...v1.4.0
[1.1.17]: https://github.com/theta42/theta-env/compare/v1.1.16...v1.1.17 [1.1.17]: https://github.com/theta42/theta-env/compare/v1.1.16...v1.1.17
[1.1.16]: https://github.com/theta42/theta-env/compare/v1.1.15...v1.1.16 [1.1.16]: https://github.com/theta42/theta-env/compare/v1.1.15...v1.1.16
[1.1.15]: https://github.com/theta42/theta-env/compare/v1.1.14...v1.1.15 [1.1.15]: https://github.com/theta42/theta-env/compare/v1.1.14...v1.1.15
+58 -13
View File
@@ -229,14 +229,15 @@ async function listClients(token) {
return (data && data.results) || []; return (data && data.results) || [];
} }
async function createClient(token) { async function createClient(token, opts) {
const o = opts || { name: CLIENT_NAME, description: 'theta-env proxy (auto-registered)', redirect_uris: [REDIRECT_URI] };
const res = await fetch(`${SSO_INTERNAL}/api/oauth/client`, { const res = await fetch(`${SSO_INTERNAL}/api/oauth/client`, {
method: 'POST', method: 'POST',
headers: { 'auth-token': token, 'Content-Type': 'application/json' }, headers: { 'auth-token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
name: CLIENT_NAME, name: o.name,
description: 'theta-env proxy (auto-registered)', description: o.description,
redirect_uris: [REDIRECT_URI], redirect_uris: o.redirect_uris,
scopes: ['openid', 'profile', 'email', 'groups'], scopes: ['openid', 'profile', 'email', 'groups'],
allowed_groups: [], allowed_groups: [],
}), }),
@@ -249,7 +250,7 @@ async function createClient(token) {
const id = (data.results && data.results.client_id) || data.client_id; const id = (data.results && data.results.client_id) || data.client_id;
const secret = data.client_secret; const secret = data.client_secret;
if (!id || !secret) throw new Error(`create OAuth client returned no id/secret: ${JSON.stringify(data)}`); if (!id || !secret) throw new Error(`create OAuth client returned no id/secret: ${JSON.stringify(data)}`);
log(`Created OAuth client ${CLIENT_NAME} (${id})`); log(`Created OAuth client ${o.name} (${id})`);
return { id, secret }; return { id, secret };
} }
@@ -487,6 +488,8 @@ const JUMP_ENABLED = /^(1|true|yes)$/i.test(process.env.CFG_JUMP_HOST_ENABLED ||
const JUMP_HOST = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : ''); const JUMP_HOST = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : '');
const JUMP_SECRETS = '/config/jump-secrets.js'; const JUMP_SECRETS = '/config/jump-secrets.js';
const JUMP_TOKEN_NAME = 'theta-jump-host'; const JUMP_TOKEN_NAME = 'theta-jump-host';
const JUMP_CLIENT_NAME = 'theta-jump';
const JUMP_REDIRECT_URI = `https://${JUMP_HOST}/api/auth/oidc/callback`;
async function mintApiToken(token, name) { async function mintApiToken(token, name) {
const res = await fetch(`${SSO_INTERNAL}/api/api-token`, { const res = await fetch(`${SSO_INTERNAL}/api/api-token`, {
@@ -501,14 +504,19 @@ async function mintApiToken(token, name) {
return raw; return raw;
} }
function jumpFileHasToken() { // The generated file is "complete" only if it has BOTH a real directory API
// token AND an OIDC client id — an existing file from the pre-OIDC layout (a
// token but no oidc block) is regenerated so the web UI's SSO login works.
function jumpFileComplete() {
try { try {
const src = fs.readFileSync(JUMP_SECRETS, 'utf8'); const src = fs.readFileSync(JUMP_SECRETS, 'utf8');
return /apiToken:\s*['"]sso_[0-9a-f]{24}_[0-9a-f]{48}['"]/.test(src); const hasToken = /apiToken:\s*['"]sso_[0-9a-f]{24}_[0-9a-f]{48}['"]/.test(src);
const hasOidc = /clientId:\s*['"][0-9a-f-]{8,}['"]/.test(src);
return hasToken && hasOidc;
} catch (_) { return false; } } catch (_) { return false; }
} }
function writeJumpSecrets(apiToken) { function writeJumpSecrets(apiToken, oidc, localAdminPass) {
const siteName = (sso.stack && sso.stack.siteName) || 'local'; const siteName = (sso.stack && sso.stack.siteName) || 'local';
const ldapsHost = (sso.ldap && sso.ldap.ldapsHost) || SSO_HOST; const ldapsHost = (sso.ldap && sso.ldap.ldapsHost) || SSO_HOST;
const body = `'use strict'; const body = `'use strict';
@@ -537,7 +545,27 @@ module.exports = {
\t\tkeyComment: ${JSON.stringify(`jump-host@${siteName}`)}, \t\tkeyComment: ${JSON.stringify(`jump-host@${siteName}`)},
\t}, \t},
\tweb: { port: 3002 }, \tweb: { port: 3002 },
\tauth: { adminGroups: ['app_sso_admin'] }, \t// Web UI SSO login — the jump host's own OAuth client. tokenEndpoint /
\t// userinfoEndpoint use the internal docker-net address (server-to-server);
\t// authorizationEndpoint is the public SSO host (browser-facing).
\toidc: {
\t\tenabled: true,
\t\tissuer: ${JSON.stringify(`https://${SSO_HOST}`)},
\t\tauthorizationEndpoint: ${JSON.stringify(`https://${SSO_HOST}/oauth/authorize`)},
\t\ttokenEndpoint: 'http://sso-manager:3001/oauth/token',
\t\tuserinfoEndpoint: 'http://sso-manager:3001/oauth/userinfo',
\t\tclientId: ${JSON.stringify(oidc.id)},
\t\tclientSecret: ${JSON.stringify(oidc.secret)},
\t\tredirectUri: ${JSON.stringify(JUMP_REDIRECT_URI)},
\t\tscopes: ['openid', 'profile', 'email', 'groups'],
\t\tgroupsClaim: 'groups',
\t\tusernameClaim: 'preferred_username',
\t},
\tauth: {
\t\tadminGroups: ['app_sso_admin'],
\t\tadminUsers: ['jumpadmin'],
\t\tlocalAdminPass: ${JSON.stringify(localAdminPass)},
\t},
\tredis: { prefix: 'jump_host_', redisConf: { url: 'redis://127.0.0.1:6379' } }, \tredis: { prefix: 'jump_host_', redisConf: { url: 'redis://127.0.0.1:6379' } },
\tstack: { ssoHost: ${JSON.stringify(SSO_HOST)}, jumpHost: ${JSON.stringify(JUMP_HOST)}, ldapsHost: ${JSON.stringify(ldapsHost)} }, \tstack: { ssoHost: ${JSON.stringify(SSO_HOST)}, jumpHost: ${JSON.stringify(JUMP_HOST)}, ldapsHost: ${JSON.stringify(ldapsHost)} },
}; };
@@ -546,13 +574,30 @@ module.exports = {
} }
async function provisionJumpHost(token) { async function provisionJumpHost(token) {
if (jumpFileHasToken()) { if (jumpFileComplete()) {
log('Jump host: /config/jump-secrets.js already has an API token — keeping.'); log('Jump host: /config/jump-secrets.js already has API token + OIDC client — keeping.');
return; return;
} }
const apiToken = await mintApiToken(token, JUMP_TOKEN_NAME); const apiToken = await mintApiToken(token, JUMP_TOKEN_NAME);
writeJumpSecrets(apiToken);
log('Jump host: wrote /config/jump-secrets.js (minted directory API token).'); // Mint (or reuse) the jump host's own OAuth client for web-UI SSO login.
const clients = await listClients(token);
let oidc = clients.find((c) => c.name === JUMP_CLIENT_NAME);
if (oidc && oidc.client_id) {
oidc = await rotateClient(token, oidc.client_id);
oidc = { id: oidc.id, secret: oidc.secret };
} else {
oidc = await createClient(token, {
name: JUMP_CLIENT_NAME,
description: 'theta-env jump host web UI (auto-registered)',
redirect_uris: [JUMP_REDIRECT_URI],
});
}
const localAdminPass = crypto.randomBytes(16).toString('hex');
writeJumpSecrets(apiToken, oidc, localAdminPass);
log(`Jump host: wrote /config/jump-secrets.js (API token + OAuth client ${oidc.id}).`);
log(`Jump host: local admin 'jumpadmin' password: ${localAdminPass}`);
} }
(async function main() { (async function main() {
+1 -1
View File
@@ -31,7 +31,7 @@ fetches all three in one step; `git submodule update --remote` bumps them.
``` ```
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
│ your browser / apps / legacy LDAP clients │ │ your browser / apps / direct LDAP clients │
└───────────────┬──────────────────────────────┘ └───────────────┬──────────────────────────────┘
│ https (:443) ldaps (:636) │ https (:443) ldaps (:636)
┌─────────▼─────────┐ ┌─────────▼─────────┐
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+11 -2
View File
@@ -15,7 +15,9 @@ LDAP directory) and [Proxy](https://theta42.github.io/proxy/) (an
OIDC-protected reverse proxy that can also look users up directly in LDAP) — OIDC-protected reverse proxy that can also look users up directly in LDAP) —
and automates the fiddly part: registering the proxy as an OIDC client of the and automates the fiddly part: registering the proxy as an OIDC client of the
SSO and pointing it at the right LDAP directory, with hostnames and secrets SSO and pointing it at the right LDAP directory, with hostnames and secrets
generated from one `setup.env`. generated from one `setup.env`. An optional third component, the
[Jump Host](https://theta42.github.io/jump-host/), adds directory-driven SSH
access to your machines through one public entry point.
## Screenshots ## Screenshots
@@ -23,6 +25,7 @@ The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
<a href="images/sso-dashboard.png" target="_blank"><img src="images/sso-dashboard.png" alt="SSO Manager dashboard" width="49%"></a> <a href="images/sso-dashboard.png" target="_blank"><img src="images/sso-dashboard.png" alt="SSO Manager dashboard" width="49%"></a>
<a href="images/proxy-hosts.png" target="_blank"><img src="images/proxy-hosts.png" alt="Proxy host list" width="49%"></a> <a href="images/proxy-hosts.png" target="_blank"><img src="images/proxy-hosts.png" alt="Proxy host list" width="49%"></a>
<a href="images/jump-dashboard.png" target="_blank"><img src="images/jump-dashboard.png" alt="Jump Host dashboard" width="49%"></a>
*(click either screenshot to view full size)* *(click either screenshot to view full size)*
@@ -41,7 +44,11 @@ snapshots state before every rebuild.
- **SSO Manager**, fronted by the proxy under TLS — manage users, groups, - **SSO Manager**, fronted by the proxy under TLS — manage users, groups,
and OAuth clients. and OAuth clients.
- **Proxy** — add the hosts you want to protect with OIDC login. - **Proxy** — add the hosts you want to protect with OIDC login.
- **LDAPS** for legacy apps that bind directly. - **LDAPS** for direct binds — Linux hosts (PAM/SSSD, sudo, SSH keys) and
LDAP-native apps authenticate against the same directory.
- **SSH Jump Host** *(optional)*`ssh uid_-_host@jump.<domain>` (WinSCP-friendly)
or an interactive picker; access is driven by directory group membership, with
a web UI for audit + metrics. Enable with `CFG_JUMP_HOST_ENABLED=true`.
- **Self-service API tokens** in both apps' UIs, for scripting/CI without a - **Self-service API tokens** in both apps' UIs, for scripting/CI without a
browser session. browser session.
- **Multi-Site Support (Geo-Location Scaling)** — built-in support for N-Way Multi-Master LDAP replication across physical locations. - **Multi-Site Support (Geo-Location Scaling)** — built-in support for N-Way Multi-Master LDAP replication across physical locations.
@@ -67,3 +74,5 @@ architecture, and running each project standalone, see the
provider + LDAP directory this stack runs. provider + LDAP directory this stack runs.
- **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this - **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this
stack runs in front of it. stack runs in front of it.
- **[Jump Host](https://theta42.github.io/jump-host/)** — the optional SSH jump
host this stack can bring up (`CFG_JUMP_HOST_ENABLED=true`).
+3
View File
@@ -60,6 +60,9 @@ setups `CFG_DOMAIN` is the only value you set:
| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` | | `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` |
| `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional | | `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional |
| `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN | | `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN |
| `CFG_JUMP_HOST_ENABLED` | `true` | optional: bring up the [SSH jump host](https://theta42.github.io/jump-host/) (default off) |
| `CFG_JUMP_HOST` | `jump.lab.local` | optional, defaults to `jump.<domain>` |
| `JUMP_SSH_PORT` | `2222` | optional: host port for the jump host's SSH (never 22 by default) |
`setup.env` is used **only on the first run** to generate `./config/`; after `setup.env` is used **only on the first run** to generate `./config/`; after
that `./config/*.js` are operator-owned and `setup.env` is ignored. Secrets that `./config/*.js` are operator-owned and `setup.env` is ignored. Secrets
+1 -1
Submodule proxy updated: 60dbfe5b9b...75045b4b33