Files
jump-host/nodejs/utils/access.js
T
wmantly 16ab12a61c
Pull Request Tests / Run Tests (20.x) (push) Failing after 1m16s
Pull Request Tests / Run Tests (22.x) (push) Failing after 1m11s
Pull Request Tests / Test Summary (push) Failing after 4s
feat: use machine identity for access queries
2026-08-02 18:49:00 -04:00

77 lines
2.9 KiB
JavaScript

'use strict';
// Host discovery — SSO Manager API in production, ORM-backed inventory in
// standalone mode. Both export the same interface:
// accessibleHosts(user) -> [host resources]
// clearCache(uid?) -> void
const conf = require('@simpleworkjs/conf');
if (conf.standalone && conf.standalone.enabled) {
// Standalone mode: use the ORM-backed host inventory. Every host is
// accessible to every user, so allHosts and accessibleHosts coincide.
const { accessibleHosts } = require('./hosts_file');
module.exports = { accessibleHosts, allHosts: () => accessibleHosts(), clearCache: () => {} };
} else {
// Production mode: LDAP groups + SSO API (unchanged).
// Which directory hosts may a user reach, and how do we dial them?
//
// We use the SSO's machine-aware /api/discovery/access/:uid endpoint,
// which evaluates the user's groups server-side and returns their complete
// access projection in one call.
//
// Results are cached per-uid for a short TTL — the TUI picker and the
// username-grammar path share the cache. Dependency-injected fetch/ldap for
// unit testing.
const { createDirectoryClient } = require('@simpleworkjs/directory-schema');
const userLdap = require('../models/user_ldap');
const CACHE_TTL_MS = 30 * 1000;
const cache = new Map(); // uid -> {at, hosts}
// Build a directory client bound to conf.sso. fetchImpl is injectable so the
// unit tests can stub the transport; the shared client validates the
// `{ results }` envelope on every call (turns the old bare-array drift into a
// thrown error instead of a silent `[]`).
function directoryClient({ fetchImpl = fetch } = {}) {
const sso = conf.sso || {};
return createDirectoryClient({ baseUrl: sso.url, apiToken: sso.apiToken, fetch: fetchImpl });
}
async function fetchResourcesByGroup(group, { fetchImpl = fetch } = {}) {
return directoryClient({ fetchImpl }).getResourcesByGroup(group);
}
// Every host in the inventory, unfiltered — for admins (the web UI's own
// account is already gated by requireAdmin before this is ever called).
async function allHosts({ fetchImpl = fetch } = {}) {
const resources = await directoryClient({ fetchImpl }).getResourcesByGroup(undefined, { kind: 'host' });
return resources.filter(r => r.kind === 'host');
}
async function accessibleHosts(user, { fetchImpl = fetch } = {}) {
const hit = cache.get(user.uid);
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.hosts;
let resources = [];
try {
resources = await directoryClient({ fetchImpl }).getAccess(user.uid);
} catch (error) {
console.error(`[access] ${error.message}`);
}
const hosts = resources.filter(r => r.kind === 'host');
cache.set(user.uid, { at: Date.now(), hosts });
return hosts;
}
function clearCache(uid) {
if (uid) cache.delete(uid);
else cache.clear();
}
module.exports = { accessibleHosts, allHosts, clearCache, fetchResourcesByGroup };
}