mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
createTaskDefinitionRecord,
|
||||
} = require('@qinglong/runtime-core/task-definition');
|
||||
const { createTriggerRecord } = require('@qinglong/runtime-core/trigger');
|
||||
const {
|
||||
ClusterAutomationManagementAuthorizationError,
|
||||
ClusterAutomationManagementConflictError,
|
||||
createClusterAutomationManagementService,
|
||||
} = require('@qinglong/cluster-admin/automation-management');
|
||||
const {
|
||||
TaskDefinitionAdministrationMutationConflictError,
|
||||
} = require('@qinglong/runtime-core/task-definition-administration');
|
||||
|
||||
const principal = Object.freeze({
|
||||
subject: Object.freeze({ type: 'user', id: 'operator-a' }),
|
||||
authenticationId: 'session-operator-a',
|
||||
authenticatedAtMs: 900,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'multi_factor',
|
||||
});
|
||||
|
||||
function taskCommand(expectedRevision = null) {
|
||||
return {
|
||||
projectId: 'project-a',
|
||||
taskId: 'task-a',
|
||||
expectedRevision,
|
||||
mutationId:
|
||||
expectedRevision === null
|
||||
? '123e4567-e89b-42d3-a456-426614174000'
|
||||
: '123e4567-e89b-42d3-a456-426614174001',
|
||||
name: 'Sensitive command name',
|
||||
kind: 'script',
|
||||
spec: {
|
||||
schema: 'qinglong/script@v1',
|
||||
config: { source: 'sensitive source text' },
|
||||
},
|
||||
labels: { environment: 'test' },
|
||||
enabled: true,
|
||||
occurredAtMs: 1_000,
|
||||
};
|
||||
}
|
||||
|
||||
function triggerCommand(expectedRevision = null) {
|
||||
return {
|
||||
projectId: 'project-a',
|
||||
triggerId: 'trigger-a',
|
||||
expectedRevision,
|
||||
mutationId:
|
||||
expectedRevision === null
|
||||
? '123e4567-e89b-42d3-a456-426614174002'
|
||||
: '123e4567-e89b-42d3-a456-426614174003',
|
||||
taskId: 'task-a',
|
||||
taskRevision: 1,
|
||||
taskContentDigest: 'a'.repeat(64),
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '*/5 * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'fire_once',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
occurredAtMs: 1_000,
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(effect = 'allow') {
|
||||
const calls = [];
|
||||
const service = createClusterAutomationManagementService({
|
||||
policy: {
|
||||
async authorize(candidate, projectId, permission) {
|
||||
calls.push(['authorize', candidate, projectId, permission]);
|
||||
return {
|
||||
effect,
|
||||
reasons: [effect === 'allow' ? 'role_grant' : 'permission_missing'],
|
||||
fence: { projectVersion: 3, bindingVersion: 5 },
|
||||
};
|
||||
},
|
||||
},
|
||||
taskDefinitions: {
|
||||
async appendAuthorizedTaskDefinitionRevision(mutation) {
|
||||
calls.push(['task', mutation]);
|
||||
return {
|
||||
status:
|
||||
mutation.command.expectedRevision === null ? 'created' : 'updated',
|
||||
definition: createTaskDefinitionRecord(mutation.command, 900),
|
||||
};
|
||||
},
|
||||
async findAuthorizedCurrentTaskDefinition(read) {
|
||||
calls.push(['task-inspect', read]);
|
||||
return createTaskDefinitionRecord(taskCommand(), 900);
|
||||
},
|
||||
async listAuthorizedTaskDefinitions(read) {
|
||||
calls.push(['task-list', read]);
|
||||
return {
|
||||
definitions: [createTaskDefinitionRecord(taskCommand(), 900)],
|
||||
truncated: true,
|
||||
next: { taskId: 'task-a' },
|
||||
};
|
||||
},
|
||||
},
|
||||
triggers: {
|
||||
async appendAuthorizedTriggerRevision(mutation) {
|
||||
calls.push(['trigger', mutation]);
|
||||
return {
|
||||
status:
|
||||
mutation.command.expectedRevision === null ? 'created' : 'updated',
|
||||
trigger: createTriggerRecord(mutation.command, 900),
|
||||
};
|
||||
},
|
||||
async findAuthorizedCurrentTrigger(read) {
|
||||
calls.push(['trigger-inspect', read]);
|
||||
return createTriggerRecord(triggerCommand(), 900);
|
||||
},
|
||||
async listAuthorizedTriggers(read) {
|
||||
calls.push(['trigger-list', read]);
|
||||
return {
|
||||
triggers: [createTriggerRecord(triggerCommand(), 900)],
|
||||
truncated: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
now: () => 1_100,
|
||||
});
|
||||
return { service, calls };
|
||||
}
|
||||
|
||||
test('authorizes Task and Trigger create/update and binds allowed audit to the repository fence', async () => {
|
||||
const { service, calls } = fixture();
|
||||
await service.publishTask({
|
||||
requestId: 'request-task-create',
|
||||
command: taskCommand(),
|
||||
principal,
|
||||
});
|
||||
await service.publishTask({
|
||||
requestId: 'request-task-update',
|
||||
command: taskCommand(1),
|
||||
principal,
|
||||
});
|
||||
await service.publishTrigger({
|
||||
requestId: 'request-trigger-create',
|
||||
command: triggerCommand(),
|
||||
principal,
|
||||
});
|
||||
await service.publishTrigger({
|
||||
requestId: 'request-trigger-update',
|
||||
command: triggerCommand(1),
|
||||
principal,
|
||||
});
|
||||
assert.deepEqual(
|
||||
calls.filter(([kind]) => kind === 'authorize').map((call) => call[3]),
|
||||
['task.create', 'task.update', 'trigger.create', 'trigger.update'],
|
||||
);
|
||||
for (const [kind, mutation] of calls.filter(([kind]) =>
|
||||
['task', 'trigger'].includes(kind),
|
||||
)) {
|
||||
assert.deepEqual(mutation.actor, principal.subject);
|
||||
assert.deepEqual(mutation.fence, {
|
||||
projectVersion: 3,
|
||||
bindingVersion: 5,
|
||||
});
|
||||
assert.equal(mutation.audit.outcome, 'allowed');
|
||||
assert.equal(mutation.audit.authenticationId, principal.authenticationId);
|
||||
assert.equal(mutation.audit.eventId, mutation.command.mutationId);
|
||||
assert.equal(
|
||||
mutation.audit.operationId,
|
||||
`${kind}.${mutation.command.expectedRevision === null ? 'create' : 'update'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed before mutation on weak identity or denied policy', async () => {
|
||||
const denied = fixture('deny');
|
||||
await assert.rejects(
|
||||
denied.service.publishTask({
|
||||
requestId: 'request-denied',
|
||||
command: taskCommand(),
|
||||
principal,
|
||||
}),
|
||||
ClusterAutomationManagementAuthorizationError,
|
||||
);
|
||||
assert.equal(denied.calls.some(([kind]) => kind === 'task'), false);
|
||||
|
||||
const allowed = fixture();
|
||||
await assert.rejects(
|
||||
allowed.service.publishTrigger({
|
||||
requestId: 'request-weak',
|
||||
command: triggerCommand(),
|
||||
principal: { ...principal, assurance: 'single_factor' },
|
||||
}),
|
||||
ClusterAutomationManagementAuthorizationError,
|
||||
);
|
||||
assert.equal(allowed.calls.length, 0);
|
||||
});
|
||||
|
||||
test('authorizes bounded Task and Trigger reads and binds each durable audit fence', async () => {
|
||||
const { service, calls } = fixture();
|
||||
const task = await service.inspectTask({
|
||||
requestId: 'request-task-inspect',
|
||||
auditEventId: '123e4567-e89b-42d3-a456-426614174010',
|
||||
projectId: 'project-a',
|
||||
taskId: 'task-a',
|
||||
principal,
|
||||
});
|
||||
const tasks = await service.listTasks({
|
||||
requestId: 'request-task-list',
|
||||
auditEventId: '123e4567-e89b-42d3-a456-426614174011',
|
||||
projectId: 'project-a',
|
||||
limit: 1,
|
||||
principal,
|
||||
});
|
||||
const trigger = await service.inspectTrigger({
|
||||
requestId: 'request-trigger-inspect',
|
||||
auditEventId: '123e4567-e89b-42d3-a456-426614174012',
|
||||
projectId: 'project-a',
|
||||
triggerId: 'trigger-a',
|
||||
principal,
|
||||
});
|
||||
const triggers = await service.listTriggers({
|
||||
requestId: 'request-trigger-list',
|
||||
auditEventId: '123e4567-e89b-42d3-a456-426614174013',
|
||||
projectId: 'project-a',
|
||||
limit: 1,
|
||||
after: { triggerId: 'trigger-0' },
|
||||
principal,
|
||||
});
|
||||
assert.equal(task.taskId, 'task-a');
|
||||
assert.equal(tasks.truncated, true);
|
||||
assert.equal(trigger.triggerId, 'trigger-a');
|
||||
assert.equal(triggers.truncated, false);
|
||||
assert.deepEqual(
|
||||
calls.filter(([kind]) => kind === 'authorize').map((call) => call[3]),
|
||||
['task.read', 'task.read', 'trigger.read', 'trigger.read'],
|
||||
);
|
||||
for (const [kind, read] of calls.filter(([kind]) => kind.includes('-'))) {
|
||||
assert.equal(read.audit.operationId, kind.startsWith('task') ? 'task.read' : 'trigger.read');
|
||||
assert.equal(read.audit.outcome, 'allowed');
|
||||
assert.deepEqual(read.actor, principal.subject);
|
||||
assert.deepEqual(read.fence, { projectVersion: 3, bindingVersion: 5 });
|
||||
}
|
||||
});
|
||||
|
||||
test('maps durable mutation conflicts without leaking repository details', async () => {
|
||||
const conflicting = createClusterAutomationManagementService({
|
||||
policy: {
|
||||
async authorize() {
|
||||
return {
|
||||
effect: 'allow',
|
||||
reasons: ['role_grant'],
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
};
|
||||
},
|
||||
},
|
||||
taskDefinitions: {
|
||||
async appendAuthorizedTaskDefinitionRevision() {
|
||||
throw new TaskDefinitionAdministrationMutationConflictError();
|
||||
},
|
||||
async findAuthorizedCurrentTaskDefinition() { return null; },
|
||||
async listAuthorizedTaskDefinitions() {
|
||||
return { definitions: [], truncated: false };
|
||||
},
|
||||
},
|
||||
triggers: {
|
||||
async appendAuthorizedTriggerRevision() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
async findAuthorizedCurrentTrigger() { return null; },
|
||||
async listAuthorizedTriggers() {
|
||||
return { triggers: [], truncated: false };
|
||||
},
|
||||
},
|
||||
now: () => 1_100,
|
||||
});
|
||||
await assert.rejects(
|
||||
conflicting.publishTask({
|
||||
requestId: 'request-conflict',
|
||||
command: taskCommand(),
|
||||
principal,
|
||||
}),
|
||||
ClusterAutomationManagementConflictError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user