fix: only catalog hosts are jump targets (v1.19.0)
Pull Request Tests / Run Tests (20.x) (push) Failing after 1m4s
Pull Request Tests / Run Tests (22.x) (push) Failing after 1m3s
Pull Request Tests / Test Summary (push) Failing after 4s

isManagedHost() treated a missing metadata.managed flag as permission, so
any host the SSO merely discovered -- an unpromoted Proxmox guest, a UniFi
client -- was offered in the TUI picker and accepted by the username
grammar.

Replaced with isCatalogHost(), mirroring the rule the SSO Directory's own
listing applies: a resource carrying discovery_sources but never promoted
is excluded; hand-created hosts and promoted ones are included; an
explicit managed:false is always excluded.

The two copies of this rule have now drifted apart once. If a third
consumer needs it, hoist it into @simpleworkjs/directory-schema rather
than copying again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 18:41:50 -04:00
parent a7b3416619
commit fedbe81690
6 changed files with 58 additions and 15 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "t42-jump-host",
"version": "1.18.0",
"version": "1.19.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "t42-jump-host",
"version": "1.18.0",
"version": "1.19.0",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "t42-jump-host",
"version": "1.18.0",
"version": "1.19.0",
"description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics",
"author": [
{
+19
View File
@@ -70,6 +70,25 @@ test('allHosts fetches the whole host inventory with no group filter', async ()
assert.deepStrictEqual(hosts.map((h) => h.id).sort(), ['1', '2']);
});
// Only catalog content is a jump target. Discovery writes `discovery_sources`;
// promoting to the catalog sets `managed: true`. An unpromoted Proxmox VM was
// reaching the picker because the filter defaulted `managed`-less hosts to true.
test('drops auto-discovered hosts that were never promoted', async () => {
clearCache();
const user = { uid: 'frank', dn: 'd' };
const fetchImpl = stubFetch({
frank: [
{ id: '1', kind: 'host', slug: 'host_web01' }, // hand-made: no discovery_sources
{ id: '2', kind: 'host', slug: 'vm-101', metadata: { discovery_sources: ['proxmox'] } }, // discovered, unpromoted
{ id: '3', kind: 'host', slug: 'vm-102', metadata: { discovery_sources: ['proxmox'], managed: true } }, // promoted
{ id: '4', kind: 'host', slug: 'host_db', metadata: { discovery_sources: ['manual'] } }, // manual source counts as catalog
{ id: '5', kind: 'host', slug: 'host_off', metadata: { managed: false } }, // explicitly out
],
});
const hosts = await accessibleHosts(user, { fetchImpl });
assert.deepStrictEqual(hosts.map((h) => h.id).sort(), ['1', '3', '4']);
});
test('a bare-array response (envelope drift) returns empty list', async () => {
clearCache();
const user = { uid: 'dave', dn: 'd' };
+20 -9
View File
@@ -46,19 +46,30 @@ if (conf.standalone && conf.standalone.enabled) {
// Every host in the inventory, unfiltered — for admins (the web UI's own
// account is already gated by requireAdmin before this is ever called).
function isManagedHost(r) {
//
// "In the catalog" is the same predicate the SSO's own Directory listing
// applies (sso-manager-node routes/api_directory_admin.js GET /resources):
// a resource that was auto-discovered and never promoted is NOT catalog
// content and must never be offered as a jump target. Discovery writes
// `metadata.discovery_sources`; promoting sets `metadata.managed = true`.
// Hosts created by hand carry no discovery_sources at all and stay in.
//
// The two copies of this rule have already drifted apart once (unpromoted
// Proxmox VMs showing up in the picker); if a third consumer needs it,
// hoist it into @simpleworkjs/directory-schema rather than copying again.
function isCatalogHost(r) {
if (!r || r.kind !== 'host') return false;
// If managed attribute is present, require it to be true/truthy
if (r.metadata && r.metadata.managed !== undefined) {
return r.metadata.managed === true || r.metadata.managed === 'true';
}
// Default to true for manually created hosts that lack explicit managed metadata
return true;
const meta = r.metadata || {};
if (meta.managed === true || meta.managed === 'true') return true;
if (meta.managed === false || meta.managed === 'false') return false;
const sources = meta.discovery_sources || [];
const autoDiscovered = sources.length > 0 && !sources.includes('manual');
return !autoDiscovered;
}
async function allHosts({ fetchImpl = fetch } = {}) {
const resources = await directoryClient({ fetchImpl }).getResourcesByGroup(undefined, { kind: 'host' });
return resources.filter(isManagedHost);
return resources.filter(isCatalogHost);
}
async function accessibleHosts(user, { fetchImpl = fetch } = {}) {
@@ -72,7 +83,7 @@ if (conf.standalone && conf.standalone.enabled) {
console.error(`[access] ${error.message}`);
}
const hosts = resources.filter(isManagedHost);
const hosts = resources.filter(isCatalogHost);
cache.set(user.uid, { at: Date.now(), hosts });
return hosts;
}