Files
qinglong/packages/ql3-cluster-control/test/admissionPipeline.test.cjs
T

283 lines
7.4 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ClusterControlAdmissionSecurityError,
createClusterControlAdmissionPipeline,
createClusterControlProjectPolicyAuthorizer,
} = require('@qinglong/cluster-control/admission');
const {
createClusterControlRouteRegistry,
} = require('@qinglong/cluster-control/routes');
const NOW = 10_000;
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'usr_primary' }),
authenticationId: 'session:abc123',
authenticatedAtMs: 9_000,
expiresAtMs: 11_000,
assurance: 'multi_factor',
});
const METADATA = Object.freeze({
requestId: 'request-1',
method: 'POST',
path: '/api/v3/projects/prj_default/runs',
query: Object.freeze({}),
headers: Object.freeze({ authorization: 'Bearer opaque' }),
signal: new AbortController().signal,
});
function options(overrides = {}) {
const events = overrides.events ?? [];
return {
routes: createClusterControlRouteRegistry([
{
method: 'POST',
path: '/api/v3/projects/{projectId}/runs',
operationId: 'run.create',
permission: 'run.start',
projectParameter: 'projectId',
handle(input, parameters) {
events.push(
`handle:${parameters.projectId}:${input.request.body.taskId}`,
);
return { statusCode: 202, body: { accepted: true } };
},
},
]),
authenticator: {
authenticate() {
events.push('authenticate');
return PRINCIPAL;
},
},
policy: {
authorize(request) {
events.push(`authorize:${request.permission}`);
return {
effect: 'allow',
reasons: ['role_grant'],
fence: { projectVersion: 2, bindingVersion: 3 },
};
},
},
audit: {
record(record) {
events.push(`audit:${record.outcome}`);
},
},
now: () => NOW,
...overrides,
};
}
test('authenticates, authorizes and audits before accepting a body', async () => {
const events = [];
const pipeline = createClusterControlAdmissionPipeline(options({ events }));
const prepared = await pipeline.prepare(METADATA);
assert.deepEqual(events, [
'authenticate',
'authorize:run.start',
'audit:allowed',
]);
assert.deepEqual(await prepared.handle({ taskId: 'task-1' }), {
statusCode: 202,
body: { accepted: true },
});
assert.deepEqual(events.slice(-1), ['handle:prj_default:task-1']);
});
test('requires a reviewed route registry and rejects unknown routes before authentication', async () => {
assert.throws(
() =>
createClusterControlAdmissionPipeline(
options({
routes: {
contractVersion: 1,
size: 1,
resolve() {
return null;
},
},
}),
),
/options are invalid/,
);
const events = [];
const pipeline = createClusterControlAdmissionPipeline(options({ events }));
await assert.rejects(
pipeline.prepare({
...METADATA,
path: '/api/v3/projects/prj_default/tasks',
}),
(error) =>
error instanceof ClusterControlAdmissionSecurityError &&
error.statusCode === 404 &&
error.code === 'route_not_found',
);
assert.deepEqual(events, []);
});
test('rejects missing authentication before policy and handler execution', async () => {
const events = [];
const pipeline = createClusterControlAdmissionPipeline(
options({
events,
authenticator: {
authenticate() {
events.push('authenticate');
return null;
},
},
}),
);
await assert.rejects(
pipeline.prepare(METADATA),
(error) =>
error instanceof ClusterControlAdmissionSecurityError &&
error.statusCode === 401 &&
error.code === 'authentication_required',
);
assert.equal(events.includes('authorize:run.start'), false);
assert.equal(
events.some((event) => event.startsWith('handle:')),
false,
);
assert.equal(events.includes('audit:authentication_rejected'), true);
});
test('maps policy decisions to low-sensitive deny and approval responses', async () => {
for (const [effect, code, outcome] of [
['deny', 'forbidden', 'denied'],
['require_approval', 'approval_required', 'approval_required'],
]) {
const events = [];
const pipeline = createClusterControlAdmissionPipeline(
options({
events,
policy: {
authorize() {
return { effect, reasons: ['policy_decision'], fence: null };
},
},
}),
);
await assert.rejects(
pipeline.prepare(METADATA),
(error) =>
error instanceof ClusterControlAdmissionSecurityError &&
error.statusCode === 403 &&
error.code === code &&
!error.message.includes('policy_decision'),
);
assert.equal(events.includes(`audit:${outcome}`), true);
}
});
test('fails closed when authentication, policy or security audit is unavailable', async () => {
const scenarios = [
{
override: {
authenticator: {
authenticate() {
throw new Error('identity database detail');
},
},
},
code: 'authentication_unavailable',
},
{
override: {
policy: {
authorize() {
throw new Error('policy database detail');
},
},
},
code: 'authorization_unavailable',
},
{
override: {
audit: {
record() {
throw new Error('audit store detail');
},
},
},
code: 'security_audit_unavailable',
},
];
for (const scenario of scenarios) {
const pipeline = createClusterControlAdmissionPipeline(
options(scenario.override),
);
await assert.rejects(
pipeline.prepare(METADATA),
(error) =>
error instanceof ClusterControlAdmissionSecurityError &&
error.statusCode === 503 &&
error.code === scenario.code &&
!error.message.includes('database detail') &&
!error.message.includes('store detail'),
);
}
});
test('adapts the shared fenced Project Policy engine without an allow-all seam', async () => {
const policy = createClusterControlProjectPolicyAuthorizer({
async resolve(projectId, subject) {
assert.equal(projectId, 'prj_default');
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 4,
createdAtMs: 0,
updatedAtMs: 1,
},
binding: {
projectId,
subject,
version: 7,
state: 'active',
role: 'operator',
mutationId: 'grant-7',
changedBy: { type: 'user', id: 'usr_owner' },
createdAtMs: 1,
},
};
},
async append() {
throw new Error('not used');
},
});
assert.deepEqual(
await policy.authorize({
principal: PRINCIPAL,
operationId: 'run.create',
permission: 'run.start',
projectId: 'prj_default',
signal: METADATA.signal,
}),
{
effect: 'allow',
reasons: ['role_grant'],
fence: { projectVersion: 4, bindingVersion: 7 },
},
);
assert.equal(
(
await policy.authorize({
principal: PRINCIPAL,
operationId: 'project.update',
permission: 'project.manage',
projectId: null,
signal: METADATA.signal,
})
).effect,
'deny',
);
});