Add LDAP-over-HTTPS API, agent secrets/IAM engines, and join key management
See CHANGELOG.md for the full breakdown. Summary:
- POST /api/v1/ldap/{bind,search}: LDAP-over-HTTPS so a client stops
speaking raw LDAP and instead calls the SSO, which binds/searches its
own OpenLDAP on the caller's behalf (DESIGN.md §3).
- LDAP byte-pump relay (utils/ldap_tunnel.js): forwards raw LDAP bytes
from an agent's local socket into OpenLDAP over the existing agent WSS
channel; the SSO never parses LDAP (DESIGN.md §4).
- POST /api/v1/agent/secrets: node-scoped OpenBao secret fetch for
agents, enforced to each agent's own secret/data/nodes/<id>/* prefix
(DESIGN.md §5).
- iam_apply signed command: push node-scoped IAM config (sudo rules, SSH
keys, access control, revocation) to an agent (DESIGN.md §6).
- Agent capability badges on the Directory Metrics tab, sourced from the
agent's own discovery frame.
- Join key management: GET /api/agent/join-keys/:id/agents (which hosts
enrolled through a key) plus a Manage join keys table in the Install
Agent modal with Revoke/Delete actions, confirmed inline per-row rather
than a blocking native confirm() or the shared app.messages.confirm()
banner (which desyncs across concurrent rows -- see CHANGELOG).
- docs/agents.md: capability matrix updated for the three new
capabilities, a full secrets-engine walkthrough with screenshots
(bash + Node consuming a rendered secret, plus the direct-API
alternative), and the join-key reuse/UI/audit questions answered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
// Agent-facing operations (DESIGN.md §5, §6). These are NOT admin-gated: the
|
||||
// caller is the agent itself, authenticated by its own token (the same one it
|
||||
// presents on its WSS channel). Mounted at /api/v1/agent.
|
||||
|
||||
const express = require('express');
|
||||
const baoConf = require('@simpleworkjs/bao-conf');
|
||||
const { authenticateAgent } = require('../utils/agent_auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// POST /secrets — fetch node-scoped OpenBao secrets for the agent's own node.
|
||||
//
|
||||
// { paths: ["secret/data/nodes/<agent-id>/db"] }
|
||||
// -> { status: "ok", secrets: { "secret/data/nodes/<agent-id>/db": { key: value } } }
|
||||
//
|
||||
// The agent may only read under its own node prefix (secret/data/nodes/<id>/*),
|
||||
// so a compromised agent cannot reach other nodes' or shared secrets. The SSO
|
||||
// fetches with its own OpenBao access (SSO_VAULT_TOKEN); the agent never holds a
|
||||
// Vault token.
|
||||
router.post('/secrets', async (req, res, next) => {
|
||||
try {
|
||||
const agent = await authenticateAgent(req);
|
||||
if (!agent) return res.status(401).json({ status: 'error', message: 'unauthorized' });
|
||||
|
||||
const { paths } = req.body || {};
|
||||
if (!Array.isArray(paths) || paths.length === 0) {
|
||||
return res.status(400).json({ status: 'error', message: 'paths (array) is required' });
|
||||
}
|
||||
|
||||
const nodeScope = `secret/data/nodes/${agent.id}/`;
|
||||
const secrets = {};
|
||||
for (const p of paths) {
|
||||
if (typeof p !== 'string' || !p.startsWith(nodeScope)) {
|
||||
return res.status(403).json({ status: 'error', message: `path outside node scope: ${p}` });
|
||||
}
|
||||
const r = await baoConf.request('GET', p);
|
||||
if (r.ok) {
|
||||
const body = await r.json().catch(() => ({}));
|
||||
secrets[p] = (body.data && body.data.data) || {};
|
||||
} else {
|
||||
// Missing secret: return an empty object for that path rather than
|
||||
// failing the whole batch; the agent renders what it can.
|
||||
secrets[p] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ status: 'ok', secrets });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user