mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
ApprovalMutationConflictError,
|
||||
ApprovalPolicyFenceConflictError,
|
||||
ApprovalUnavailableError,
|
||||
createApprovalRequest,
|
||||
} = require('@qinglong/runtime-core/approved-action');
|
||||
const {
|
||||
LocalSqliteApprovalRequestRepository,
|
||||
} = require('@qinglong/local-sqlite/approved-action');
|
||||
const {
|
||||
migrateLocalSqliteDatabase,
|
||||
} = require('@qinglong/local-sqlite/migration');
|
||||
|
||||
const DIGEST_A = 'a'.repeat(64);
|
||||
const DIGEST_B = 'b'.repeat(64);
|
||||
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
|
||||
const SYSTEM = Object.freeze({ type: 'system', id: 'approved-dispatcher' });
|
||||
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
|
||||
|
||||
function action(overrides = {}) {
|
||||
return {
|
||||
permission: 'package.manage',
|
||||
actionType: 'plugin_package.install',
|
||||
actionRef: 'proposal:pkg-demo-v1',
|
||||
actionDigest: DIGEST_A,
|
||||
previewDigest: DIGEST_B,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function request(id = 'approval-1') {
|
||||
return createApprovalRequest({
|
||||
id,
|
||||
projectId: 'default',
|
||||
action: action(),
|
||||
risk: 'high',
|
||||
decisionMode: 'human_confirmation',
|
||||
requestedBy: REQUESTER,
|
||||
requestedAtMs: 1_000,
|
||||
expiresAtMs: 61_000,
|
||||
requestFence: FENCE,
|
||||
});
|
||||
}
|
||||
|
||||
function audit(eventId, operationId, subject, authenticationId, outcome, atMs) {
|
||||
return {
|
||||
eventId,
|
||||
requestId: 'request-http-1',
|
||||
operationId,
|
||||
projectId: 'default',
|
||||
subject,
|
||||
authenticationId,
|
||||
outcome,
|
||||
reasons: [outcome === 'approval_required' ? 'package_review' : 'role_grant'],
|
||||
fence: FENCE,
|
||||
occurredAtMs: atMs,
|
||||
};
|
||||
}
|
||||
|
||||
function createCommand(overrides = {}) {
|
||||
return {
|
||||
request: request(),
|
||||
audit: audit(
|
||||
'10000000-0000-4000-8000-000000000001',
|
||||
'approval.request',
|
||||
REQUESTER,
|
||||
'auth-requester-1',
|
||||
'approval_required',
|
||||
1_000,
|
||||
),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function decideCommand(overrides = {}) {
|
||||
return {
|
||||
requestId: 'approval-1',
|
||||
expectedVersion: 1,
|
||||
decisionId: 'decision-1',
|
||||
decision: 'approved',
|
||||
reasonCode: 'reviewed',
|
||||
principal: {
|
||||
subject: REQUESTER,
|
||||
authenticationId: 'auth-step-up-1',
|
||||
authenticatedAtMs: 1_500,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'local_console',
|
||||
},
|
||||
decidedAtMs: 2_000,
|
||||
authorizationFence: FENCE,
|
||||
audit: audit(
|
||||
'10000000-0000-4000-8000-000000000002',
|
||||
'approval.decide',
|
||||
REQUESTER,
|
||||
'auth-step-up-1',
|
||||
'allowed',
|
||||
2_000,
|
||||
),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function consumeCommand(overrides = {}) {
|
||||
return {
|
||||
requestId: 'approval-1',
|
||||
expectedVersion: 2,
|
||||
consumptionId: 'consume-1',
|
||||
dispatchId: 'dispatch-1',
|
||||
action: action(),
|
||||
requestedBy: REQUESTER,
|
||||
consumedBy: SYSTEM,
|
||||
consumedAtMs: 3_000,
|
||||
authorizationFence: FENCE,
|
||||
audit: audit(
|
||||
'10000000-0000-4000-8000-000000000003',
|
||||
'approval.consume',
|
||||
SYSTEM,
|
||||
'auth-dispatcher-1',
|
||||
'allowed',
|
||||
3_000,
|
||||
),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function fixture(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();
|
||||
return {
|
||||
client,
|
||||
repository: new LocalSqliteApprovalRequestRepository(client),
|
||||
};
|
||||
}
|
||||
|
||||
test('persists request, strong decision, dispatch and audit with exact replay', async (t) => {
|
||||
const { client, repository } = await fixture(t);
|
||||
assert.equal((await repository.create(createCommand())).status, 'created');
|
||||
assert.equal((await repository.create(createCommand())).status, 'existing');
|
||||
|
||||
const decided = await repository.decide(decideCommand());
|
||||
assert.equal(decided.status, 'decided');
|
||||
assert.equal(decided.request.state, 'approved');
|
||||
assert.equal((await repository.decide(decideCommand())).status, 'existing');
|
||||
|
||||
const consumed = await repository.consume(consumeCommand());
|
||||
assert.equal(consumed.status, 'consumed');
|
||||
assert.equal(consumed.request.state, 'consumed');
|
||||
assert.equal(consumed.dispatch.approvedBy.id, 'usr_owner');
|
||||
assert.equal(
|
||||
(await repository.consume(consumeCommand())).status,
|
||||
'existing',
|
||||
);
|
||||
assert.deepEqual(
|
||||
await repository.findDispatchById('dispatch-1'),
|
||||
consumed.dispatch,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "operation_id" LIKE 'approval.%'`,
|
||||
)
|
||||
.get().count,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects replay drift and rolls request plus audit back together', async (t) => {
|
||||
const { client, repository } = await fixture(t);
|
||||
await repository.create(createCommand());
|
||||
await assert.rejects(
|
||||
repository.create(
|
||||
createCommand({
|
||||
audit: {
|
||||
...createCommand().audit,
|
||||
reasons: ['changed'],
|
||||
},
|
||||
}),
|
||||
),
|
||||
ApprovalMutationConflictError,
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.decide(
|
||||
decideCommand({
|
||||
audit: {
|
||||
...decideCommand().audit,
|
||||
operationId: 'approval.consume',
|
||||
},
|
||||
}),
|
||||
),
|
||||
ApprovalMutationConflictError,
|
||||
);
|
||||
assert.equal((await repository.findById('approval-1')).state, 'pending');
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "operation_id" = 'approval.decide'`,
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('fences a role change before decision without partial audit', async (t) => {
|
||||
const { client, repository } = await fixture(t);
|
||||
await repository.create(createCommand());
|
||||
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',2,'active','owner',
|
||||
'grant-owner-2','user','usr_owner',1500)`,
|
||||
)
|
||||
.run();
|
||||
await assert.rejects(
|
||||
repository.decide(decideCommand()),
|
||||
ApprovalPolicyFenceConflictError,
|
||||
);
|
||||
assert.equal((await repository.findById('approval-1')).state, 'pending');
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = '10000000-0000-4000-8000-000000000002'`,
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed when stored canonical request or dispatch JSON drifts', async (t) => {
|
||||
const { client, repository } = await fixture(t);
|
||||
await repository.create(createCommand());
|
||||
client
|
||||
.prepare(
|
||||
`UPDATE "QingLong3ApprovalRequests"
|
||||
SET "request_json" = json_set("request_json", '$.risk', 'low')
|
||||
WHERE "request_id" = 'approval-1'`,
|
||||
)
|
||||
.run();
|
||||
await assert.rejects(
|
||||
repository.findById('approval-1'),
|
||||
ApprovalUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('runs an optional authentication guard inside every mutation transaction', 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',
|
||||
'10000000-0000-4000-8000-000000000099','user','usr_owner',1)`
|
||||
)
|
||||
.run();
|
||||
let admitted = false;
|
||||
let guardCalls = 0;
|
||||
const repository = new LocalSqliteApprovalRequestRepository(client, () => {
|
||||
guardCalls += 1;
|
||||
if (!admitted) throw new Error('credential fence rejected');
|
||||
});
|
||||
await assert.rejects(
|
||||
repository.create(createCommand()),
|
||||
ApprovalUnavailableError,
|
||||
);
|
||||
assert.equal(guardCalls, 1);
|
||||
assert.equal(await repository.findById('approval-1'), null);
|
||||
admitted = true;
|
||||
await repository.create(createCommand());
|
||||
await repository.decide(decideCommand());
|
||||
await repository.consume(consumeCommand());
|
||||
assert.equal(guardCalls, 4);
|
||||
});
|
||||
|
||||
test('exports the authority only through the approved-action subpath', () => {
|
||||
const root = require('@qinglong/local-sqlite');
|
||||
const subpath = require('@qinglong/local-sqlite/approved-action');
|
||||
assert.equal(root.LocalSqliteApprovalRequestRepository, undefined);
|
||||
assert.equal(
|
||||
subpath.LocalSqliteApprovalRequestRepository,
|
||||
LocalSqliteApprovalRequestRepository,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user