mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 20:15:19 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user