Files
jump-host/nodejs/utils/username_grammar.js
T
wmantly 36e9d5b0b3 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>
2026-07-23 15:48:49 -04:00

58 lines
1.9 KiB
JavaScript

'use strict';
// Parse the jump host's SSH username grammar:
//
// {uid} -> interactive TUI picker
// {uid}_-_{target} -> bridge straight to <target>
//
// where <target> is a directory host slug (with or without the host_ prefix),
// a bare hostname, or an IPv4 address. The separator `_-_` was chosen because
// it is legal in an SSH username everywhere (WinSCP included) and cannot
// appear in a POSIX uid. We split on the FIRST `_-_`: uids cannot contain it
// (POSIX uids don't allow the sequence in practice and the SSO's invite flow
// never generates one), while a target could in theory contain a later `-`
// sequence.
//
// Returns { uid, target } — target is null in picker mode.
// Throws on a syntactically invalid uid or target.
const SEP = '_-_';
// POSIX-ish uid: same shape the SSO enforces.
const UID_RE = /^[a-z_][a-z0-9._-]{0,31}$/;
// Directory slug chars (slugify output) or a hostname label string.
const TARGET_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
function isIPv4(s) {
const m = IPV4_RE.exec(s);
if (!m) return false;
return m.slice(1).every((o) => Number(o) <= 255);
}
function parseUsername(username) {
if (typeof username !== 'string' || !username.length) {
throw new Error('Empty username');
}
const idx = username.indexOf(SEP);
if (idx === -1) {
if (!UID_RE.test(username)) throw new Error(`Invalid username: ${username}`);
return { uid: username, target: null };
}
const uid = username.slice(0, idx);
const target = username.slice(idx + SEP.length);
if (!UID_RE.test(uid)) throw new Error(`Invalid uid in username: ${uid}`);
if (!target.length || (!TARGET_RE.test(target) && !isIPv4(target))) {
throw new Error(`Invalid target in username: ${target}`);
}
return { uid, target };
}
module.exports = { parseUsername, isIPv4, SEP };