Files
qinglong/packages/ql3-local-admin/test/pluginPackageManagement.test.cjs
T

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,
);
});