feat: add platform audit alert notifications

This commit is contained in:
Codex
2026-06-30 06:46:35 +08:00
parent 5bb0512ba7
commit 7ab702f471
24 changed files with 1731 additions and 30 deletions

View File

@@ -1341,6 +1341,105 @@ async function testPlatformTenantOperationsAndAudit() {
});
assert.equal(invalidAuditAlertStatus.code, 'INVALID_ALERT_STATUS', 'audit alerts should not be reopened through status endpoint');
const notificationChannel = await request('/api/platform-admin/audit-notification-channels', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
channelCode: 'integration_platform_audit',
name: '集成测试平台审计通知',
provider: 'generic',
webhookUrl: 'https://ops.example.test/platform-audit',
secret: 'integration-platform-audit-notification-secret',
minSeverity: 'medium',
statusFilter: ['open'],
actionPatterns: ['platform.tenant.*'],
tenantIds: [tenantId],
timeoutSec: 5,
metadata: { owner: 'security' },
},
});
assert.equal(notificationChannel.item?.channelCode, 'integration_platform_audit', 'platform admin should upsert audit notification channel');
assert.equal(notificationChannel.item?.secretRef, 'app_private.platform_secrets:webhook:integration_platform_audit', 'channel should expose only platform secretRef');
assert.equal(notificationChannel.item?.webhook?.host, 'ops.example.test', 'channel response should expose safe webhook host');
assert.equal(notificationChannel.item?.webhookUrl, undefined, 'channel response must not expose raw webhook URL');
assert.ok(!JSON.stringify(notificationChannel).includes('integration-platform-audit-notification-secret'), 'channel response must not leak webhook secret');
const notificationChannels = await request('/api/platform-admin/audit-notification-channels', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { enabled: true, provider: 'generic', limit: 20 },
});
assert.ok(
notificationChannels.items?.some(item => item.channelCode === 'integration_platform_audit'),
'platform admin should list audit notification channels',
);
assert.ok(!JSON.stringify(notificationChannels).includes('integration-platform-audit-notification-secret'), 'channel list must not leak webhook secret');
const invalidNotificationChannel = await request('/api/platform-admin/audit-notification-channels', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
channelCode: 'bad_channel',
name: 'bad channel',
provider: 'generic',
webhookUrl: 'ftp://ops.example.test/hook',
},
expectStatus: 400,
});
assert.equal(invalidNotificationChannel.code, 'INVALID_WEBHOOK_URL', 'audit notification channel should reject unsafe webhook URL');
const eventPool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
let platformNotificationEventId = '';
try {
const channelRow = await eventPool.query(
"select id from public.platform_audit_notification_channels where channel_code = 'integration_platform_audit' limit 1",
);
const insertedEvent = await eventPool.query(
`
insert into public.platform_audit_notification_events (
channel_id, alert_id, audit_log_id, provider, status, attempts,
last_http_code, request_payload, last_response_summary
)
values (
$1, $2, $3, 'generic', 'sent', 1,
200,
'{"body":{"token":"must-not-leak","nested":{"password":"must-not-leak"}}}'::jsonb,
'{"ok":true}'
)
on conflict (channel_id, alert_id)
do update set status = excluded.status,
attempts = excluded.attempts,
request_payload = excluded.request_payload,
updated_at = now()
returning id
`,
[channelRow.rows[0].id, platformAlertId, statusAuditLog.id],
);
platformNotificationEventId = insertedEvent.rows[0].id;
} finally {
await eventPool.end();
}
const notificationEvents = await request('/api/platform-admin/audit-notification-events', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { alertId: platformAlertId, status: 'sent', limit: 20 },
});
assert.ok(
notificationEvents.items?.some(item => item.id === platformNotificationEventId),
'platform admin should list audit notification events',
);
const listedNotificationEvent = notificationEvents.items?.find(item => item.id === platformNotificationEventId);
assert.equal(listedNotificationEvent?.requestPayload?.body?.token, '[REDACTED]', 'notification event list should redact token-like payload details');
assert.equal(listedNotificationEvent?.requestPayload?.body?.nested?.password, '[REDACTED]', 'notification event list should redact nested password-like payload details');
assert.ok(!JSON.stringify(notificationEvents).includes('must-not-leak'), 'notification event list must not leak sensitive details');
const candidates = await request('/api/platform-admin/invoices/subscription-candidates', {
tenantId: false,
userId: false,
@@ -1555,6 +1654,20 @@ async function testPlatformTenantOperationsAndAudit() {
});
assert.equal(studentAuditAlertStatusDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not update platform audit alerts');
const studentAuditNotificationChannelDenied = await request('/api/platform-admin/audit-notification-channels', {
tenantId: false,
userId: USER_ID,
expectStatus: 403,
});
assert.equal(studentAuditNotificationChannelDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform audit notification channels');
const studentAuditNotificationEventDenied = await request('/api/platform-admin/audit-notification-events', {
tenantId: false,
userId: USER_ID,
expectStatus: 403,
});
assert.equal(studentAuditNotificationEventDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform audit notification events');
const studentReminderDenied = await request('/api/platform-admin/invoices/reminders', {
tenantId: false,
userId: USER_ID,