feat(audit): локальный журнал и push в auth-portal
Таблица audit_log, recordAudit на мутациях, GET /api/v1/audit и dual-write source_app=fw. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const AUDIT_SEVERITIES = ['info', 'warning', 'critical'] as const
|
||||
export type AuditSeverity = (typeof AUDIT_SEVERITIES)[number]
|
||||
export const auditSeveritySchema = z.enum(AUDIT_SEVERITIES)
|
||||
|
||||
export const AUDIT_SOURCE_APPS = ['portal', 'vps', 'cfdm', 'bgp', 'fw'] as const
|
||||
export type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number]
|
||||
export const auditSourceAppSchema = z.enum(AUDIT_SOURCE_APPS)
|
||||
|
||||
export const AUDIT_TARGET_TYPES = [
|
||||
'user',
|
||||
'settings',
|
||||
'session',
|
||||
'system',
|
||||
'app_resource',
|
||||
] as const
|
||||
export type AuditTargetType = (typeof AUDIT_TARGET_TYPES)[number]
|
||||
export const auditTargetTypeSchema = z.enum(AUDIT_TARGET_TYPES)
|
||||
|
||||
/** EvoFirewall action keys pushed to auth-portal ingest. */
|
||||
export const FW_AUDIT_ACTIONS = [
|
||||
'agent.create',
|
||||
'agent.update',
|
||||
'agent.approve',
|
||||
'agent.revoke',
|
||||
'agent.delete',
|
||||
'agent.clone_rules',
|
||||
'agent.policy_sets.update',
|
||||
'override.create',
|
||||
'override.delete',
|
||||
'list.create',
|
||||
'list.entries.add',
|
||||
'list.entries.delete',
|
||||
'list.refresh',
|
||||
'list.delete',
|
||||
'policy_set.create',
|
||||
'policy_set.update',
|
||||
'policy_set.delete',
|
||||
'rule.create',
|
||||
'rule.update',
|
||||
'rule.reorder',
|
||||
'rule.delete',
|
||||
] as const
|
||||
export type FwAuditAction = (typeof FW_AUDIT_ACTIONS)[number]
|
||||
|
||||
export const auditLogEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
event_id: z.string().nullable(),
|
||||
source_app: auditSourceAppSchema,
|
||||
action: z.string(),
|
||||
severity: auditSeveritySchema,
|
||||
actor_user_id: z.string().nullable(),
|
||||
actor_email: z.string().nullable(),
|
||||
actor_name: z.string().nullable(),
|
||||
target_type: auditTargetTypeSchema.nullable(),
|
||||
target_id: z.string().nullable(),
|
||||
summary: z.string(),
|
||||
details: z.record(z.string(), z.unknown()).nullable(),
|
||||
ip: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
})
|
||||
export type AuditLogEntry = z.infer<typeof auditLogEntrySchema>
|
||||
|
||||
export const auditListQuerySchema = z.object({
|
||||
action: z.string().optional(),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
limit: z.coerce.number().int().min(1).max(500).default(200),
|
||||
})
|
||||
export type AuditListQuery = z.infer<typeof auditListQuerySchema>
|
||||
|
||||
export const ingestAuditEventSchema = z.object({
|
||||
event_id: z.string().min(1).max(128),
|
||||
source_app: z.literal('fw'),
|
||||
action: z.string().min(1).max(200),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
actor_user_id: z.string().nullable().optional(),
|
||||
actor_email: z.string().email().nullable().optional(),
|
||||
actor_name: z.string().nullable().optional(),
|
||||
target_type: auditTargetTypeSchema.nullable().optional(),
|
||||
target_id: z.string().nullable().optional(),
|
||||
summary: z.string().min(1).max(500),
|
||||
details: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
ip: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
})
|
||||
export type IngestAuditEvent = z.infer<typeof ingestAuditEventSchema>
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './contracts.js'
|
||||
export * from './contracts/audit.js'
|
||||
export * from './list-entries.js'
|
||||
export * from './permissions.js'
|
||||
export * from './app-switcher.js'
|
||||
|
||||
@@ -35,7 +35,11 @@ export function permissionForRequest(
|
||||
if (path.startsWith('/api/v1/lists')) {
|
||||
return write ? 'fw:lists:write' : 'fw:lists:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/rules') || path.startsWith('/api/v1/policies')) {
|
||||
if (
|
||||
path.startsWith('/api/v1/rules') ||
|
||||
path.startsWith('/api/v1/policies') ||
|
||||
path.startsWith('/api/v1/policy-sets')
|
||||
) {
|
||||
return write ? 'fw:policies:write' : 'fw:policies:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/stats') || path.startsWith('/api/v1/dashboard')) {
|
||||
@@ -44,6 +48,9 @@ export function permissionForRequest(
|
||||
if (path.startsWith('/api/v1/settings') || path.startsWith('/api/v1/install-context')) {
|
||||
return write ? 'fw:settings:admin' : 'fw:settings:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/audit')) {
|
||||
return 'fw:audit:read'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user