Release 1.2.0: adopt shared @simpleworkjs/* packages; fix directory envelope drift

Rewire onto the shared @simpleworkjs/oidc-client, /directory-schema, /ldap, and
/app-stack packages (deleting the byte-identical local forks). utils/access.js
now fetches reachable hosts through the shared directory client, which
validates the {results} envelope and treats envelope drift as a failed group
rather than silently returning []. models/user_ldap.js is a thin wrapper over
createLdapClient (loose TLS default preserved). build_info moves to utils/ with
the shared {buildVersion,buildHash,buildYear} shape. Align ldapts ^8.1.8 and
redis ^6.1.0. Lockfile regenerated from the registry (no file:/link:).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:55:03 -04:00
parent 23d99980ce
commit 67e2fc54c2
21 changed files with 224 additions and 809 deletions
+12 -99
View File
@@ -1,109 +1,22 @@
'use strict';
// Thin LDAP helpers — the jump host's entire LDAP surface:
// Thin LDAP helpers — the jump host's entire LDAP surface, now backed by the
// shared @simpleworkjs/ldap package:
// 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).
// Behavior is unchanged from the previous in-tree implementation: posixAccount
// user filter, groupOfNames group filter, bind-as-user password check,
// TypeOrValueExists treated as success on key add, and the same loose TLS
// default ({ rejectUnauthorized: false } when conf.ldap omits tlsOptions).
const { Client, Change, Attribute } = require('ldapts');
const conf = require('@simpleworkjs/conf');
const { createLdapClient } = require('@simpleworkjs/ldap');
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 };
const ldapConf = conf.ldap || {};
module.exports = createLdapClient({
...ldapConf,
tlsOptions: ldapConf.tlsOptions || { rejectUnauthorized: false },
});