Files
sso-manager-node/nodejs/tests/api_agent_ops.test.js
T
wmantly 181ca8c9cb 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>
2026-08-07 17:03:41 -04:00

73 lines
2.1 KiB
JavaScript

'use strict';
// Agent-facing ops (DESIGN.md §5): node-scoped secrets. OpenBao is not present
// in the test env, so @simpleworkjs/bao-conf is mocked.
jest.mock('@simpleworkjs/bao-conf', () => ({
request: jest.fn(async (method, path) => {
if (path.startsWith('secret/data/nodes/')) {
return {
ok: true,
status: 200,
json: async () => ({ data: { data: { username: 'alice', password: 's3cret' } } }),
};
}
return { ok: false, status: 404, json: async () => ({}) };
}),
}));
const { request, app } = require('./setup');
const { Agent } = require('../models/agent');
async function enrollAgent() {
const { agent, token } = await Agent.enroll({
name: `ops-test-${Date.now().toString(36)}`,
description: 'api_agent_ops test',
enrolledBy: 'test'
});
return { agent, token };
}
describe('Agent ops — POST /api/v1/agent/secrets', () => {
test('an agent can fetch its own node-scoped secrets', async () => {
const { agent, token } = await enrollAgent();
const path = `secret/data/nodes/${agent.id}/db`;
const res = await request(app)
.post('/api/v1/agent/secrets')
.set('Authorization', `Bearer ${token}`)
.send({ paths: [path] });
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.secrets[path]).toEqual({ username: 'alice', password: 's3cret' });
});
test('a path outside the node scope is rejected', async () => {
const { token } = await enrollAgent();
const res = await request(app)
.post('/api/v1/agent/secrets')
.set('Authorization', `Bearer ${token}`)
.send({ paths: ['secret/data/nodes/other-node/db'] });
expect(res.status).toBe(403);
});
test('no bearer token returns 401', async () => {
const res = await request(app)
.post('/api/v1/agent/secrets')
.send({ paths: ['secret/data/nodes/x/db'] });
expect(res.status).toBe(401);
});
test('missing paths returns 400', async () => {
const { token } = await enrollAgent();
const res = await request(app)
.post('/api/v1/agent/secrets')
.set('Authorization', `Bearer ${token}`)
.send({});
expect(res.status).toBe(400);
});
});