mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
281 lines
8.6 KiB
JavaScript
281 lines
8.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createLocalPluginPackageManagementService,
|
|
} = require('@qinglong/local-admin/package-management');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('@qinglong/local-sqlite/operation-authority');
|
|
const {
|
|
LocalSqlitePluginPackageInstallRepository,
|
|
} = require('@qinglong/local-sqlite/plugin-package-install');
|
|
const {
|
|
migrateLocalSqliteDatabase,
|
|
} = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
PLUGIN_PACKAGE_API_VERSION,
|
|
PLUGIN_PACKAGE_KIND,
|
|
planPluginPackageInstall,
|
|
} = require('@qinglong/runtime-core/plugin-package');
|
|
|
|
const OWNER = Object.freeze({ type: 'user', id: 'usr_owner' });
|
|
const CONSUMER = Object.freeze({
|
|
subject: { type: 'system', id: 'local_package_dispatcher' },
|
|
authenticationId: 'local-package-dispatcher-auth',
|
|
});
|
|
|
|
function actionInput() {
|
|
const manifest = {
|
|
apiVersion: PLUGIN_PACKAGE_API_VERSION,
|
|
kind: PLUGIN_PACKAGE_KIND,
|
|
metadata: {
|
|
name: 'example-monitor',
|
|
displayName: 'Example Monitor',
|
|
version: '1.2.0',
|
|
description: 'One bounded package',
|
|
license: 'Apache-2.0',
|
|
},
|
|
spec: {
|
|
compatibility: {
|
|
qinglong: '>=3.0.0-0 <4.0.0',
|
|
architectures: ['arm64'],
|
|
deploymentProfiles: ['edge'],
|
|
},
|
|
runtimes: [],
|
|
resources: {
|
|
memory: { recommended: '16Mi' },
|
|
disk: { install: '4Mi', working: '16Mi' },
|
|
},
|
|
permissions: {
|
|
network: { allowedHosts: [] },
|
|
secrets: [],
|
|
tools: [],
|
|
},
|
|
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
|
},
|
|
};
|
|
const environment = {
|
|
qinglongVersion: '3.0.0-alpha.0',
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
runtimes: [],
|
|
availableMemoryBytes: 128 * 1024 * 1024,
|
|
availableDiskBytes: 256 * 1024 * 1024,
|
|
};
|
|
return {
|
|
lockId: 'proposal-monitor-v1',
|
|
projectId: 'default',
|
|
manifest,
|
|
plan: planPluginPackageInstall(manifest, environment),
|
|
environment,
|
|
source: {
|
|
kind: 'offline',
|
|
locator: `offline:sha256:${'a'.repeat(64)}`,
|
|
artifactDigest: 'a'.repeat(64),
|
|
artifactBytes: 2_048,
|
|
contentDigest: 'b'.repeat(64),
|
|
},
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
targetGeneration: 1,
|
|
};
|
|
}
|
|
|
|
function principal(now, assurance = 'single_factor') {
|
|
return {
|
|
subject: OWNER,
|
|
authenticationId: `auth-owner-${assurance}`,
|
|
authenticatedAtMs: now - 100,
|
|
expiresAtMs: now + 100_000,
|
|
assurance,
|
|
};
|
|
}
|
|
|
|
test('runs authenticated local proposal, self-confirmation, consumption and admission end to end', async (t) => {
|
|
const client = new DatabaseSync(':memory:');
|
|
t.after(() => client.close());
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
await migrateLocalSqliteDatabase(client);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ProjectRoleBindings"
|
|
("project_id","subject_type","subject_id","version","state","role",
|
|
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
|
|
VALUES ('default','user','usr_owner',1,'active','owner',
|
|
'grant-owner-1','user','usr_owner',0)`,
|
|
)
|
|
.run();
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
let now = Date.now();
|
|
const requestedAtMs = now;
|
|
let generatedId = 0;
|
|
const service = createLocalPluginPackageManagementService({
|
|
authority,
|
|
profile: 'edge',
|
|
consumer: CONSUMER,
|
|
dispatcher: {
|
|
owner: 'local_package_dispatcher_1',
|
|
clock: () => now,
|
|
createId: () => `dispatcher-id-${++generatedId}`,
|
|
},
|
|
now: () => now,
|
|
});
|
|
const proposalRequest = {
|
|
actionRef: 'proposal:monitor-v1',
|
|
approvalRequestId: 'approval-monitor-v1',
|
|
proposalAuditEventId: '10000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '10000000-0000-4000-8000-000000000002',
|
|
requestedAtMs,
|
|
actionInput: actionInput(),
|
|
principal: principal(now),
|
|
};
|
|
const proposed = await service.propose(proposalRequest);
|
|
assert.equal(proposed.proposalStatus, 'created');
|
|
assert.equal(proposed.approvalStatus, 'created');
|
|
assert.equal(proposed.approvalRequest.decisionMode, 'human_confirmation');
|
|
const replayed = await service.propose(proposalRequest);
|
|
assert.equal(replayed.proposalStatus, 'existing');
|
|
assert.equal(replayed.approvalStatus, 'existing');
|
|
|
|
now = Date.now();
|
|
const decided = await service.decide({
|
|
approvalRequestId: 'approval-monitor-v1',
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-monitor-v1',
|
|
auditEventId: '10000000-0000-4000-8000-000000000003',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
decidedAtMs: now,
|
|
principal: principal(now, 'local_console'),
|
|
});
|
|
assert.equal(decided.request.state, 'approved');
|
|
|
|
now = Date.now();
|
|
const consumed = await service.consume({
|
|
approvalRequestId: 'approval-monitor-v1',
|
|
expectedVersion: 2,
|
|
consumptionId: 'consume-monitor-v1',
|
|
dispatchId: 'dispatch-monitor-v1',
|
|
auditEventId: '10000000-0000-4000-8000-000000000004',
|
|
consumedAtMs: now,
|
|
});
|
|
assert.equal(consumed.request.state, 'consumed');
|
|
|
|
now = Date.now();
|
|
const dispatched = await service.dispatch();
|
|
assert.equal(dispatched.scanned, 1);
|
|
assert.equal(dispatched.started, 1);
|
|
assert.equal(dispatched.succeeded, 1);
|
|
const installation = await new LocalSqlitePluginPackageInstallRepository(
|
|
authority,
|
|
).find('default', 'example-monitor');
|
|
assert.equal(installation.state, 'queued');
|
|
assert.equal(installation.targetGeneration, 1);
|
|
|
|
const inspected = await service.inspect(
|
|
'proposal:monitor-v1',
|
|
'approval-monitor-v1',
|
|
);
|
|
assert.equal(inspected.proposal.actionRef, 'proposal:monitor-v1');
|
|
assert.equal(inspected.approvalRequest.state, 'consumed');
|
|
assert.equal(
|
|
client
|
|
.prepare(
|
|
`SELECT count(*) AS count
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" IN (
|
|
'plugin_package.propose',
|
|
'approval.request',
|
|
'approval.decide',
|
|
'approval.consume',
|
|
'plugin_package.admit'
|
|
)`,
|
|
)
|
|
.get().count,
|
|
5,
|
|
);
|
|
assert.equal(
|
|
require('..').createLocalPluginPackageManagementService,
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test('rejects weak decisions and expired management commands before mutation', async (t) => {
|
|
const client = new DatabaseSync(':memory:');
|
|
t.after(() => client.close());
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
await migrateLocalSqliteDatabase(client);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ProjectRoleBindings"
|
|
("project_id","subject_type","subject_id","version","state","role",
|
|
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
|
|
VALUES ('default','user','usr_owner',1,'active','owner',
|
|
'grant-owner-1','user','usr_owner',0)`,
|
|
)
|
|
.run();
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
let now = Date.now();
|
|
const requestedAtMs = now;
|
|
const service = createLocalPluginPackageManagementService({
|
|
authority,
|
|
profile: 'edge',
|
|
consumer: CONSUMER,
|
|
dispatcher: {
|
|
owner: 'local_package_dispatcher_2',
|
|
clock: () => now,
|
|
createId: () => 'dispatcher-id-fixed',
|
|
},
|
|
approvalLifetimeMs: 1_000,
|
|
now: () => now,
|
|
});
|
|
await service.propose({
|
|
actionRef: 'proposal:monitor-weak-v1',
|
|
approvalRequestId: 'approval-monitor-weak-v1',
|
|
proposalAuditEventId: '20000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '20000000-0000-4000-8000-000000000002',
|
|
requestedAtMs,
|
|
actionInput: actionInput(),
|
|
principal: principal(now),
|
|
});
|
|
now = requestedAtMs + 100;
|
|
await assert.rejects(
|
|
service.decide({
|
|
approvalRequestId: 'approval-monitor-weak-v1',
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-monitor-weak-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000003',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
decidedAtMs: now,
|
|
principal: principal(now),
|
|
}),
|
|
{ code: 'APPROVAL_HUMAN_DECISION_REQUIRED' },
|
|
);
|
|
now = requestedAtMs + 1_000;
|
|
await assert.rejects(
|
|
service.propose({
|
|
actionRef: 'proposal:expired-v1',
|
|
approvalRequestId: 'approval-expired-v1',
|
|
proposalAuditEventId: '20000000-0000-4000-8000-000000000004',
|
|
approvalAuditEventId: '20000000-0000-4000-8000-000000000005',
|
|
requestedAtMs,
|
|
actionInput: actionInput(),
|
|
principal: principal(now),
|
|
}),
|
|
{ code: 'PLUGIN_PACKAGE_MANAGEMENT_REQUEST_INVALID' },
|
|
);
|
|
assert.equal(
|
|
client
|
|
.prepare(
|
|
`SELECT count(*) AS count
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" = 'approval.decide'`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
});
|