36e9d5b0b3
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>
110 lines
2.9 KiB
JavaScript
110 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
// Thin LDAP helpers — the jump host's entire LDAP surface:
|
|
// getUser(uid) -> { dn, uid, sshPublicKeys: [] } or null
|
|
// getGroups(dn) -> [cn, ...] (groupOfNames membership)
|
|
// checkPassword(dn, pw) -> bool (simple bind as the user)
|
|
// addSshKey(dn, keyLine) -> void (idempotent multi-value add)
|
|
//
|
|
// Mirrors the patterns in sso-manager-node/nodejs/models/user_ldap.js and
|
|
// group_ldap.js (ldapts, admin-bound search, bind-as-user password check,
|
|
// TypeOrValueExists treated as success on key add).
|
|
|
|
const { Client, Change, Attribute } = require('ldapts');
|
|
const conf = require('@simpleworkjs/conf');
|
|
|
|
function ldapConf() {
|
|
return conf.ldap || {};
|
|
}
|
|
|
|
function makeClient() {
|
|
const c = ldapConf();
|
|
return new Client({
|
|
url: c.url,
|
|
tlsOptions: c.tlsOptions || { rejectUnauthorized: false },
|
|
});
|
|
}
|
|
|
|
// Escape a value being interpolated into an LDAP filter (RFC 4515).
|
|
function escapeFilter(value) {
|
|
return String(value).replace(/[\\*()\0]/g, (ch) => ({
|
|
'\\': '\\5c', '*': '\\2a', '(': '\\28', ')': '\\29', '\0': '\\00',
|
|
}[ch]));
|
|
}
|
|
|
|
async function withClient(fn) {
|
|
const c = ldapConf();
|
|
const client = makeClient();
|
|
try {
|
|
await client.bind(c.bindDN, c.bindPassword);
|
|
return await fn(client);
|
|
} finally {
|
|
await client.unbind().catch(() => {});
|
|
}
|
|
}
|
|
|
|
async function getUser(uid) {
|
|
const c = ldapConf();
|
|
const attr = c.userNameAttribute || 'uid';
|
|
return withClient(async (client) => {
|
|
const { searchEntries } = await client.search(c.userBase, {
|
|
scope: 'sub',
|
|
filter: `(&(objectClass=posixAccount)(${attr}=${escapeFilter(uid)}))`,
|
|
attributes: ['dn', attr, 'cn', 'sshPublicKey'],
|
|
});
|
|
if (!searchEntries.length) return null;
|
|
const e = searchEntries[0];
|
|
let keys = e.sshPublicKey || [];
|
|
if (!Array.isArray(keys)) keys = [keys];
|
|
return {
|
|
dn: e.dn,
|
|
uid: String(e[attr]),
|
|
sshPublicKeys: keys.map(String),
|
|
};
|
|
});
|
|
}
|
|
|
|
async function getGroups(dn) {
|
|
const c = ldapConf();
|
|
return withClient(async (client) => {
|
|
const { searchEntries } = await client.search(c.groupBase, {
|
|
scope: 'sub',
|
|
filter: `(&(objectClass=groupOfNames)(member=${escapeFilter(dn)}))`,
|
|
attributes: ['cn'],
|
|
});
|
|
return searchEntries.map((e) => String(e.cn));
|
|
});
|
|
}
|
|
|
|
async function checkPassword(dn, password) {
|
|
if (!password) return false;
|
|
const client = makeClient();
|
|
try {
|
|
await client.bind(dn, password);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
} finally {
|
|
await client.unbind().catch(() => {});
|
|
}
|
|
}
|
|
|
|
async function addSshKey(dn, keyLine) {
|
|
return withClient(async (client) => {
|
|
try {
|
|
await client.modify(dn, [
|
|
new Change({
|
|
operation: 'add',
|
|
modification: new Attribute({ type: 'sshPublicKey', values: [keyLine] }),
|
|
}),
|
|
]);
|
|
} catch (error) {
|
|
// Same de-dup semantics as the SSO's User.addSSHkey.
|
|
if (error.name === 'TypeOrValueExistsError') return;
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = { getUser, getGroups, checkPassword, addSshKey, escapeFilter, makeClient };
|