feat: add housekeeping capability context
This commit is contained in:
1 parent
7d62a04f0e
commit
c63c9830b2
4 files changed
+180
No files matched your search
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { PermissionSet } from "@/types/admin";
|
||||||
|
import {
|
||||||
|
createHousekeepingCapabilityContext,
|
||||||
|
satisfiesCapability,
|
||||||
|
} from "./capability-context";
|
||||||
|
import {
|
||||||
|
allCapabilities,
|
||||||
|
anyCapability,
|
||||||
|
type HousekeepingActor,
|
||||||
|
} from "./contracts";
|
||||||
|
|
||||||
|
const actor: HousekeepingActor = {
|
||||||
|
id: 42,
|
||||||
|
username: "operator",
|
||||||
|
rank: 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissionSet = (
|
||||||
|
slugs: readonly string[],
|
||||||
|
isSuperAdmin = false,
|
||||||
|
): PermissionSet => {
|
||||||
|
const granted = new Set(slugs);
|
||||||
|
const has = (slug: string) => isSuperAdmin || granted.has(slug);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSuperAdmin,
|
||||||
|
has,
|
||||||
|
hasAny: (...requested) => requested.some(has),
|
||||||
|
hasAll: (...requested) => requested.every(has),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("housekeeping capability context", () => {
|
||||||
|
it("evaluates any and all requirements from effective permission methods", () => {
|
||||||
|
const context = createHousekeepingCapabilityContext(
|
||||||
|
actor,
|
||||||
|
permissionSet(["admin.users.view", "admin.tickets.view"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
satisfiesCapability(
|
||||||
|
context,
|
||||||
|
anyCapability("admin.users.view", "admin.logs.view"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
satisfiesCapability(
|
||||||
|
context,
|
||||||
|
allCapabilities("admin.users.view", "admin.logs.view"),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the super administrator bypass explicit", () => {
|
||||||
|
const context = createHousekeepingCapabilityContext(
|
||||||
|
actor,
|
||||||
|
permissionSet([], true),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(context.isSuperAdmin).toBe(true);
|
||||||
|
expect(context.has("unknown.future.slug")).toBe(true);
|
||||||
|
expect(
|
||||||
|
satisfiesCapability(context, allCapabilities("unknown.future.slug")),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates capability checks without applying rank logic", () => {
|
||||||
|
const permissions: PermissionSet = {
|
||||||
|
isSuperAdmin: false,
|
||||||
|
has: vi.fn(() => false),
|
||||||
|
hasAny: vi.fn(() => true),
|
||||||
|
hasAll: vi.fn(() => false),
|
||||||
|
};
|
||||||
|
const context = createHousekeepingCapabilityContext(
|
||||||
|
{ ...actor, rank: 0 },
|
||||||
|
permissions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
satisfiesCapability(context, anyCapability("admin.users.view")),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
satisfiesCapability(context, allCapabilities("admin.users.view")),
|
||||||
|
).toBe(false);
|
||||||
|
expect(permissions.hasAny).toHaveBeenCalledWith("admin.users.view");
|
||||||
|
expect(permissions.hasAll).toHaveBeenCalledWith("admin.users.view");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { PermissionSet } from "@/lib/permissions";
|
||||||
|
import type {
|
||||||
|
CapabilityRequirement,
|
||||||
|
HousekeepingActor,
|
||||||
|
HousekeepingCapabilityContext,
|
||||||
|
} from "./contracts";
|
||||||
|
|
||||||
|
export function createHousekeepingCapabilityContext(
|
||||||
|
actor: HousekeepingActor,
|
||||||
|
permissions: PermissionSet,
|
||||||
|
): HousekeepingCapabilityContext {
|
||||||
|
return {
|
||||||
|
actor,
|
||||||
|
isSuperAdmin: permissions.isSuperAdmin,
|
||||||
|
has: (slug) => permissions.has(slug),
|
||||||
|
hasAny: (...slugs) => permissions.hasAny(...slugs),
|
||||||
|
hasAll: (...slugs) => permissions.hasAll(...slugs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function satisfiesCapability(
|
||||||
|
context: HousekeepingCapabilityContext,
|
||||||
|
requirement: CapabilityRequirement,
|
||||||
|
): boolean {
|
||||||
|
if (context.isSuperAdmin) return true;
|
||||||
|
|
||||||
|
return requirement.mode === "any"
|
||||||
|
? context.hasAny(...requirement.slugs)
|
||||||
|
: context.hasAll(...requirement.slugs);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { getAdminContext } = vi.hoisted(() => ({ getAdminContext: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock("react", () => ({
|
||||||
|
cache: <T extends (...args: never[]) => unknown>(callback: T) => callback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/permissions", () => ({ getAdminContext }));
|
||||||
|
|
||||||
|
import { getHousekeepingCapabilityContext } from "./server-capability-context";
|
||||||
|
|
||||||
|
describe("getHousekeepingCapabilityContext", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
getAdminContext.mockResolvedValue({
|
||||||
|
session: {
|
||||||
|
user: {
|
||||||
|
id: 42,
|
||||||
|
username: "operator",
|
||||||
|
rank: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
isSuperAdmin: false,
|
||||||
|
has: (slug: string) => slug === "admin.users.view",
|
||||||
|
hasAny: (...slugs: string[]) => slugs.includes("admin.users.view"),
|
||||||
|
hasAll: (...slugs: string[]) =>
|
||||||
|
slugs.every((slug) => slug === "admin.users.view"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the refreshed administrator actor and forwarded permissions", async () => {
|
||||||
|
const context = await getHousekeepingCapabilityContext();
|
||||||
|
|
||||||
|
expect(context).toMatchObject({
|
||||||
|
actor: { id: 42, username: "operator", rank: 7 },
|
||||||
|
isSuperAdmin: false,
|
||||||
|
});
|
||||||
|
expect(context.has("admin.users.view")).toBe(true);
|
||||||
|
expect(context.has("admin.logs.view")).toBe(false);
|
||||||
|
expect(getAdminContext).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { cache } from "react";
|
||||||
|
import { getAdminContext } from "@/lib/permissions";
|
||||||
|
import { createHousekeepingCapabilityContext } from "./capability-context";
|
||||||
|
|
||||||
|
export const getHousekeepingCapabilityContext = cache(async () => {
|
||||||
|
const { session, permissions } = await getAdminContext();
|
||||||
|
|
||||||
|
return createHousekeepingCapabilityContext(
|
||||||
|
{
|
||||||
|
id: session.user.id,
|
||||||
|
username: session.user.username,
|
||||||
|
rank: session.user.rank,
|
||||||
|
},
|
||||||
|
permissions,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user