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>
231 lines
7.9 KiB
JavaScript
231 lines
7.9 KiB
JavaScript
'use strict';
|
|
const router = require('express').Router();
|
|
const permission = require('../utils/permission');
|
|
const { Resource, ResourceEdge, ResourceGroup } = require('../models/resource');
|
|
const { Group } = require('../models/group_ldap');
|
|
const { projectResources } = require('@simpleworkjs/directory-schema');
|
|
|
|
// Require the admin group
|
|
router.use(async (req, res, next) => {
|
|
try {
|
|
await permission.byGroup(req.user, ['app_sso_directory_admin', 'app_sso_admin']);
|
|
next();
|
|
} catch(err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// --- Resources ---
|
|
router.get('/resources', async (req, res, next) => {
|
|
try {
|
|
const resources = await Resource.list();
|
|
// Even admins never receive secret metadata (e.g. client_secret_hash) over
|
|
// the wire; projectResources strips it unconditionally.
|
|
res.json({ results: projectResources(resources, { fullMetadata: true }) });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.post('/resources', async (req, res, next) => {
|
|
try {
|
|
if (!req.body.hostId && req.body.parentSlug) {
|
|
const parents = await Resource.list({ where: { slug: req.body.parentSlug } });
|
|
if (parents.length > 0) req.body.hostId = parents[0].id;
|
|
}
|
|
|
|
if (req.body.kind === 'host' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'Hosts must have a parent Site or Host' });
|
|
}
|
|
if (req.body.kind === 'service' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'Services must have a parent Host' });
|
|
}
|
|
if (req.body.kind === 'oauth' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'OAuth Integrations must have a parent Service' });
|
|
}
|
|
|
|
req.body.owner = req.body.owner || req.user.uid;
|
|
|
|
let r;
|
|
if (req.body.kind === 'oauth') {
|
|
const { OAuthClient } = require('../models/oauth_client');
|
|
// Pass created_by explicitly for the wrapper
|
|
req.body.created_by = req.body.owner;
|
|
// In the UI we might pass slug, but OAuthClient wrapper expects name
|
|
r = await OAuthClient.add(req.body);
|
|
} else {
|
|
r = await Resource.create(req.body);
|
|
}
|
|
|
|
if ((r.kind === 'host' || r.kind === 'service' || r.kind === 'oauth') && req.body.hostId) {
|
|
await ResourceEdge.create({ parentId: req.body.hostId, childId: r.id, relation: r.kind === 'oauth' ? 'oauth' : 'hosts' });
|
|
}
|
|
|
|
if (r.kind === 'host' || r.kind === 'service') {
|
|
const createGroup = async (suffix, accessLevel) => {
|
|
const cn = `${r.slug}_${suffix}`;
|
|
try {
|
|
await Group.add({
|
|
name: cn,
|
|
owner: req.user.dn,
|
|
description: `${suffix === 'admin' ? 'Admin' : 'Access'} group for ${r.name}`
|
|
});
|
|
} catch (err) {
|
|
if (err.name !== 'EntryAlreadyExistsError' && err.code !== 68) {
|
|
console.error(`Failed to create LDAP group ${cn}:`, err);
|
|
}
|
|
}
|
|
try {
|
|
await ResourceGroup.create({ resourceId: r.id, groupCn: cn, accessLevel });
|
|
} catch(err) { /* ignore duplicate links */ }
|
|
};
|
|
await createGroup('access', 'member');
|
|
await createGroup('admin', 'owner');
|
|
}
|
|
|
|
res.json({ results: r });
|
|
} catch (err) {
|
|
if (err.name === 'SequelizeUniqueConstraintError') {
|
|
return res.status(400).json({ error: 'A resource with this slug already exists.' });
|
|
}
|
|
if (err.name === 'SequelizeValidationError') {
|
|
return res.status(400).json({ error: err.message });
|
|
}
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.put('/resources/:id', async (req, res, next) => {
|
|
try {
|
|
let r;
|
|
if (req.body.kind === 'oauth') {
|
|
const { OAuthClient } = require('../models/oauth_client');
|
|
r = await OAuthClient.get(req.params.id);
|
|
} else {
|
|
r = await Resource.get(req.params.id);
|
|
}
|
|
if (!r) return res.status(404).json({ error: 'Not found' });
|
|
|
|
if (req.body.kind === 'host' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'Hosts must have a parent Site or Host' });
|
|
}
|
|
if (req.body.kind === 'service' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'Services must have a parent Host' });
|
|
}
|
|
if (req.body.kind === 'oauth' && !req.body.hostId) {
|
|
return res.status(400).json({ error: 'OAuth Integrations must have a parent Service' });
|
|
}
|
|
|
|
let updated;
|
|
if (req.body.kind === 'oauth') {
|
|
updated = await r.update(req.body);
|
|
} else {
|
|
updated = await r.update(req.body);
|
|
}
|
|
|
|
if ((updated.kind === 'host' || updated.kind === 'service' || updated.kind === 'oauth') && req.body.hostId !== undefined) {
|
|
const existingEdges = await ResourceEdge.list({ where: { childId: r.id } });
|
|
for (const e of existingEdges) {
|
|
if (e.relation === 'hosts' || e.relation === 'oauth') await e.delete();
|
|
}
|
|
if (req.body.hostId) {
|
|
await ResourceEdge.create({ parentId: req.body.hostId, childId: r.id, relation: updated.kind === 'oauth' ? 'oauth' : 'hosts' });
|
|
}
|
|
}
|
|
|
|
res.json({ results: updated });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/resources/:id/rotate-secret', async (req, res, next) => {
|
|
try {
|
|
const { OAuthClient } = require('../models/oauth_client');
|
|
const client = await OAuthClient.get(req.params.id);
|
|
const secret = await client.rotateSecret();
|
|
res.json({ secret });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.delete('/resources/:id', async (req, res, next) => {
|
|
try {
|
|
const r = await Resource.get(req.params.id);
|
|
if (!r) return res.status(404).json({ error: 'Not found' });
|
|
await r.delete();
|
|
// Also delete edges and groups involving this resource
|
|
const edgesParent = await ResourceEdge.list({ where: { parentId: req.params.id } });
|
|
const edgesChild = await ResourceEdge.list({ where: { childId: req.params.id } });
|
|
const groups = await ResourceGroup.list({ where: { resourceId: req.params.id } });
|
|
for (const e of [...edgesParent, ...edgesChild]) await e.delete();
|
|
for (const g of groups) await g.delete();
|
|
res.json({ results: true });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// --- Edges ---
|
|
router.get('/edges', async (req, res, next) => {
|
|
try {
|
|
const edges = await ResourceEdge.list();
|
|
res.json({ results: edges });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.post('/edges', async (req, res, next) => {
|
|
try {
|
|
const edge = await ResourceEdge.create(req.body);
|
|
res.json({ results: edge });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.delete('/edges/:id', async (req, res, next) => {
|
|
try {
|
|
const edge = await ResourceEdge.get(req.params.id);
|
|
if (!edge) return res.status(404).json({ error: 'Not found' });
|
|
await edge.delete();
|
|
res.json({ results: true });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// --- Groups ---
|
|
router.get('/groups', async (req, res, next) => {
|
|
try {
|
|
const groups = await ResourceGroup.list();
|
|
res.json({ results: groups });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.post('/groups', async (req, res, next) => {
|
|
try {
|
|
const g = await ResourceGroup.create(req.body);
|
|
res.json({ results: g });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.delete('/groups/:id', async (req, res, next) => {
|
|
try {
|
|
const g = await ResourceGroup.get(req.params.id);
|
|
if (!g) return res.status(404).json({ error: 'Not found' });
|
|
await g.delete();
|
|
res.json({ results: true });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
router.get('/audit-logs', async (req, res, next) => {
|
|
try {
|
|
const fs = require('fs');
|
|
const { execSync } = require('child_process');
|
|
let ldapLogs = '';
|
|
let oauthLogs = '';
|
|
let auditLogs = '';
|
|
|
|
try { ldapLogs = execSync('tail -n 100 /var/lib/ldap/slapd.log 2>/dev/null').toString(); } catch(e){}
|
|
try { oauthLogs = execSync('tail -n 100 /var/lib/ldap/oauth.log 2>/dev/null').toString(); } catch(e){}
|
|
try { auditLogs = execSync('tail -n 100 /var/lib/ldap/auditlog.ldif 2>/dev/null').toString(); } catch(e){}
|
|
|
|
res.json({ results: { ldap: ldapLogs, oauth: oauthLogs, audit: auditLogs } });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
module.exports = router;
|