feat: actionable metrics, LDAP log parsing, UI updates

This commit is contained in:
2026-07-22 21:58:06 -04:00
parent a100f755ce
commit c4d7a1a8e9
48 changed files with 3158 additions and 2106 deletions
+227
View File
@@ -0,0 +1,227 @@
'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');
// 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();
res.json({ results: resources });
} 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;