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
+32 -3
View File
@@ -4,9 +4,7 @@ export const users = sqliteTable("users", {
id: text("id").primaryKey(),
username: text("username").notNull().unique(),
passwordHash: text("password_hash").notNull(),
role: text("role", { enum: ["admin", "user"] })
.notNull()
.default("user"),
role: text("role").notNull().default("user"),
team: text("team").notNull().default("Default"),
mustChangePassword: integer("must_change_password", { mode: "boolean" }).notNull().default(true),
createdAt: integer("created_at", { mode: "timestamp" })
@@ -69,10 +67,12 @@ export const apiKeys = sqliteTable("api_keys", {
keyHash: text("key_hash").notNull(),
keyPrefix: text("key_prefix"),
name: text("name").notNull().default("Default API Key"),
permissions: text("permissions"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
lastUsedAt: integer("last_used_at", { mode: "timestamp" }),
expiresAt: integer("expires_at", { mode: "timestamp" }),
});
export const pipelines = sqliteTable("pipelines", {
@@ -86,6 +86,35 @@ export const pipelines = sqliteTable("pipelines", {
.$defaultFn(() => new Date()),
});
export const auditLog = sqliteTable("audit_log", {
id: text("id").primaryKey(),
actorId: text("actor_id").references(() => users.id, { onDelete: "set null" }),
actorUsername: text("actor_username").notNull(),
action: text("action").notNull(),
targetType: text("target_type"),
targetId: text("target_id"),
details: text("details"),
ipAddress: text("ip_address"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
export const roles = sqliteTable("roles", {
id: text("id").primaryKey(),
name: text("name").notNull().unique(),
description: text("description").notNull().default(""),
permissions: text("permissions").notNull(),
isBuiltin: integer("is_builtin", { mode: "boolean" }).notNull().default(false),
createdBy: text("created_by").references(() => users.id, { onDelete: "set null" }),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
export const userFiles = sqliteTable("user_files", {
id: text("id").primaryKey(),
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),