feat(housekeeping): dispatch audited commands
This commit is contained in:
1 parent
ca666d9f90
commit
6aed71a8b2
8 files changed
+996
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { registerHousekeepingCommand } from "@/features/housekeeping/foundation/commands/registry";
|
||||
import {
|
||||
anyCapability,
|
||||
ok,
|
||||
} from "@/features/housekeeping/foundation/contracts";
|
||||
import type { AuditEntry } from "@/lib/services/audit";
|
||||
|
||||
const { auditEntries, context, rateLimitCalls } = vi.hoisted(() => ({
|
||||
auditEntries: [] as AuditEntry[],
|
||||
context: {
|
||||
actor: { id: 71, username: "server-operator", rank: 4 },
|
||||
isSuperAdmin: false,
|
||||
has: (slug: string) => slug === "admin.settings.edit",
|
||||
hasAny: (...slugs: string[]) => slugs.includes("admin.settings.edit"),
|
||||
hasAll: (...slugs: string[]) =>
|
||||
slugs.every((slug) => slug === "admin.settings.edit"),
|
||||
},
|
||||
rateLimitCalls: [] as Array<[string, number, number]>,
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||
getHousekeepingCapabilityContext: async () => context,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/audit", () => ({
|
||||
housekeepingAuditWriter: {
|
||||
write: async (entry: AuditEntry) => {
|
||||
auditEntries.push({ ...entry });
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
clientIp: async () => "203.0.113.7",
|
||||
rateLimit: async (key: string, attempts: number, windowMs: number) => {
|
||||
rateLimitCalls.push([key, attempts, windowMs]);
|
||||
return { ok: true, retryAfter: 0 };
|
||||
},
|
||||
}));
|
||||
|
||||
import { executeHousekeepingCommand } from "./housekeeping-command";
|
||||
|
||||
registerHousekeepingCommand({
|
||||
id: "system.server-action.serializable",
|
||||
owner: "system",
|
||||
risk: "safe",
|
||||
capability: anyCapability("admin.settings.edit"),
|
||||
input: z.object({ value: z.string() }),
|
||||
requiresReason: false,
|
||||
rateLimit: { attempts: 5, windowMs: 120_000 },
|
||||
execute: async (commandContext, input) =>
|
||||
ok(
|
||||
{
|
||||
value: input.value,
|
||||
actorId: commandContext.capability.actor.id,
|
||||
ipAddress: commandContext.ipAddress,
|
||||
},
|
||||
commandContext.correlationId,
|
||||
),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
auditEntries.length = 0;
|
||||
rateLimitCalls.length = 0;
|
||||
});
|
||||
|
||||
describe("executeHousekeepingCommand", () => {
|
||||
it("accepts a plain request and ignores spoofed server-owned metadata", async () => {
|
||||
const request = {
|
||||
commandId: "system.server-action.serializable",
|
||||
input: { value: "saved" },
|
||||
risk: "sensitive",
|
||||
owner: "people",
|
||||
capability: { mode: "any", slugs: ["forged.permission"] },
|
||||
actor: { id: 999 },
|
||||
ipAddress: "198.51.100.9",
|
||||
rateLimit: { attempts: 999, windowMs: 1 },
|
||||
audit: { action: "forged.action", target: "forged-target" },
|
||||
};
|
||||
|
||||
const result = await executeHousekeepingCommand(request);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
data: {
|
||||
value: "saved",
|
||||
actorId: 71,
|
||||
ipAddress: "203.0.113.7",
|
||||
},
|
||||
});
|
||||
expect(rateLimitCalls).toEqual([
|
||||
[
|
||||
"housekeeping-command:71:203.0.113.7:system.server-action.serializable",
|
||||
5,
|
||||
120_000,
|
||||
],
|
||||
]);
|
||||
expect(auditEntries).toMatchObject([
|
||||
{
|
||||
userId: 71,
|
||||
action: "system.server-action.serializable",
|
||||
target: "system",
|
||||
domain: "system",
|
||||
ipAddress: "203.0.113.7",
|
||||
outcome: "success",
|
||||
},
|
||||
]);
|
||||
expect(auditEntries[0]?.correlationId).toBe(result.correlationId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
dispatchHousekeepingCommand,
|
||||
type HousekeepingCommandRequest,
|
||||
} from "@/features/housekeeping/foundation/commands/dispatcher";
|
||||
import type { HousekeepingResult } from "@/features/housekeeping/foundation/contracts";
|
||||
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { housekeepingAuditWriter } from "@/lib/services/audit";
|
||||
|
||||
export async function executeHousekeepingCommand(
|
||||
request: HousekeepingCommandRequest,
|
||||
): Promise<HousekeepingResult<unknown>> {
|
||||
const [context, ipAddress] = await Promise.all([
|
||||
getHousekeepingCapabilityContext(),
|
||||
clientIp(),
|
||||
]);
|
||||
|
||||
return dispatchHousekeepingCommand(request, {
|
||||
context,
|
||||
ipAddress,
|
||||
audit: housekeepingAuditWriter,
|
||||
rateLimit: async (key, attempts, windowMs) =>
|
||||
(await rateLimit(key, attempts, windowMs)).ok,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user