mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
283 lines
7.4 KiB
JavaScript
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',
|
|
);
|
|
});
|