mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
193 lines
5.9 KiB
JavaScript
193 lines
5.9 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
LocalSqliteSecurityAuditQueryRepository,
|
|
} = require('@qinglong/local-sqlite/security-audit-query');
|
|
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('@qinglong/local-sqlite/operation-authority');
|
|
const {
|
|
LocalSecurityAuditQueryAuthorizationFenceConflictError,
|
|
} = require('@qinglong/runtime-core/local-security-audit-query');
|
|
|
|
async function fixture(t) {
|
|
const directory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-security-audit-query-'),
|
|
);
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
const databasePath = path.join(directory, 'qinglong3.sqlite');
|
|
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
|
const client = new DatabaseSync(databasePath);
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
const now = 10_000;
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3Projects" (
|
|
"id", "name", "slug", "status", "version",
|
|
"created_at_ms", "updated_at_ms"
|
|
) VALUES ('project-alpha', 'Project Alpha', 'project-alpha',
|
|
'active', 1, ?, ?)`,
|
|
)
|
|
.run(now - 200, now - 200);
|
|
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', 'owner-user', 1, 'active', 'owner', ?,
|
|
'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run('96000000-0000-4000-8000-000000000001', now - 100);
|
|
const insertAudit = client.prepare(
|
|
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
|
"event_id", "request_id", "operation_id", "project_id",
|
|
"subject_type", "subject_id", "authentication_id", "outcome",
|
|
"reasons_json", "fence_project_version", "fence_binding_version",
|
|
"occurred_at_ms"
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
);
|
|
for (const [eventId, occurredAtMs, outcome, subjectId] of [
|
|
['97000000-0000-4000-8000-000000000003', 9_003, 'denied', 'planner'],
|
|
['97000000-0000-4000-8000-000000000002', 9_002, 'denied', 'planner'],
|
|
['97000000-0000-4000-8000-000000000001', 9_001, 'denied', 'planner'],
|
|
['97000000-0000-4000-8000-000000000000', 9_000, 'allowed', 'planner'],
|
|
]) {
|
|
insertAudit.run(
|
|
eventId,
|
|
`request-${occurredAtMs}`,
|
|
'tool.invoke',
|
|
'project-alpha',
|
|
'agent',
|
|
subjectId,
|
|
`private-auth-${occurredAtMs}`,
|
|
outcome,
|
|
'["policy_result"]',
|
|
1,
|
|
1,
|
|
occurredAtMs,
|
|
);
|
|
}
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
t.after(() => authority.close());
|
|
return {
|
|
client,
|
|
repository: new LocalSqliteSecurityAuditQueryRepository(
|
|
authority,
|
|
() => {},
|
|
),
|
|
};
|
|
}
|
|
|
|
function authorization(overrides = {}) {
|
|
return {
|
|
authorityProjectId: 'default',
|
|
actor: { type: 'user', id: 'owner-user' },
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function queryAudit(eventId) {
|
|
return {
|
|
eventId,
|
|
requestId: `audit-query-${eventId.at(-1)}`,
|
|
operationId: 'security.audit.list',
|
|
projectId: 'default',
|
|
subject: { type: 'user', id: 'owner-user' },
|
|
authenticationId: 'local_security_audit:test',
|
|
outcome: 'allowed',
|
|
reasons: ['instance_authority_security_audit_query'],
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
occurredAtMs: 10_000 + Number(eventId.at(-1)),
|
|
};
|
|
}
|
|
|
|
test('filters and keyset-pages a pre-audit snapshot with an exact has-more cursor', async (t) => {
|
|
const value = await fixture(t);
|
|
const filter = {
|
|
projectId: 'project-alpha',
|
|
subject: { type: 'agent', id: 'planner' },
|
|
outcome: 'denied',
|
|
};
|
|
const first = await value.repository.listAuthorized({
|
|
query: { limit: 2, filter },
|
|
authorization: authorization(),
|
|
audit: queryAudit('98000000-0000-4000-8000-000000000001'),
|
|
});
|
|
assert.deepEqual(
|
|
first.records.map((record) => record.eventId),
|
|
[
|
|
'97000000-0000-4000-8000-000000000003',
|
|
'97000000-0000-4000-8000-000000000002',
|
|
],
|
|
);
|
|
assert.deepEqual(first.nextCursor, {
|
|
occurredAtMs: 9_002,
|
|
eventId: '97000000-0000-4000-8000-000000000002',
|
|
});
|
|
assert.equal(
|
|
first.records.some(
|
|
(record) => record.eventId === '98000000-0000-4000-8000-000000000001',
|
|
),
|
|
false,
|
|
);
|
|
|
|
const second = await value.repository.listAuthorized({
|
|
query: { limit: 2, before: first.nextCursor, filter },
|
|
authorization: authorization(),
|
|
audit: queryAudit('98000000-0000-4000-8000-000000000002'),
|
|
});
|
|
assert.deepEqual(
|
|
second.records.map((record) => record.eventId),
|
|
['97000000-0000-4000-8000-000000000001'],
|
|
);
|
|
assert.equal(second.nextCursor, null);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT count(*) AS "count"
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" = 'security.audit.list'
|
|
AND "outcome" = 'allowed'`,
|
|
)
|
|
.get().count,
|
|
2,
|
|
);
|
|
});
|
|
|
|
test('rejects a foreign instance authority before reading or auditing rows', async (t) => {
|
|
const value = await fixture(t);
|
|
await assert.rejects(
|
|
value.repository.listAuthorized({
|
|
query: { limit: 1, filter: {} },
|
|
authorization: authorization({
|
|
authorityProjectId: 'project-alpha',
|
|
}),
|
|
audit: {
|
|
...queryAudit('99000000-0000-4000-8000-000000000001'),
|
|
projectId: 'project-alpha',
|
|
},
|
|
}),
|
|
LocalSecurityAuditQueryAuthorizationFenceConflictError,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT count(*) AS "count"
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "event_id" = ?`,
|
|
)
|
|
.get('99000000-0000-4000-8000-000000000001').count,
|
|
0,
|
|
);
|
|
});
|