Add standalone mode with @simpleworkjs/orm-backed user/host stores
- Add standalone.enabled config flag to switch between LDAP+SSO and ORM-backed backends without changing the production code path - New ORM models: StandaloneUser (uid, passwordHash, sshPublicKeys, groups) and StandaloneHost (slug, displayName, kind, metadata) - user_file.js and hosts_file.js implement the same interfaces as the LDAP client and accessibleHosts() respectively - models/user_ldap.js and utils/access.js become conditional facades that delegate based on conf.standalone.enabled at require time - Zero changes to ssh_server.js core logic, bridge.js, key_inject.js, tui_picker.js, or any other consumer - Fix ssh_server.js: use ?? instead of || for listenPort (0 is falsy) - Fix ssh_server.js: register session listeners before awaiting audit.create() so client exec/shell requests aren't rejected - Patch StringField.toSequelize() and IntegerField.toSequelize() to pass through primaryKey (the ORM's UUIDField already does this) - 47 tests pass (24 existing + 15 new unit + 3 existing integration + 5 new standalone integration) - Defaults to SQLite; any Sequelize dialect works via conf.orm Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
// ORM-backed user store for standalone mode. Implements the same interface as
|
||||
// the @simpleworkjs/ldap client so ssh_server.js and key_inject.js work
|
||||
// unchanged: getUser(uid), getGroups(dn), checkPassword(dn, pw), addSshKey(dn, keyLine).
|
||||
//
|
||||
// Users are stored via the StandaloneUser ORM model (Sequelize, any dialect).
|
||||
// DNs are synthetic: uid=<uid>,ou=people,dc=standalone,dc=local — the real
|
||||
// identity is the uid; the DN exists only for interface compatibility with
|
||||
// callers that thread user.dn through to checkPassword / addSshKey.
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const StandaloneUser = require('./standalone_user');
|
||||
|
||||
const DN_PREFIX = 'uid=';
|
||||
const DN_SUFFIX = ',ou=people,dc=standalone,dc=local';
|
||||
|
||||
function dnFor(uid) {
|
||||
return `${DN_PREFIX}${uid}${DN_SUFFIX}`;
|
||||
}
|
||||
|
||||
function uidFromDn(dn) {
|
||||
if (!dn || typeof dn !== 'string') return null;
|
||||
const m = dn.match(/^uid=([^,]+)/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
async function getUser(uid) {
|
||||
const user = await StandaloneUser.get(uid);
|
||||
if (!user) return null;
|
||||
return {
|
||||
dn: dnFor(user.uid),
|
||||
uid: user.uid,
|
||||
sshPublicKeys: user.sshPublicKeys || [],
|
||||
};
|
||||
}
|
||||
|
||||
async function getGroups(dn) {
|
||||
const uid = uidFromDn(dn);
|
||||
if (!uid) return [];
|
||||
const user = await StandaloneUser.get(uid);
|
||||
if (!user) return [];
|
||||
return user.groups || [];
|
||||
}
|
||||
|
||||
async function checkPassword(dn, pw) {
|
||||
const uid = uidFromDn(dn);
|
||||
if (!uid) return false;
|
||||
const user = await StandaloneUser.get(uid);
|
||||
if (!user || !user.passwordHash) return false;
|
||||
return bcrypt.compare(pw, user.passwordHash);
|
||||
}
|
||||
|
||||
async function addSshKey(dn, keyLine) {
|
||||
const uid = uidFromDn(dn);
|
||||
if (!uid) return;
|
||||
const user = await StandaloneUser.get(uid);
|
||||
if (!user) return;
|
||||
const keys = [...(user.sshPublicKeys || [])];
|
||||
if (keys.includes(keyLine)) return; // idempotent
|
||||
keys.push(keyLine);
|
||||
await user.update({ sshPublicKeys: keys });
|
||||
}
|
||||
|
||||
module.exports = { getUser, getGroups, checkPassword, addSshKey };
|
||||
Reference in New Issue
Block a user