4fb4e77007
- New GET /api/user/hosts (auth-only): all hosts for admins, group-filtered list for everyone else. - accessibleHosts() accepts a pre-resolved user.groups, so the web UI's OIDC session skips a redundant LDAP getGroups(dn) call. - Dashboard shows a "Hosts you can reach" / "All hosts" table. - @simpleworkjs/ldap 1.0.1 fixes addSshKey's ObjectClassViolationError on accounts predating the ldapPublicKey objectClass -- was aborting key injection (and the SSH connection) on affected accounts. - Bump to 1.5.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
12 KiB
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog; versions
correspond to git tags (vX.Y.Z) and nodejs/package.json's version.
[1.5.0] - 2026-07-27
Added
- Web UI dashboard now lists the hosts you can reach ("Hosts you can reach", or "All hosts" for admins) — previously the dashboard only showed usage metrics, with no way to see your actual access from the browser. Backed by a new
GET /api/user/hostsendpoint (auth-only, not admin-gated): admins get the full inventory viautils/access.js's newallHosts(), everyone else gets the same group-based resolution the SSH front door uses. utils/access.js'saccessibleHosts()now accepts a pre-resolvedgroupsarray on the user object, skipping the LDAPgetGroups(dn)round-trip — the web UI's OIDC session already has its groups claim and has no LDAPdnto query with.
Fixed
- Bumped
@simpleworkjs/ldapto 1.0.1, which fixesaddSshKeythrowingObjectClassViolationError(LDAP0x41) on accounts predating theldapPublicKeyauxiliary objectClass. This is the code path this jump host's key-injection (utils/key_inject.js) uses on every first connection for a user — on affected accounts it aborted the SSH connection entirely (key-inject-failed).
[1.4.0] - 2026-07-26
Added
- Standalone mode — run the jump host with no LDAP directory and no SSO Manager at all. Set
standalone.enabled: trueand user authentication and host discovery switch to@simpleworkjs/orm-backed stores (Sequelize; SQLite by default, any Sequelize-supported dialect viaconf.orm) instead of the directory services.models/user_ldap.jsandutils/access.jsbecome conditional facades that pick their backend at require time —ssh_server.js,bridge.js,key_inject.js,tui_picker.js, and the web UI are unchanged either way. - New ORM models:
StandaloneUser(uid,passwordHash,sshPublicKeys,groups) andStandaloneHost(slug,displayName,kind,metadata), plusmodels/user_file.jsandutils/hosts_file.js, which implement the same interfaces as the LDAP client andaccessibleHosts()respectively. There's no admin UI for standalone users/hosts yet — see the README's "Standalone mode" section for the ORM-model seeding snippet. In standalone mode every stored host is reachable by every stored user; there's no group-based authorization yet. - 47 tests pass (24 existing + 15 new unit + 3 existing integration + 5 new standalone integration).
Fixed
services/ssh_server.jsused|| 2222for the listen port, so an explicitlistenPort: 0(ephemeral port, used by the test suite) was silently overridden back to 2222. Changed to?? 2222.services/ssh_server.jsawaitedaudit.create()before registering session listeners. A client that sendsexec/shellimmediately after connecting could have its request dropped because nothing was listening yet. Listener registration now happens first.
[1.3.0] - 2026-07-26
Changed
- Unified the front-end UI shell across the three theta42 apps.
views/top.ejs,views/bottom.ejsandpublic/lib/js/app-base.jsare 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 newnodejs/utils/ui.js, exposed to every render asuiviaapp.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.jsreveals.group-required-<cn>elements for each group the current user is in, read fromGET /api/user/me. sso-manager-node reports LDAP DNs inmemberOfand the OIDC clients report CNs ingroups; both normalise to CNs client-side, and the clients' effective-rightsisAdminflag is exposed as a syntheticadmingroup — 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/meis fetched once per page load and cached (app.auth.loadUser). The nav, per-viewforceLoginand every group-gated element read that one promise instead of issuing their own request.app.auth.isLoggedInis dual-mode: it returns a Promise and invokes an optional node-style callback, so the async and callback call styles both work against one sharedtop.ejs.app.auth.forceLoginno longer uses$.holdReady(removed in jQuery 4). An unauthenticated user is redirected to/login?redirect=<path>; group requirements are still enforced, andlogOutnow only clears the session, leaving the destination to the caller (ui.logoutRedirect).- Dependency alignment across all three apps:
jquery^4.0.0andejs^3.1.10.
Fixed
app.api.deletedropped its callback when called byformAJAX.formAJAXalways passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran.deletenow accepts both(url, callback)and(url, data, callback).app.api.post/putreferenced an undefinedcallback2and 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. logInRedirecton 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 }inpublic/css/styles.css, the base rule the shared gating model reveals against.#spa-shelldropped its inlinemargin-top;styles.cssalready 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.jsblocks 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.2.0] - 2026-07-25
Added
- Adopted the shared
@simpleworkjs/*packages published under the simpleworkjs org, replacing this app's byte-identical forks of the same code so the theta42 apps share one codebase and API schema:@simpleworkjs/oidc-client— the OIDC client (session models, auth router, OIDC utils, safe-redirect, local-admin bootstrap). Deleted the localutils/oidc.js,utils/safe_redirect.js,models/oidc_state.js,models/token.js,models/auth.js,routes/auth.js;models/index.jswires the factory and the local-admin bootstrap.@simpleworkjs/directory-schema— the sso↔jump-host directory contract.utils/access.jsnow fetches reachable hosts through the sharedcreateDirectoryClient(getResourcesByGroup).@simpleworkjs/ldap—models/user_ldap.jsis now a thin wrapper overcreateLdapClient, preserving this app's loose TLS default (rejectUnauthorized: false) and the exact export shape.@simpleworkjs/app-stack— unifiedbuild_info({buildVersion, buildHash, buildYear}) and thestatic-modulesmounting helper.build_infomoved frommodels/toutils/;routes/render.jsusesmountStaticModules.
Fixed
- Directory envelope drift was silently treated as "no reachable hosts".
utils/access.jspreviously readdata.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 directredisdep is unused — onlymodel-redisis used, which already bringsredis^6.1.0). The new@simpleworkjs/*deps resolve from the npm registry (^1.0.0); nofile:/link:entries in the lockfile, sonpm ciis clean in docker builds. build_infoexport shape changed from{commit, version}to{buildVersion, buildHash, buildYear}(the shared shape used by all three apps). The/healthendpoint and footer now reportbuildVersion/buildHash.
[1.1.0] - 2026-07-23
Changed
- Rebuilt the web UI on the shared theta42 app stack so it looks and behaves like the SSO Manager and Proxy: Express + EJS with the same
top.ejs/bottom.ejsshell, Bootstrap 5, jQuery, jq-repeat, FontAwesome, the sharedapp-base.jsclient framework, and Socket.IO — replacing the bespoke minimal theme. Dashboard, Sessions, and Audit pages now render in the common look/feel. - Web-UI auth is now OIDC + a local anti-lockout admin (the proxy's model), replacing the direct LDAP-bind login. Normal users log in through the SSO ("Log in with SSO"); a local
auth.adminUsersaccount (bootstrapped on first boot, password fromauth.localAdminPass) still works if the SSO is unreachable. Admin access is gated byauth.adminGroupsor the local admin account. New config:oidcblock +auth.adminUsers/localAdminPass. Note: the SSH bridge and its own LDAP auth are unchanged — this only affects the web management UI.
[1.0.1] - 2026-07-23
Fixed
- Test scripts use shell-expanded globs and CI provides a redis service, so
npm testruns green on the Node 20/22 CI runners (thenode --test**glob and the redis-backed models only worked locally before). No runtime change.
[1.0.0] - 2026-07-23
Added
- Initial release. An SSH jump host for the theta42 stack:
- Username-grammar routing:
ssh {uid}_-_{target}@jumphostbridges straight to the downstream host (target= a directory host slug, bare hostname, or IP). Shell, exec, and the SFTP subsystem all pass through, so WinSCP/sftpwork. - Interactive TUI picker: plain
ssh {uid}@jumphostlists the hosts the user can reach (from the SSO directory) and bridges to the chosen one. - LDAP auth of the inbound user (publickey against the user's
sshPublicKey, or password via LDAP bind — password policy is off/local/all). - Directory-driven access: reachable hosts are the union of the user's
LDAP groups × the SSO directory (
/api/discovery/resources?group=). - Per-user key injection: the jump host appends its own public key to the
user's
sshPublicKeyon first use, then connects downstream as that user (downstream hosts already serve LDAP keys via ldap-client's AuthorizedKeysCommand). - Web UI + HTTP API (
:3002) for auditing and metrics: active sessions, paged audit log, per-user/per-host counters. Admin login gated by LDAP group membership. - Audit logging of every connection attempt/session (user, target, method, result, bytes, duration, downstream host-key fingerprint).
- Packaged like theta42/proxy: idempotent
ops/install.sh+ systemd unit, all-in-one Docker image, standalonedocker-compose.yml.
- Username-grammar routing: