Files
qinglong/packages/ql3-local-admin/test/securityAuditRetention.test.cjs

206 lines
5.6 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalSecurityAuditRetentionAuthorizationError,
LocalSecurityAuditRetentionConfigurationError,
createLocalSecurityAuditRetentionService,
} = require('@qinglong/local-admin/security-audit-retention');
const {
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
} = require('@qinglong/runtime-core/local-security-audit-retention');
const NOW = 4_000_000_000;
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-user' }),
authenticationId: 'local_security_audit:test',
authenticatedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
assurance: 'local_console',
});
function projectPolicy(role = 'owner') {
return {
async resolve(projectId, subject) {
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 4,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: {
projectId,
subject,
version: 7,
state: 'active',
role,
mutationId: 'owner-binding',
changedBy: subject,
createdAtMs: 0,
},
};
},
async append() {
throw new Error('not used');
},
};
}
function request(overrides = {}) {
return {
authorityProjectId: 'default',
retentionMs: MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
eligibleBeforeMs: NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
limit: 64,
mutationId: 'a1000000-0000-4000-8000-000000000001',
requestId: 'audit-compact-1',
failureAuditEventId: 'a1000000-0000-4000-8000-000000000002',
principal: PRINCIPAL,
...overrides,
};
}
test('authorizes an instance Owner and binds the exact retention command', async () => {
let command;
const repository = {
async resolveCompaction() {
return null;
},
async compactAuthorized(value) {
command = value;
return {
status: 'inserted',
record: {
mutationId: value.mutationId,
requestId: value.requestId,
authorityProjectId: value.authorization.authorityProjectId,
retentionMs: value.retentionMs,
eligibleBeforeMs: value.eligibleBeforeMs,
batchLimit: value.limit,
deletedCount: 0,
deletedPayloadBytes: 0,
first: null,
last: null,
recordsDigest: 'a'.repeat(64),
createdAtMs: value.audit.occurredAtMs,
},
audit: value.audit,
};
},
async record() {
throw new Error('not used');
},
};
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
repository,
{ now: () => NOW },
);
const result = await service.compact(request());
assert.equal(result.status, 'inserted');
assert.deepEqual(command.authorization, {
authorityProjectId: 'default',
actor: PRINCIPAL.subject,
fence: { projectVersion: 4, bindingVersion: 7 },
});
assert.deepEqual(command.audit, {
eventId: request().mutationId,
requestId: request().requestId,
operationId: 'security.audit.compact',
projectId: 'default',
subject: PRINCIPAL.subject,
authenticationId: PRINCIPAL.authenticationId,
outcome: 'allowed',
reasons: ['instance_authority_security_audit_compaction'],
fence: { projectVersion: 4, bindingVersion: 7 },
occurredAtMs: NOW,
});
});
test('rejects an unsafe retention fence and oversized batch before repository access', async () => {
let accessed = false;
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
{
async resolveCompaction() {
accessed = true;
},
async compactAuthorized() {
accessed = true;
},
async record() {
accessed = true;
},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(
request({
eligibleBeforeMs: NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS + 1,
}),
),
LocalSecurityAuditRetentionConfigurationError,
);
await assert.rejects(
service.compact(request({ limit: 513 })),
LocalSecurityAuditRetentionConfigurationError,
);
assert.equal(accessed, false);
});
test('records denial with the failure identity for a non-Owner', async () => {
const audits = [];
let compacted = false;
const service = createLocalSecurityAuditRetentionService(
projectPolicy('viewer'),
{
async resolveCompaction() {
return null;
},
async compactAuthorized() {
compacted = true;
throw new Error('must not run');
},
async record(audit) {
audits.push(audit);
},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(request()),
LocalSecurityAuditRetentionAuthorizationError,
);
assert.equal(compacted, false);
assert.equal(audits.length, 1);
assert.equal(audits[0].eventId, request().failureAuditEventId);
assert.equal(audits[0].outcome, 'denied');
assert.deepEqual(audits[0].reasons, ['permission_missing']);
});
test('preserves the final credential and authority fence conflict', async () => {
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
{
async resolveCompaction() {
return null;
},
async compactAuthorized() {
throw new LocalSecurityAuditRetentionAuthorizationFenceConflictError();
},
async record() {},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(request()),
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
);
});