fix(blueprint): require planning access for detailed reads

This commit is contained in:
2026-03-30 11:55:43 +02:00
parent 7aa32f8a5c
commit 3a29ce4332
5 changed files with 102 additions and 7 deletions
@@ -391,6 +391,7 @@ describe("assistant router tool gating", () => {
expect(userWithoutPlanning).not.toContain("list_allocations");
expect(userWithoutPlanning).not.toContain("list_demands");
expect(userWithoutPlanning).not.toContain("list_blueprints");
expect(userWithoutPlanning).not.toContain("get_blueprint");
expect(userWithoutPlanning).not.toContain("list_clients");
expect(userWithoutPlanning).not.toContain("list_roles");
expect(userWithoutPlanning).not.toContain("list_management_levels");
@@ -402,6 +403,7 @@ describe("assistant router tool gating", () => {
expect(userWithPlanning).toContain("list_allocations");
expect(userWithPlanning).toContain("list_demands");
expect(userWithPlanning).toContain("list_blueprints");
expect(userWithPlanning).toContain("get_blueprint");
expect(userWithPlanning).toContain("list_clients");
expect(userWithPlanning).toContain("list_roles");
expect(userWithPlanning).toContain("list_management_levels");
@@ -1,4 +1,4 @@
import { PermissionKey, SystemRole } from "@capakraken/shared";
import { BlueprintTarget, PermissionKey, SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { blueprintRouter } from "../router/blueprint.js";
import { clientRouter } from "../router/client.js";
@@ -73,6 +73,99 @@ describe("master-data router authorization", () => {
});
});
it("requires planning read access for blueprint list and detailed reads", async () => {
const findMany = vi.fn();
const findUnique = vi.fn();
const findFirst = vi.fn();
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({
blueprint: {
findMany,
findUnique,
findFirst,
},
}));
await expect(caller.list({ isActive: true })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Planning read access required",
});
await expect(caller.getByIdentifier({ identifier: "Consulting Blueprint" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Planning read access required",
});
await expect(caller.getById({ id: "bp_1" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Planning read access required",
});
expect(findMany).not.toHaveBeenCalled();
expect(findUnique).not.toHaveBeenCalled();
expect(findFirst).not.toHaveBeenCalled();
});
it("allows blueprint list and detailed reads for users with planning access", async () => {
const listFindMany = vi.fn().mockResolvedValue([
{
id: "bp_1",
name: "Consulting Blueprint",
target: "PROJECT",
description: "Default consulting setup",
isActive: true,
},
]);
const getByIdFindUnique = vi.fn()
.mockResolvedValueOnce({
id: "bp_1",
name: "Consulting Blueprint",
target: "PROJECT",
description: "Default consulting setup",
fieldDefs: [{ key: "market", type: "text" }],
defaults: { market: "EU" },
validationRules: [],
rolePresets: [],
isActive: true,
})
.mockResolvedValueOnce(null);
const getByIdentifierFindFirst = vi.fn().mockResolvedValue({
id: "bp_1",
name: "Consulting Blueprint",
target: "PROJECT",
description: "Default consulting setup",
fieldDefs: [{ key: "market", type: "text" }],
defaults: { market: "EU" },
validationRules: [],
rolePresets: [],
isActive: true,
});
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({
blueprint: {
findMany: listFindMany,
findUnique: getByIdFindUnique,
findFirst: getByIdentifierFindFirst,
},
}, {
granted: [PermissionKey.VIEW_PLANNING],
}));
const listResult = await caller.list({ target: BlueprintTarget.PROJECT, isActive: true });
const byIdResult = await caller.getById({ id: "bp_1" });
const byIdentifierResult = await caller.getByIdentifier({ identifier: "Consulting Blueprint" });
expect(listResult).toHaveLength(1);
expect(byIdResult.name).toBe("Consulting Blueprint");
expect(byIdentifierResult.fieldDefs).toEqual([{ key: "market", type: "text" }]);
expect(listFindMany).toHaveBeenCalledWith({
where: { target: "PROJECT", isActive: true },
orderBy: { name: "asc" },
});
expect(getByIdFindUnique).toHaveBeenCalledWith({
where: { id: "bp_1" },
});
expect(getByIdentifierFindFirst).toHaveBeenCalledWith({
where: { name: { equals: "Consulting Blueprint", mode: "insensitive" } },
});
});
it("keeps country lists available to authenticated users as safe lookup data", async () => {
const findMany = vi.fn().mockResolvedValue([
{