mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
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 { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
LocalSqliteOperationAuthority,
|
||||
} = require('@qinglong/local-sqlite/operation-authority');
|
||||
const {
|
||||
LocalSqliteSecurityAuditRetentionRepository,
|
||||
} = require('@qinglong/local-sqlite/security-audit-retention');
|
||||
const {
|
||||
LocalSecurityAuditCompactionMutationConflictError,
|
||||
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
|
||||
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
|
||||
localSecurityAuditCompactionPayload,
|
||||
} = require('@qinglong/runtime-core/local-security-audit-retention');
|
||||
|
||||
const NOW = 4_000_000_000;
|
||||
const CUTOFF = NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS;
|
||||
|
||||
function audit(eventId, occurredAtMs, outcome, operationId = 'tool.invoke') {
|
||||
return {
|
||||
eventId,
|
||||
requestId: `request-${eventId}`,
|
||||
operationId,
|
||||
projectId: 'default',
|
||||
subject: { type: 'user', id: 'owner-user' },
|
||||
authenticationId: 'local_security_audit:test',
|
||||
outcome,
|
||||
reasons: ['test_reason'],
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
occurredAtMs,
|
||||
};
|
||||
}
|
||||
|
||||
function insertAudit(client, value) {
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
value.eventId,
|
||||
value.requestId,
|
||||
value.operationId,
|
||||
value.projectId,
|
||||
value.subject.type,
|
||||
value.subject.id,
|
||||
value.authenticationId,
|
||||
value.outcome,
|
||||
JSON.stringify(value.reasons),
|
||||
value.fence.projectVersion,
|
||||
value.fence.bindingVersion,
|
||||
value.occurredAtMs,
|
||||
);
|
||||
}
|
||||
|
||||
async function fixture(t, profile = 'edge') {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-security-audit-retention-'),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const databasePath = path.join(directory, 'qinglong3.sqlite');
|
||||
await migrateLocalSqlitePath({ databasePath, profile });
|
||||
const client = new DatabaseSync(databasePath);
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
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('b1000000-0000-4000-8000-000000000001', NOW - 1_000);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
t.after(() => authority.close());
|
||||
return {
|
||||
client,
|
||||
repository: new LocalSqliteSecurityAuditRetentionRepository(
|
||||
authority,
|
||||
() => {},
|
||||
profile === 'edge' ? 64 : 512,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function authorization(overrides = {}) {
|
||||
return {
|
||||
authorityProjectId: 'default',
|
||||
actor: { type: 'user', id: 'owner-user' },
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function command(mutationId, overrides = {}) {
|
||||
return {
|
||||
mutationId,
|
||||
requestId: `compact-${mutationId}`,
|
||||
retentionMs: MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
|
||||
eligibleBeforeMs: CUTOFF,
|
||||
limit: 64,
|
||||
authorization: authorization(),
|
||||
audit: {
|
||||
...audit(mutationId, NOW, 'allowed', 'security.audit.compact'),
|
||||
requestId: `compact-${mutationId}`,
|
||||
reasons: ['instance_authority_security_audit_compaction'],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function existingIds(client) {
|
||||
return client
|
||||
.prepare(
|
||||
`SELECT "event_id" AS "eventId"
|
||||
FROM "QingLong3SecurityAuditEvents"
|
||||
ORDER BY "event_id"`,
|
||||
)
|
||||
.all()
|
||||
.map((row) => row.eventId);
|
||||
}
|
||||
|
||||
test('deletes only unreferenced expired diagnostic audit and keeps an immutable receipt', async (t) => {
|
||||
const value = await fixture(t);
|
||||
const denied = audit(
|
||||
'b2000000-0000-4000-8000-000000000001',
|
||||
CUTOFF - 4,
|
||||
'denied',
|
||||
);
|
||||
const diagnostic = audit(
|
||||
'b2000000-0000-4000-8000-000000000002',
|
||||
CUTOFF - 3,
|
||||
'allowed',
|
||||
'security.audit.list',
|
||||
);
|
||||
const allowedMutation = audit(
|
||||
'b2000000-0000-4000-8000-000000000003',
|
||||
CUTOFF - 2,
|
||||
'allowed',
|
||||
'policy.project.create',
|
||||
);
|
||||
const referenced = audit(
|
||||
'b2000000-0000-4000-8000-000000000004',
|
||||
CUTOFF - 1,
|
||||
'denied',
|
||||
);
|
||||
const recent = audit(
|
||||
'b2000000-0000-4000-8000-000000000005',
|
||||
CUTOFF,
|
||||
'denied',
|
||||
);
|
||||
for (const record of [
|
||||
denied,
|
||||
diagnostic,
|
||||
allowedMutation,
|
||||
referenced,
|
||||
recent,
|
||||
]) {
|
||||
insertAudit(value.client, record);
|
||||
}
|
||||
value.client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LegacyAdoptions" (
|
||||
"mutation_id", "decision_id", "project_id", "profile",
|
||||
"plan_digest", "inventory_digest", "decision_digest",
|
||||
"receipt_digest", "authorization_file_digest",
|
||||
"publication_digest", "row_count", "adopted_task_count",
|
||||
"adopted_trigger_count", "skipped_count", "audit_event_id",
|
||||
"created_at_ms"
|
||||
) VALUES (?, ?, 'default', 'edge', ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
referenced.eventId,
|
||||
'b2000000-0000-7000-8000-000000000004',
|
||||
...Array(6).fill('a'.repeat(64)),
|
||||
referenced.eventId,
|
||||
referenced.occurredAtMs,
|
||||
);
|
||||
|
||||
const mutationId = 'b3000000-0000-4000-8000-000000000001';
|
||||
const result = await value.repository.compactAuthorized(command(mutationId));
|
||||
assert.equal(result.status, 'inserted');
|
||||
assert.equal(result.record.deletedCount, 2);
|
||||
assert.deepEqual(result.record.first, {
|
||||
occurredAtMs: denied.occurredAtMs,
|
||||
eventId: denied.eventId,
|
||||
});
|
||||
assert.deepEqual(result.record.last, {
|
||||
occurredAtMs: diagnostic.occurredAtMs,
|
||||
eventId: diagnostic.eventId,
|
||||
});
|
||||
assert.deepEqual(
|
||||
{
|
||||
recordsDigest: result.record.recordsDigest,
|
||||
payloadBytes: result.record.deletedPayloadBytes,
|
||||
},
|
||||
localSecurityAuditCompactionPayload([denied, diagnostic]),
|
||||
);
|
||||
assert.deepEqual(existingIds(value.client), [
|
||||
allowedMutation.eventId,
|
||||
referenced.eventId,
|
||||
recent.eventId,
|
||||
mutationId,
|
||||
]);
|
||||
assert.equal(
|
||||
value.client
|
||||
.prepare(
|
||||
`SELECT count(*) AS "count"
|
||||
FROM "QingLong3SecurityAuditCompactions"
|
||||
WHERE "mutation_id" = ? AND "audit_event_id" = ?`,
|
||||
)
|
||||
.get(mutationId, mutationId).count,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test('exactly replays a batch and rejects semantic drift', async (t) => {
|
||||
const value = await fixture(t);
|
||||
insertAudit(
|
||||
value.client,
|
||||
audit('b4000000-0000-4000-8000-000000000001', CUTOFF - 1, 'denied'),
|
||||
);
|
||||
const mutationId = 'b5000000-0000-4000-8000-000000000001';
|
||||
const input = command(mutationId);
|
||||
const inserted = await value.repository.compactAuthorized(input);
|
||||
const replay = await value.repository.compactAuthorized(input);
|
||||
assert.equal(inserted.status, 'inserted');
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.record, inserted.record);
|
||||
await assert.rejects(
|
||||
value.repository.compactAuthorized({
|
||||
...input,
|
||||
requestId: 'compact-drift',
|
||||
audit: { ...input.audit, requestId: 'compact-drift' },
|
||||
}),
|
||||
LocalSecurityAuditCompactionMutationConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('honors the hard batch cap and advances only through fresh mutations', async (t) => {
|
||||
const value = await fixture(t);
|
||||
const firstCandidate = audit(
|
||||
'b8000000-0000-4000-8000-000000000001',
|
||||
CUTOFF - 2,
|
||||
'denied',
|
||||
);
|
||||
const secondCandidate = audit(
|
||||
'b8000000-0000-4000-8000-000000000002',
|
||||
CUTOFF - 1,
|
||||
'denied',
|
||||
);
|
||||
insertAudit(value.client, firstCandidate);
|
||||
insertAudit(value.client, secondCandidate);
|
||||
|
||||
const first = await value.repository.compactAuthorized(
|
||||
command('b9000000-0000-4000-8000-000000000001', { limit: 1 }),
|
||||
);
|
||||
assert.equal(first.record.deletedCount, 1);
|
||||
assert.deepEqual(existingIds(value.client), [
|
||||
secondCandidate.eventId,
|
||||
first.record.mutationId,
|
||||
]);
|
||||
|
||||
const second = await value.repository.compactAuthorized(
|
||||
command('b9000000-0000-4000-8000-000000000002', { limit: 1 }),
|
||||
);
|
||||
assert.equal(second.record.deletedCount, 1);
|
||||
assert.deepEqual(existingIds(value.client), [
|
||||
first.record.mutationId,
|
||||
second.record.mutationId,
|
||||
]);
|
||||
|
||||
const empty = await value.repository.compactAuthorized(
|
||||
command('b9000000-0000-4000-8000-000000000003', { limit: 1 }),
|
||||
);
|
||||
assert.equal(empty.record.deletedCount, 0);
|
||||
assert.equal(empty.record.deletedPayloadBytes, 0);
|
||||
assert.equal(empty.record.first, null);
|
||||
assert.equal(empty.record.last, null);
|
||||
});
|
||||
|
||||
test('rejects a foreign authority without deleting or writing a receipt', async (t) => {
|
||||
const value = await fixture(t);
|
||||
const candidate = audit(
|
||||
'b6000000-0000-4000-8000-000000000001',
|
||||
CUTOFF - 1,
|
||||
'denied',
|
||||
);
|
||||
insertAudit(value.client, candidate);
|
||||
const input = command('b7000000-0000-4000-8000-000000000001');
|
||||
await assert.rejects(
|
||||
value.repository.compactAuthorized({
|
||||
...input,
|
||||
authorization: authorization({ authorityProjectId: 'foreign' }),
|
||||
audit: { ...input.audit, projectId: 'foreign' },
|
||||
}),
|
||||
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
|
||||
);
|
||||
assert.deepEqual(existingIds(value.client), [candidate.eventId]);
|
||||
assert.equal(
|
||||
value.client
|
||||
.prepare(
|
||||
`SELECT count(*) AS "count"
|
||||
FROM "QingLong3SecurityAuditCompactions"`,
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user