'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. const { accessibleHosts } = require('./hosts_file'); module.exports = { accessibleHosts, clearCache: () => {} }; } else { // Production mode: LDAP groups + SSO API (unchanged). // Which directory hosts may a user reach, and how do we dial them? // // v1 resolution (see directory_spec.md ยง9.2 in sso-manager-node): the SSO's // /api/discovery/me only answers for the API token's own user, and /graph // omits ResourceGroup links โ€” so we combine the user's LDAP groups (queried // directly) with per-group resource lookups: // // 1. LDAP: groups the user's DN is a member of // 2. SSO: GET /api/discovery/resources?group= per group (ApiToken) // 3. union, keep kind === 'host' // // 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); } async function accessibleHosts(user, { fetchImpl = fetch, ldap = userLdap } = {}) { const hit = cache.get(user.uid); if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.hosts; const groups = await ldap.getGroups(user.dn); const seen = new Map(); for (const cn of groups) { let resources; try { resources = await fetchResourcesByGroup(cn, { fetchImpl }); } catch (error) { // One bad group must not hide the rest; the SSO being down // surfaces as an empty list + log line, not a crash. console.error(`[access] ${error.message}`); continue; } for (const r of resources) { if (r.kind === 'host' && !seen.has(r.id)) seen.set(r.id, r); } } const hosts = [...seen.values()]; cache.set(user.uid, { at: Date.now(), hosts }); return hosts; } function clearCache(uid) { if (uid) cache.delete(uid); else cache.clear(); } module.exports = { accessibleHosts, clearCache, fetchResourcesByGroup }; }