feat(blueprint): scope summary reads to planning audience

This commit is contained in:
2026-03-30 10:55:28 +02:00
parent 9b764008c3
commit 81a46c81bd
5 changed files with 65 additions and 2 deletions
@@ -387,6 +387,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("list_clients");
expect(userWithoutPlanning).not.toContain("list_roles");
expect(userWithoutPlanning).not.toContain("list_management_levels");
@@ -397,6 +398,7 @@ describe("assistant router tool gating", () => {
expect(userWithoutPlanning).not.toContain("find_best_project_resource");
expect(userWithPlanning).toContain("list_allocations");
expect(userWithPlanning).toContain("list_demands");
expect(userWithPlanning).toContain("list_blueprints");
expect(userWithPlanning).toContain("list_clients");
expect(userWithPlanning).toContain("list_roles");
expect(userWithPlanning).toContain("list_management_levels");
@@ -1,5 +1,6 @@
import { PermissionKey, SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { blueprintRouter } from "../router/blueprint.js";
import { clientRouter } from "../router/client.js";
import { countryRouter } from "../router/country.js";
import { managementLevelRouter } from "../router/management-level.js";
@@ -26,6 +27,52 @@ function createProtectedContext(
}
describe("master-data router authorization", () => {
it("requires planning read access for blueprint summaries with project counts", async () => {
const findMany = vi.fn();
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({
blueprint: {
findMany,
},
}));
await expect(caller.listSummaries()).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Planning read access required",
});
expect(findMany).not.toHaveBeenCalled();
});
it("allows blueprint summaries for users with planning access", async () => {
const findMany = vi.fn().mockResolvedValue([
{
id: "bp_1",
name: "Consulting Blueprint",
_count: { projects: 4 },
},
]);
const caller = createCallerFactory(blueprintRouter)(createProtectedContext({
blueprint: {
findMany,
},
}, {
granted: [PermissionKey.VIEW_PLANNING],
}));
const result = await caller.listSummaries();
expect(result).toHaveLength(1);
expect(result[0]?._count.projects).toBe(4);
expect(findMany).toHaveBeenCalledWith({
select: {
id: true,
name: true,
_count: { select: { projects: true } },
},
orderBy: { name: "asc" },
});
});
it("keeps country lists available to authenticated users as safe lookup data", async () => {
const findMany = vi.fn().mockResolvedValue([
{
+1
View File
@@ -228,6 +228,7 @@ const COST_TOOLS = new Set([
const PLANNING_READ_TOOLS = new Set([
"list_allocations",
"list_demands",
"list_blueprints",
"list_clients",
"list_roles",
"list_management_levels",
+2 -2
View File
@@ -2,11 +2,11 @@ import { BlueprintTarget, CreateBlueprintSchema, UpdateBlueprintSchema, type Blu
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc.js";
import { adminProcedure, createTRPCRouter, planningReadProcedure, protectedProcedure } from "../trpc.js";
import { createAuditEntry } from "../lib/audit.js";
export const blueprintRouter = createTRPCRouter({
listSummaries: protectedProcedure
listSummaries: planningReadProcedure
.query(async ({ ctx }) => {
return ctx.db.blueprint.findMany({
select: {