verae-staff-iam/test/health.test.js
George Lambert ca117eb458 feat: LAB_CA_ENABLE opt-in lab mint via lab-test-ca.sh (prod refused)
POST /enroll/csr returns 201 + X-Lab-CA when lab-pki ready; default 501.
2026-10-14 01:08:21 -04:00

716 lines
29 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } 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 catalog = await (await fetch(`http://127.0.0.1:${port}/roles/catalog`)).json();
assert.deepEqual(catalog.roles, ['starter', 'ender', 'admin']);
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'));
const patRoles = await (await fetch(`http://127.0.0.1:${port}/users/pat/roles`)).json();
assert.equal(patRoles.username, 'pat');
assert.deepEqual(patRoles.roles, ['sales']);
assert.equal(patRoles.active, true);
const byId = await (await fetch(`http://127.0.0.1:${port}/users/${encodeURIComponent(body.id)}/roles`)).json();
assert.deepEqual(byId.roles, ['sales']);
const missing = await fetch(`http://127.0.0.1:${port}/users/no-such-user/roles`);
assert.equal(missing.status, 404);
assert.deepEqual((await missing.json()).roles, []);
const enderUser = await fetch(`http://127.0.0.1:${port}/users`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ username: 'ender1', password: 'ender-pass-1', name: 'Ender', roles: ['ender', 'starter'] }),
});
assert.equal(enderUser.status, 201);
assert.deepEqual((await enderUser.json()).roles.sort(), ['ender', 'starter']);
/* 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)));
/* I4: always advertise CSR schema URL + honest 501 for POST mint */
assert.equal(enroll.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(enroll.csrPostStatus, 501);
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);
assert.equal(enrollAuth.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(enrollAuth.csrPostStatus, 501);
/* 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: valid-looking PEM shape → 501 scaffold, never mints */
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));
/* invalid PEM shape → 400 (no mint) */
const csrBad = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: hdr,
body: JSON.stringify({ csr: 'not-a-real-csr' }),
});
assert.equal(csrBad.status, 400);
const csrBadBody = await csrBad.json();
assert.equal(csrBadBody.error, 'invalid_csr_pem');
assert.ok(!('certificate' in csrBadBody) && !('pem' in csrBadBody) && !('cert' in csrBadBody));
/* empty body → 501 scaffold (no csr field) */
const csrEmpty = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({}),
});
assert.equal(csrEmpty.status, 501);
assert.equal((await csrEmpty.json()).error, 'not_implemented');
/* audit: csr_attempt without PEM body */
const auditRes = await fetch(`http://127.0.0.1:${port}/audit`, { headers: hdr });
assert.equal(auditRes.status, 200);
const { audit } = await auditRes.json();
assert.ok(Array.isArray(audit));
const csrAttempts = audit.filter((a) => a.action === 'csr_attempt');
assert.ok(csrAttempts.length >= 3);
for (const a of csrAttempts) {
const d = String(a.detail || '');
assert.ok(/outcome=/.test(d));
assert.ok(/pemShape=/.test(d));
assert.ok(!/BEGIN CERTIFICATE REQUEST/i.test(d));
assert.ok(!/not-a-real-csr/.test(d));
assert.ok(!/FAKE/.test(JSON.stringify(a)));
}
assert.ok(csrAttempts.some((a) => /outcome=scaffold_501/.test(String(a.detail))));
assert.ok(csrAttempts.some((a) => /outcome=invalid_pem_400/.test(String(a.detail))));
} finally {
child.kill('SIGTERM');
}
});
test('POST /enroll/csr PEM shape: invalid → 400; valid-looking → 501; never mints', async () => {
const port = 18038;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-csr-pem-'));
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 cases400 = [
'not-a-real-csr',
'-----BEGIN CERTIFICATE-----\nABC\n-----END CERTIFICATE-----',
'-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST-----',
'-----BEGIN PRIVATE KEY-----\nABC\n-----END PRIVATE KEY-----',
'-----BEGIN CERTIFICATE REQUEST-----\nABC',
42,
];
for (const csr of cases400) {
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ csr }),
});
assert.equal(r.status, 400, `expected 400 for ${JSON.stringify(csr)}`);
const b = await r.json();
assert.equal(b.error, 'invalid_csr_pem');
assert.ok(!('certificate' in b) && !('pem' in b) && !('pkcs12' in b));
}
const okPems = [
'-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----',
'-----BEGIN NEW CERTIFICATE REQUEST-----\nQUJD\n-----END NEW CERTIFICATE REQUEST-----',
];
for (const csr of okPems) {
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ csr }),
});
assert.equal(r.status, 501, 'valid-looking PEM must still be scaffold 501');
const b = await r.json();
assert.equal(b.error, 'not_implemented');
assert.ok(!('certificate' in b) && !('pem' in b) && !('cert' in b) && !('pkcs12' in b));
}
} 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);
assert.equal(before.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(before.csrPostStatus, 501);
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)));
assert.equal(mid.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(mid.csrPostStatus, 501);
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);
assert.equal(withCert.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(withCert.csrPostStatus, 501);
} 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);
assert.equal(pending.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(pending.csrPostStatus, 501);
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);
assert.equal(enrolled.csrSchemaUrl, '/enroll/csr/schema');
assert.equal(enrolled.csrPostStatus, 501);
} finally {
child.kill('SIGTERM');
}
});
function haveOpenssl() {
try {
const r = spawnSync('openssl', ['version'], { encoding: 'utf8' });
return r.status === 0;
} catch {
return false;
}
}
test('POST /enroll/csr default LAB_CA_ENABLE off → still 501 (no cert)', async () => {
const port = 18048;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-lab-off-'));
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
LAB_CA_ENABLE: '',
NODE_ENV: 'test',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const st = await (await fetch(`http://127.0.0.1:${port}/enroll/status`)).json();
assert.equal(st.csrPostStatus, 501);
assert.equal(st.labCa.enabled, false);
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
csr: '-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----',
}),
});
assert.equal(r.status, 501);
assert.equal(r.headers.get('x-lab-ca'), null);
const b = await r.json();
assert.equal(b.error, 'not_implemented');
assert.ok(!('certificate' in b));
} finally {
child.kill('SIGTERM');
}
});
test('POST /enroll/csr LAB_CA_ENABLE=1 without lab-pki → still 501', async () => {
const port = 18049;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-lab-nopki-'));
const emptyPki = path.join(dir, 'empty-pki');
fs.mkdirSync(emptyPki);
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
LAB_CA_ENABLE: '1',
LAB_PKI_DIR: emptyPki,
NODE_ENV: 'test',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const st = await (await fetch(`http://127.0.0.1:${port}/enroll/status`)).json();
assert.equal(st.labCa.enabled, true);
assert.equal(st.labCa.ready, false);
assert.equal(st.csrPostStatus, 501);
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
csr: '-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----',
}),
});
assert.equal(r.status, 501);
assert.equal(r.headers.get('x-lab-ca'), null);
assert.equal((await r.json()).error, 'not_implemented');
} finally {
child.kill('SIGTERM');
}
});
test('POST /enroll/csr LAB_CA_ENABLE=1 + lab-pki → 201 + X-Lab-CA (skip if no openssl)', async (t) => {
if (!haveOpenssl()) {
t.skip('openssl not on PATH');
return;
}
const port = 18050;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-lab-on-'));
const pki = path.join(dir, 'lab-pki');
const init = spawnSync('bash', [path.join(root, 'scripts/lab-test-ca.sh'), 'init', '--dir', pki], {
encoding: 'utf8',
});
if (init.status !== 0) {
t.skip(`lab-test-ca init failed: ${init.stderr || init.stdout}`);
return;
}
const key = path.join(dir, 'dev.key');
const csrPath = path.join(dir, 'dev.csr');
const gen = spawnSync(
'openssl',
[
'req',
'-new',
'-newkey',
'rsa:2048',
'-nodes',
'-keyout',
key,
'-out',
csrPath,
'-subj',
'/CN=lab-user@staff.local',
],
{ encoding: 'utf8' },
);
if (gen.status !== 0) {
t.skip(`openssl req failed: ${gen.stderr}`);
return;
}
const csrPem = fs.readFileSync(csrPath, 'utf8');
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
LAB_CA_ENABLE: '1',
LAB_PKI_DIR: pki,
NODE_ENV: 'test',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 600));
try {
const st = await (await fetch(`http://127.0.0.1:${port}/enroll/status`)).json();
assert.equal(st.labCa.enabled, true);
assert.equal(st.labCa.ready, true);
assert.equal(st.csrPostStatus, 201);
assert.match(String(st.labCa.warning), /NOT FOR PRODUCTION/i);
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ csr: csrPem, commonName: 'lab-user@staff.local' }),
});
assert.equal(r.status, 201);
assert.equal(r.headers.get('x-lab-ca'), '1');
assert.equal(r.headers.get('x-lab-ca-warning'), 'NOT-FOR-PRODUCTION');
const b = await r.json();
assert.equal(b.ok, true);
assert.equal(b.lab, true);
assert.match(String(b.warning), /NOT FOR PRODUCTION/i);
assert.match(b.certificate, /-----BEGIN CERTIFICATE-----/);
assert.match(b.certificate, /-----END CERTIFICATE-----/);
assert.ok(!('privateKey' in b) && !('pkcs12' in b));
} finally {
child.kill('SIGTERM');
}
});
test('POST /enroll/csr NODE_ENV=production refuses lab even if LAB_CA_ENABLE=1', async () => {
const port = 18051;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iam-lab-prod-'));
const pki = path.join(dir, 'lab-pki');
fs.mkdirSync(pki);
fs.writeFileSync(path.join(pki, 'lab-root.key.pem'), 'x');
fs.writeFileSync(path.join(pki, 'lab-root.crt.pem'), 'x');
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(port),
STAFF_IAM_PATH: path.join(dir, 'iam.json'),
LAB_CA_ENABLE: '1',
LAB_PKI_DIR: pki,
NODE_ENV: 'production',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const st = await (await fetch(`http://127.0.0.1:${port}/enroll/status`)).json();
assert.equal(st.labCa.enabled, false);
assert.equal(st.csrPostStatus, 501);
const r = await fetch(`http://127.0.0.1:${port}/enroll/csr`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
csr: '-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----',
}),
});
assert.equal(r.status, 501);
assert.equal(r.headers.get('x-lab-ca'), null);
} finally {
child.kill('SIGTERM');
}
});