feat: initial jump-host — SSH jump host for the theta42 stack

An SSH jump host that authenticates users against the shared LDAP
directory, authorizes them from the SSO Manager's inventory graph, and
bridges them to downstream hosts — auditing everything.

- Username-grammar routing (uid_-_target@jump) + interactive TUI picker
- Inbound LDAP auth (publickey / password with off|local|all policy)
- Directory-driven access (LDAP groups x /api/discovery/resources?group=)
- Per-user key injection into sshPublicKey, connects downstream as the user
- Shell / exec / SFTP-subsystem bridging (WinSCP works)
- Web UI + HTTP API (:3002) for audit + metrics; LDAP-admin gated
- Packaged like proxy: ops/install.sh + systemd, all-in-one Docker, compose
- Tests: 23 unit + 3 integration (node --test), all green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:46:19 -04:00
commit 36e9d5b0b3
51 changed files with 4291 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
'use strict';
// Web UI/API auth: a signed-in admin session (cookie) whose LDAP groups
// intersect conf.auth.adminGroups. /health and the login routes are exempt
// (mounted before this middleware).
const conf = require('@simpleworkjs/conf');
const Session = require('../models/session');
function parseCookies(header) {
const out = {};
(header || '').split(';').forEach((p) => {
const i = p.indexOf('=');
if (i > -1) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim());
});
return out;
}
async function requireAdmin(req, res, next) {
const token = parseCookies(req.headers.cookie).jump_session;
const session = await Session.verify(token);
if (!session) {
if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'unauthorized' });
return res.redirect('/login');
}
const groups = JSON.parse(session.groups || '[]');
const admin = (conf.auth.adminGroups || []).some((g) => groups.includes(g));
if (!admin) {
if (req.path.startsWith('/api/')) return res.status(403).json({ error: 'forbidden' });
return res.status(403).render('login', { error: 'Your account is not a jump-host admin.', name: conf.name });
}
req.jumpUser = { uid: session.uid, groups };
next();
}
module.exports = { requireAdmin, parseCookies };