feat: production-grade RBAC with editor role, custom roles, API key scoping, and audit log (#89)

* feat(rbac): add editor role, 3 new permissions, ownership helper

* feat(rbac): add audit_log table, apiKeys.permissions column, editor role to schema

* feat(rbac): wire requirePermission into all routes, add editor role support

* refactor(rbac): replace ad-hoc role checks with permission-based ownership

* feat(rbac): add audit log DB writes + query endpoint

Dual-write audit events to stdout (existing) and SQLite audit_log table.
Add GET /api/v1/audit-log with pagination, action filter, and date range
filtering, gated behind audit:read permission.

* feat(rbac): add API key permission scoping with ceiling enforcement

* feat(rbac): add escalation prevention and last-admin protection

* feat(rbac): add editor role to UI, API key permission scoping in settings

* test(rbac): add full permission matrix integration test

* test(rbac): add editor role E2E tests

* feat(rbac): add custom roles with CRUD API and DB-backed permission lookup

* feat(rbac): add API key expiration

* feat(rbac): add roles management UI and API key expiration to settings

* feat(rbac): add audit log UI to settings

* fix: remove any cast in API key permission validation

* test(rbac): add unit tests for username validation rules

* test(rbac): add unit tests for effective permissions and ownership

* test(rbac): add comprehensive route permission matrix (all routes × all roles)

* test(rbac): add auth route edge case tests (login failures, session expiry, password side effects)

* test(rbac): add escalation prevention tests (register, update, self-demote, last-admin)

* test(rbac): add ownership enforcement tests (files, pipelines, editor access, cross-user isolation)

* test(rbac): add API key edge cases (name validation, delete behavior, key revocation)

* test(rbac): add audit log edge cases (all events, pagination clamping, structure)

* test(rbac): add custom roles edge case tests (validation, CRUD, functional permissions)

* test(rbac): add comprehensive E2E tests (roles UI, audit log, custom role, API key scoping)
This commit is contained in:
Ashim
2026-04-22 18:10:04 +08:00
committed by GitHub
parent 2d7a61c18f
commit 5a45bcbc8f
40 changed files with 5054 additions and 87 deletions
+45 -3
View File
@@ -1,4 +1,6 @@
import { randomUUID } from "node:crypto";
import type { FastifyBaseLogger } from "fastify";
import { db, schema } from "../db/index.js";
type AuditEvent =
| "LOGIN_SUCCESS"
@@ -12,13 +14,16 @@ type AuditEvent =
| "FILE_UPLOADED"
| "FILE_DELETED"
| "API_KEY_CREATED"
| "API_KEY_DELETED";
| "API_KEY_DELETED"
| "ROLE_CREATED"
| "ROLE_UPDATED"
| "ROLE_DELETED"
| "SETTINGS_UPDATED";
/**
* Emit a structured audit log entry for security-relevant events.
*
* Logs are written at INFO level with `audit: true` so they can be
* filtered by log aggregators (e.g. `jq 'select(.audit)'`).
* Dual-writes: structured stdout log (for aggregators) + SQLite row.
*/
export function auditLog(
logger: FastifyBaseLogger,
@@ -26,4 +31,41 @@ export function auditLog(
details: Record<string, unknown> = {},
): void {
logger.info({ audit: true, event, ...details }, `[AUDIT] ${event}`);
const actorId = (details.userId as string) ?? (details.adminId as string) ?? null;
const actorUsername = (details.username as string) ?? (details.newUsername as string) ?? "system";
const targetId = (details.targetUserId as string) ?? (details.keyId as string) ?? null;
const targetType = deriveTargetType(event);
try {
db.insert(schema.auditLog)
.values({
id: randomUUID(),
actorId,
actorUsername,
action: event,
targetType,
targetId,
details: JSON.stringify(details),
ipAddress: null,
})
.run();
} catch {
logger.warn({ event }, "Failed to write audit log to DB");
}
}
function deriveTargetType(event: AuditEvent): string | null {
if (
event.startsWith("USER_") ||
event.startsWith("LOGIN") ||
event.startsWith("PASSWORD") ||
event === "LOGOUT"
)
return "user";
if (event.startsWith("API_KEY")) return "api_key";
if (event.startsWith("FILE")) return "file";
if (event.startsWith("ROLE")) return "role";
if (event === "SETTINGS_UPDATED") return "setting";
return null;
}