fc0d9104d0
Rewire onto @simpleworkjs/directory-schema, /ldap, and /app-stack. The
directory discovery API now returns the {results} envelope via explicit
/resources, /resources/:slug, /graph, /me handlers and routes every read
through projectResource/projectResources, which unconditionally strips
client_secret_hash (and any /secret|password|privatekey/i key) and reduces
metadata to a public allowlist for non-admins — closing the leak where the ORM
serialized metadata wholesale. The dead routes/api_discovery.js (mounted after
the 404 catcher) is removed; ?group= now returns 200 instead of 404. user_ldap
+ group_ldap take escapeFilter/escapeDN + makeClient/withClient from the shared
ldap package (posix/write-side stays app-local; cert validation unchanged).
build_info unified to {buildVersion,buildHash,buildYear}; ldapts ^8.1.8. New
tests/discovery.test.js locks in the envelope + no-secrets guarantees. Lockfile
regenerated from the registry (no file:/link:).
Co-Authored-By: Claude <noreply@anthropic.com>
79 lines
3.1 KiB
JavaScript
79 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
// Public directory discovery API. Mounted at /api/discovery (app.js, before
|
|
// the 404 catcher). Every response uses the `{ results }` envelope and the
|
|
// security projection from @simpleworkjs/directory-schema, so secrets (e.g. an
|
|
// OAuth client's client_secret_hash) never leave the server and non-admins only
|
|
// see the public metadata allowlist.
|
|
//
|
|
// This replaces the autoRouter mount (which returned bare arrays — the shape
|
|
// jump-host's `data.results || []` silently collapsed to `[]`, so no user could
|
|
// bridge) and absorbs the dead /me handler that used to live in
|
|
// routes/api_discovery.js (mounted after the 404, so unreachable).
|
|
|
|
const router = require('express').Router();
|
|
const { Resource, ResourceGroup } = require('../models/resource');
|
|
const {
|
|
envelope,
|
|
projectResource,
|
|
projectResources,
|
|
isDirectoryAdmin,
|
|
} = require('@simpleworkjs/directory-schema');
|
|
|
|
// GET /api/discovery/resources[?kind=&group=&parent=]
|
|
router.get('/resources', async (req, res, next) => {
|
|
try {
|
|
const resources = await Resource.search(req.query);
|
|
res.json(envelope(projectResources(resources, { fullMetadata: isDirectoryAdmin(req.user) })));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /api/discovery/resources/:slug
|
|
router.get('/resources/:slug', async (req, res, next) => {
|
|
try {
|
|
const resource = await Resource.getBySlug(req.params.slug);
|
|
// parents/children are edges (no secrets); project only the resource body.
|
|
const projected = projectResource(resource, { fullMetadata: isDirectoryAdmin(req.user) });
|
|
projected.parents = resource.parents;
|
|
projected.children = resource.children;
|
|
res.json(envelope(projected));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /api/discovery/graph
|
|
router.get('/graph', async (req, res, next) => {
|
|
try {
|
|
const graph = await Resource.getGraph();
|
|
res.json(envelope({
|
|
resources: projectResources(graph.resources, { fullMetadata: isDirectoryAdmin(req.user) }),
|
|
edges: graph.edges,
|
|
}));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /api/discovery/me
|
|
// Returns the resources the current caller can reach. Machines see only their
|
|
// own resource; humans get the union of their LDAP groups' resources plus
|
|
// anything flagged isPublic. Uses req.user.groups (populated by the auth
|
|
// middleware for session/PAT callers) rather than re-querying LDAP by DN, so it
|
|
// works for every auth transport without assuming a .dn is present.
|
|
router.get('/me', async (req, res, next) => {
|
|
try {
|
|
let accessible;
|
|
if (req.user && req.user.isMachine) {
|
|
accessible = await Resource.list({ where: { id: req.resourceId } });
|
|
} else {
|
|
const userGroups = (req.user && req.user.groups) || [];
|
|
const ids = new Set();
|
|
if (userGroups.length) {
|
|
const rgs = await ResourceGroup.list({ where: { groupCn: { in: userGroups } } });
|
|
for (const rg of rgs) ids.add(rg.resourceId);
|
|
}
|
|
const all = await Resource.list();
|
|
accessible = all.filter(r => ids.has(r.id) || (r.metadata && r.metadata.isPublic));
|
|
}
|
|
res.json(envelope(projectResources(accessible, { fullMetadata: isDirectoryAdmin(req.user) })));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
module.exports = router; |