fix: enforce public contrast and audit rank errors

This commit is contained in:
Simo committed 2026-07-11 22:06:45 +02:00
1 parent e1381c0f40
commit b695a33ead
12 files changed
+184 -29

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
export interface AuthorizationEvent {
kind: "permission.denied" | "permission.load_error";
userId: number;
username?: string;
rank: number;
permission?: string;
source: string;
reason: string;
error?: unknown;
}
const clean = (value: string) => value.replace(/(token|password|secret|cookie|authorization|select|insert|update|delete)[^\s]*/gi, "[REDACTED]").slice(0, 160);
export function authorizationActivity(event: AuthorizationEvent) {
const description = [
`user=${clean(event.username ?? String(event.userId))}`,
`rank=${event.rank}`,
event.permission ? `permission=${clean(event.permission)}` : null,
`source=${clean(event.source)}`,
`reason=${clean(event.reason)}`,
].filter(Boolean).join("; ");
return { staffId: event.userId, action: event.kind, description, targetType: "user", targetId: event.userId };
}
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { authorizationActivity } from "@/lib/admin/authorization-event";
describe("authorizationActivity", () => {
it("creates a safe rank-aware denial record", () => {
const record = authorizationActivity({ kind: "permission.denied", userId: 42, username: "admin", rank: 11, permission: "admin.logs.view", source: "/admin/logs", reason: "missing permission" });
expect(record.action).toBe("permission.denied");
expect(record.description).toContain("rank=11");
expect(record.description).toContain("permission=admin.logs.view");
});
it("redacts secrets and technical details", () => {
const record = authorizationActivity({ kind: "permission.load_error", userId: 42, rank: 11, source: "permissions", reason: "token=abc password=hunter2 SELECT * FROM users" });
expect(record.description).not.toMatch(/abc|hunter2|SELECT/i);
});
});
+9
View File
@@ -0,0 +1,9 @@
import { logServerError } from "@/lib/server-log";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { authorizationActivity, type AuthorizationEvent } from "@/lib/admin/authorization-event";
export async function logAuthorizationEvent(event: AuthorizationEvent): Promise<void> {
const correlationId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
await logStaffActivity({ ...authorizationActivity(event), description: `${authorizationActivity(event).description}; correlation=${correlationId}` });
if (event.error) logServerError(event.kind, event.error, { correlationId, userId: event.userId, rank: event.rank, permission: event.permission ?? null, source: event.source });
}
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { buildStaffActivityWhere } from "@/lib/admin/log-filters";
describe("buildStaffActivityWhere", () => {
it("filters authorization events by prefix", () => {
expect(buildStaffActivityWhere({ authorizationOnly: true })).toEqual({ action: { startsWith: "permission." } });
});
it("combines staff and search filters", () => {
const result = buildStaffActivityWhere({ q: "rank", staffId: 11, authorizationOnly: true });
expect(result).toMatchObject({ userId: 11n, action: { startsWith: "permission." } });
expect(result.OR).toHaveLength(3);
});
});
+16
View File
@@ -0,0 +1,16 @@
import type { Prisma } from "@/generated/prisma/client";
export interface StaffActivityFilters { q?: string; staffId?: number | null; action?: string | null; authorizationOnly?: boolean }
export function buildStaffActivityWhere(filters: StaffActivityFilters): Prisma.StaffActivitiesWhereInput {
const where: Prisma.StaffActivitiesWhereInput = {};
if (filters.q?.trim()) where.OR = [
{ action: { contains: filters.q.trim() } },
{ description: { contains: filters.q.trim() } },
{ ipAddress: { contains: filters.q.trim() } },
];
if (filters.staffId) where.userId = BigInt(filters.staffId);
if (filters.authorizationOnly) where.action = { startsWith: "permission." };
else if (filters.action) where.action = { contains: filters.action };
return where;
}