verae-staff-iam/test/health.test.js
George Lambert ae820a5db6 feat: GET /enroll/csr/schema field map (mint still 501)
Schema-only CSR contract for future live PKI; no fake crypto.
2026-10-13 23:08:58 -04:00

403 lines
18 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
test('iam login, permission check, user CRUD', async () => {
const port = 18028;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-'));
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: { ...process.env, PORT: String(port), STAFF_IAM_PATH: path.join(dir, 'iam.json') },
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const h = await (await fetch(`http://127.0.0.1:${port}/health`)).json();
assert.equal(h.role, 'verae-staff-iam');
const login = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(login.status, 200);
const { token, user } = await login.json();
assert.ok(user.permissions.includes('*'));
const hdr = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
const ok = await fetch(`http://127.0.0.1:${port}/check?permission=cs.credit`, { headers: hdr });
assert.equal(ok.status, 200);
const csLogin = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'cs', password: 'cs-dev-key' }),
});
const cs = await csLogin.json();
const deny = await fetch(`http://127.0.0.1:${port}/check?permission=accounting.export`, {
headers: { authorization: `Bearer ${cs.token}` },
});
assert.equal(deny.status, 403);
const created = await fetch(`http://127.0.0.1:${port}/users`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ username: 'pat', password: 'pat-pass-99', name: 'Pat', roles: ['sales'] }),
});
assert.equal(created.status, 201);
const body = await created.json();
assert.deepEqual(body.roles, ['sales']);
const list = await (await fetch(`http://127.0.0.1:${port}/users`, { headers: hdr })).json();
assert.ok(list.users.some((u) => u.username === 'pat'));
/* TOTP: setup → enable → login requires code */
const { totp, verifyTotp } = await import('../src/totp.js');
assert.ok(verifyTotp);
const setup = await fetch(`http://127.0.0.1:${port}/2fa/setup`, { method: 'POST', headers: hdr });
assert.equal(setup.status, 200);
const setupBody = await setup.json();
assert.ok(setupBody.secret);
assert.ok(String(setupBody.uri).startsWith('otpauth://totp/'));
const code = totp(setupBody.secret, Date.now());
const en = await fetch(`http://127.0.0.1:${port}/2fa/enable`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ code }),
});
assert.equal(en.status, 200);
const noCode = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(noCode.status, 401);
assert.equal((await noCode.json()).error, 'totp_required');
const withCode = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
username: 'admin',
password: 'admin-dev-key',
totpCode: totp(setupBody.secret, Date.now()),
}),
});
assert.equal(withCode.status, 200);
assert.ok((await withCode.json()).token);
const enroll = await (await fetch(`http://127.0.0.1:${port}/enroll/status`)).json();
assert.equal(enroll.ok, true);
assert.equal(enroll.mtlsEnforce, false);
assert.equal(enroll.state, 'not_enrolled');
assert.equal(enroll.stateLabel, 'Not enrolled');
assert.ok(typeof enroll.note === 'string');
assert.ok(typeof enroll.mint === 'string');
assert.ok(typeof enroll.renew === 'string');
assert.ok(Array.isArray(enroll.nextSteps));
assert.ok(enroll.nextSteps.length >= 3);
assert.ok(enroll.nextSteps.every((s) => typeof s === 'string' && s.length > 0));
assert.ok(enroll.nextSteps.some((s) => /mtlsEnrolled|PUT \/users/i.test(s)));
assert.ok(enroll.nextSteps.some((s) => /proxy|PKI/i.test(s)));
assert.ok(enroll.nextSteps.some((s) => /POST \/enroll\/csr|501|not_implemented/i.test(s)));
assert.ok(enroll.proxy && typeof enroll.proxy === 'object');
assert.equal(enroll.proxy.verifyOk, false);
assert.equal(enroll.mtlsPresent, false);
assert.match(enroll.updatedAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
assert.ok(!Number.isNaN(Date.parse(enroll.updatedAt)));
assert.ok(enroll.proxyHeadersSeen && typeof enroll.proxyHeadersSeen === 'object');
assert.equal(enroll.proxyHeadersSeen.verify, false);
assert.equal(enroll.proxyHeadersSeen.verifyOk, false);
assert.equal(enroll.proxyHeadersSeen.dn, false);
assert.equal(enroll.proxyHeadersSeen.pem, false);
assert.equal(enroll.proxyHeadersSeen.any, false);
/* signed-in: still not_enrolled until admin marks mtlsEnrolled */
const enrollAuth = await (
await fetch(`http://127.0.0.1:${port}/enroll/status`, { headers: hdr })
).json();
assert.equal(enrollAuth.signedIn, true);
assert.equal(enrollAuth.state, 'not_enrolled');
assert.equal(enrollAuth.mtlsEnrolled, false);
assert.ok(Array.isArray(enrollAuth.nextSteps) && enrollAuth.nextSteps.length >= 3);
assert.ok(enrollAuth.nextSteps.some((s) => /POST \/enroll\/csr|501|not_implemented/i.test(s)));
assert.match(enrollAuth.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(enrollAuth.proxyHeadersSeen.any, false);
/* GET /enroll/csr/schema — fields only, no mint */
const schemaRes = await fetch(`http://127.0.0.1:${port}/enroll/csr/schema`);
assert.equal(schemaRes.status, 200);
const schema = await schemaRes.json();
assert.equal(schema.ok, true);
assert.equal(schema.mint, false);
assert.equal(schema.status, 'schema_only');
assert.equal(schema.path, 'POST /enroll/csr');
assert.ok(Array.isArray(schema.fields) && schema.fields.length >= 1);
assert.ok(schema.fields.every((f) => f && typeof f.name === 'string' && typeof f.type === 'string'));
assert.ok(schema.fields.some((f) => f.name === 'csr' && f.required === true));
assert.ok(Array.isArray(schema.notReturned));
assert.ok(schema.notReturned.includes('certificate') || schema.notReturned.includes('pem'));
assert.ok(!('certificate' in schema) && !('pem' in schema) && !('pkcs12' in schema));
const schemaBlob = JSON.stringify(schema);
/* Descriptions may name PEM armor labels; forbid actual PEM blocks / keys. */
assert.ok(!/-----BEGIN[^-]*-----[\s\S]*-----END/i.test(schemaBlob));
assert.ok(!/BEGIN PRIVATE KEY/i.test(schemaBlob));
/* POST /enroll/csr is scaffold-only: always 501, never mints a cert */
const csrAnon = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ csr: '-----BEGIN CERTIFICATE REQUEST-----\nFAKE\n-----END CERTIFICATE REQUEST-----' }),
});
assert.equal(csrAnon.status, 501);
const csrAnonBody = await csrAnon.json();
assert.equal(csrAnonBody.error, 'not_implemented');
assert.ok(Array.isArray(csrAnonBody.nextSteps) && csrAnonBody.nextSteps.length >= 3);
assert.ok(csrAnonBody.nextSteps.every((s) => typeof s === 'string' && s.length > 0));
assert.ok(csrAnonBody.nextSteps.some((s) => /proxy|PKI/i.test(s)));
assert.ok(csrAnonBody.nextSteps.some((s) => /csr\/schema/i.test(s)));
assert.ok(!('certificate' in csrAnonBody) && !('pem' in csrAnonBody) && !('cert' in csrAnonBody));
const csrAuth = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ csr: 'not-a-real-csr' }),
});
assert.equal(csrAuth.status, 501);
const csrAuthBody = await csrAuth.json();
assert.equal(csrAuthBody.error, 'not_implemented');
assert.ok(Array.isArray(csrAuthBody.nextSteps) && csrAuthBody.nextSteps.length >= 3);
} finally {
child.kill('SIGTERM');
}
});
test('STAFF_TOTP_REQUIRED blocks full access until enroll', async () => {
const port = 18030;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-totp-req-'));
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
STAFF_TOTP_REQUIRED: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const health = await (await fetch(`http://127.0.0.1:${port}/health`)).json();
assert.equal(health.totpRequired, true);
const login = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(login.status, 200);
const loginBody = await login.json();
assert.equal(loginBody.totpEnrollmentRequired, true);
assert.equal(loginBody.user.totpEnrollmentRequired, true);
assert.equal(loginBody.user.totpEnabled, false);
const token = loginBody.token;
const hdr = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
const me = await (await fetch(`http://127.0.0.1:${port}/me`, { headers: hdr })).json();
assert.equal(me.user.totpEnrollmentRequired, true);
assert.equal(me.user.totpRequired, true);
const users = await fetch(`http://127.0.0.1:${port}/users`, { headers: hdr });
assert.equal(users.status, 403);
assert.equal((await users.json()).error, 'totp_enrollment_required');
const check = await fetch(`http://127.0.0.1:${port}/check?permission=iam.users.read`, { headers: hdr });
assert.equal(check.status, 403);
assert.equal((await check.json()).error, 'totp_enrollment_required');
const { totp } = await import('../src/totp.js');
const setup = await fetch(`http://127.0.0.1:${port}/2fa/setup`, { method: 'POST', headers: hdr });
assert.equal(setup.status, 200);
const setupBody = await setup.json();
assert.ok(setupBody.secret);
assert.ok(setupBody.uri);
const en = await fetch(`http://127.0.0.1:${port}/2fa/enable`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ code: totp(setupBody.secret, Date.now()) }),
});
assert.equal(en.status, 200);
assert.equal((await en.json()).totpEnrollmentRequired, false);
const usersOk = await fetch(`http://127.0.0.1:${port}/users`, { headers: hdr });
assert.equal(usersOk.status, 200);
const dis = await fetch(`http://127.0.0.1:${port}/2fa/disable`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ code: totp(setupBody.secret, Date.now()) }),
});
assert.equal(dis.status, 403);
assert.equal((await dis.json()).error, 'totp_required_by_policy');
const noCode = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(noCode.status, 401);
assert.equal((await noCode.json()).error, 'totp_required');
} finally {
child.kill('SIGTERM');
}
});
test('iam mTLS gate uses proxy headers only when enrolled + enforce', async () => {
const port = 18029;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-mtls-'));
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
STAFF_MTLS_ENFORCE: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const login = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(login.status, 200);
const { token } = await login.json();
const hdr = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
/* enforce on but user not enrolled yet → not_enrolled */
const before = await (await fetch(`http://127.0.0.1:${port}/enroll/status`, { headers: hdr })).json();
assert.equal(before.state, 'not_enrolled');
assert.equal(before.mtlsEnforce, true);
await fetch(`http://127.0.0.1:${port}/users/admin`, {
method: 'PUT',
headers: hdr,
body: JSON.stringify({ mtlsEnrolled: true }),
});
/* enrolled + enforce, no cert headers → enforce_on */
const mid = await (await fetch(`http://127.0.0.1:${port}/enroll/status`, { headers: hdr })).json();
assert.equal(mid.state, 'enforce_on');
assert.equal(mid.mtlsEnrolled, true);
assert.equal(mid.mtlsPresent, false);
assert.ok(String(mid.note).toLowerCase().includes('enforce') || String(mid.mint).includes('proxy'));
assert.ok(Array.isArray(mid.nextSteps) && mid.nextSteps.length >= 3);
assert.ok(mid.nextSteps.some((s) => /401|error.?cert|proxy/i.test(s)));
const blocked = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(blocked.status, 401);
assert.equal((await blocked.json()).error, 'cert');
const ok = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-ssl-client-verify': 'SUCCESS',
},
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(ok.status, 200);
/* same request shape for status with cert header → still enforce_on, present true */
const withCert = await (
await fetch(`http://127.0.0.1:${port}/enroll/status`, {
headers: { ...hdr, 'x-ssl-client-verify': 'SUCCESS' },
})
).json();
assert.equal(withCert.state, 'enforce_on');
assert.equal(withCert.mtlsPresent, true);
assert.equal(withCert.proxy.verifyOk, true);
assert.equal(withCert.proxy.verify, 'SUCCESS');
assert.match(withCert.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(withCert.proxyHeadersSeen.verify, true);
assert.equal(withCert.proxyHeadersSeen.verifyOk, true);
assert.equal(withCert.proxyHeadersSeen.dn, false);
assert.equal(withCert.proxyHeadersSeen.pem, false);
assert.equal(withCert.proxyHeadersSeen.any, true);
} finally {
child.kill('SIGTERM');
}
});
test('enroll/status states: pending_proxy and enrolled (enforce off)', async () => {
const port = 18031;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-enroll-states-'));
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
/* enforce off */
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const login = await fetch(`http://127.0.0.1:${port}/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin-dev-key' }),
});
assert.equal(login.status, 200);
const { token } = await login.json();
const hdr = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
await fetch(`http://127.0.0.1:${port}/users/admin`, {
method: 'PUT',
headers: hdr,
body: JSON.stringify({ mtlsEnrolled: true }),
});
const pending = await (await fetch(`http://127.0.0.1:${port}/enroll/status`, { headers: hdr })).json();
assert.equal(pending.state, 'pending_proxy');
assert.equal(pending.stateLabel, 'Enrolled — waiting for proxy cert');
assert.ok(pending.mint.includes('proxy'));
assert.ok(pending.renew.includes('proxy'));
assert.ok(Array.isArray(pending.nextSteps) && pending.nextSteps.length >= 3);
assert.ok(pending.nextSteps.some((s) => /present|proxy|header/i.test(s)));
assert.match(pending.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(pending.proxyHeadersSeen.any, false);
assert.equal(pending.proxyHeadersSeen.dn, false);
const enrolled = await (
await fetch(`http://127.0.0.1:${port}/enroll/status`, {
headers: {
...hdr,
'x-ssl-client-dn': 'CN=admin-workstation,O=Verae',
},
})
).json();
assert.equal(enrolled.state, 'enrolled');
assert.equal(enrolled.mtlsPresent, true);
assert.equal(enrolled.proxy.hasDn, true);
assert.equal(enrolled.proxy.subjectDn, 'CN=admin-workstation,O=Verae');
assert.ok(enrolled.note.toLowerCase().includes('seen') || enrolled.note.toLowerCase().includes('certificate'));
assert.ok(Array.isArray(enrolled.nextSteps) && enrolled.nextSteps.length >= 3);
assert.ok(enrolled.nextSteps.some((s) => /renew|enforce|no action/i.test(s)));
assert.match(enrolled.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(enrolled.proxyHeadersSeen.verify, false);
assert.equal(enrolled.proxyHeadersSeen.verifyOk, false);
assert.equal(enrolled.proxyHeadersSeen.dn, true);
assert.equal(enrolled.proxyHeadersSeen.pem, false);
assert.equal(enrolled.proxyHeadersSeen.any, true);
} finally {
child.kill('SIGTERM');
}
});