feat(audit): add platform security audit log

New audit_events table, distinct from the Provisioner's environment_events (Lab-lifecycle
only). recordAudit() is called from the auth/RBAC/admin layers added in this phase — login
success/failure, logout, password change, instructor CRUD, role changes, session
revocation, cohort/roster changes, curriculum assignment, Prepare/Destroy Labs, and every
403 authorization denial (recorded centrally by the RBAC middleware, not per-route). Never
records plaintext passwords, raw session tokens, Provisioner secrets, or raw routing tokens.
This commit is contained in:
2026-08-18 13:36:11 -05:00
parent 85fa920f96
commit 37230d4ef1
2 changed files with 40 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
// Platform-level security/admin audit log — distinct from the Provisioner's environment_events
// (Lab lifecycle only). Never pass plaintext passwords, raw session tokens, Provisioner
// secrets, or raw routing tokens into `metadata` — callers are responsible for that, this
// module doesn't scrub input.
async function recordAudit(pool, { actorType, actorId, action, resourceType, resourceId, result, sourceIp, metadata }) {
try {
await pool.query(
`INSERT INTO audit_events (actor_type, actor_id, action, resource_type, resource_id, result, source_ip, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[actorType, actorId ? String(actorId) : null, action, resourceType || null, resourceId ? String(resourceId) : null,
result, sourceIp || null, metadata ? JSON.stringify(metadata) : null]
);
} catch (e) {
// Audit logging must never take down the request it's describing.
console.error('[audit] failed to record event:', action, e.message);
}
}
module.exports = { recordAudit };