feat(auth): introduce explicit planning read permission
This commit is contained in:
@@ -8,6 +8,7 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
|||||||
const ALL_PERMISSION_KEYS = Object.values(PermissionKey);
|
const ALL_PERMISSION_KEYS = Object.values(PermissionKey);
|
||||||
|
|
||||||
const PERMISSION_LABELS: Record<string, string> = {
|
const PERMISSION_LABELS: Record<string, string> = {
|
||||||
|
viewPlanning: "View Planning",
|
||||||
viewCosts: "View Costs",
|
viewCosts: "View Costs",
|
||||||
useAssistantAdvancedTools: "Assistant Advanced Tools",
|
useAssistantAdvancedTools: "Assistant Advanced Tools",
|
||||||
exportData: "Export Data",
|
exportData: "Export Data",
|
||||||
@@ -24,6 +25,7 @@ const PERMISSION_LABELS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PERMISSION_DESCRIPTIONS: Record<string, string> = {
|
const PERMISSION_DESCRIPTIONS: Record<string, string> = {
|
||||||
|
viewPlanning: "Read project and allocation planning views without mutation access",
|
||||||
viewCosts: "Access to cost data, budget views, and financial reports",
|
viewCosts: "Access to cost data, budget views, and financial reports",
|
||||||
useAssistantAdvancedTools: "Unlocks advanced AI assistant workflows for complex cross-entity analyses",
|
useAssistantAdvancedTools: "Unlocks advanced AI assistant workflows for complex cross-entity analyses",
|
||||||
exportData: "Export data to Excel, CSV, or PDF formats",
|
exportData: "Export data to Excel, CSV, or PDF formats",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useViewPrefs } from "~/hooks/useViewPrefs.js";
|
|||||||
const ALL_PERMISSION_KEYS = Object.values(PermissionKey);
|
const ALL_PERMISSION_KEYS = Object.values(PermissionKey);
|
||||||
|
|
||||||
const PERMISSION_LABELS: Record<string, string> = {
|
const PERMISSION_LABELS: Record<string, string> = {
|
||||||
|
viewPlanning: "View Planning",
|
||||||
viewCosts: "View Costs",
|
viewCosts: "View Costs",
|
||||||
useAssistantAdvancedTools: "Assistant Advanced Tools",
|
useAssistantAdvancedTools: "Assistant Advanced Tools",
|
||||||
exportData: "Export Data",
|
exportData: "Export Data",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- `self-service`: authenticated users can only read or mutate data that belongs to their linked resource or account
|
- `self-service`: authenticated users can only read or mutate data that belongs to their linked resource or account
|
||||||
- `authenticated-safe-lookup`: authenticated users can access a deliberately narrow, identity-safe lookup surface
|
- `authenticated-safe-lookup`: authenticated users can access a deliberately narrow, identity-safe lookup surface
|
||||||
- `resource-overview`: users with `viewAllResources` or `manageResources`
|
- `resource-overview`: users with `viewAllResources` or `manageResources`
|
||||||
- `planning-read`: users with at least one of `viewCosts`, `manageProjects`, or `manageAllocations`
|
- `planning-read`: users with `viewPlanning`
|
||||||
- `controller-finance`: controller, manager, or admin through `controllerProcedure`
|
- `controller-finance`: controller, manager, or admin through `controllerProcedure`
|
||||||
- `manager-write`: manager or admin through `managerProcedure`
|
- `manager-write`: manager or admin through `managerProcedure`
|
||||||
- `admin-only`: admin through `adminProcedure`
|
- `admin-only`: admin through `adminProcedure`
|
||||||
@@ -49,6 +49,6 @@
|
|||||||
|
|
||||||
## Immediate Follow-Ups
|
## Immediate Follow-Ups
|
||||||
|
|
||||||
- introduce a dedicated project-read permission instead of the current interim `planning-read` composite
|
- monitor whether `viewPlanning` should later split into narrower project-read vs allocation-read audiences
|
||||||
- split `allocation` further into narrower future audiences where resource-capacity and staffing-demand reads diverge
|
- split `allocation` further into narrower future audiences where resource-capacity and staffing-demand reads diverge
|
||||||
- add authorization tests for every route listed above so the matrix is CI-enforced, not just documented
|
- add authorization tests for every route listed above so the matrix is CI-enforced, not just documented
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AllocationStatus, SystemRole } from "@capakraken/shared";
|
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { allocationRouter } from "../router/allocation.js";
|
import { allocationRouter } from "../router/allocation.js";
|
||||||
import { emitAllocationCreated, emitAllocationDeleted, emitNotificationCreated } from "../sse/event-bus.js";
|
import { emitAllocationCreated, emitAllocationDeleted, emitNotificationCreated } from "../sse/event-bus.js";
|
||||||
@@ -92,6 +92,24 @@ function createProtectedCaller(db: Record<string, unknown>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createProtectedCallerWithOverrides(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
|
||||||
|
) {
|
||||||
|
return createCaller({
|
||||||
|
session: {
|
||||||
|
user: { email: "user@example.com", name: "User", image: null },
|
||||||
|
expires: "2026-03-13T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
db: db as never,
|
||||||
|
dbUser: {
|
||||||
|
id: "user_1",
|
||||||
|
systemRole: SystemRole.USER,
|
||||||
|
permissionOverrides: overrides,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("allocation router authorization", () => {
|
describe("allocation router authorization", () => {
|
||||||
const planningWindow = {
|
const planningWindow = {
|
||||||
resourceId: "resource_1",
|
resourceId: "resource_1",
|
||||||
@@ -140,6 +158,54 @@ describe("allocation router authorization", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows explicit viewPlanning overrides to read assignment lists", async () => {
|
||||||
|
const assignment = {
|
||||||
|
id: "assignment_1",
|
||||||
|
demandRequirementId: null,
|
||||||
|
resourceId: "resource_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-02T00:00:00.000Z"),
|
||||||
|
hoursPerDay: 8,
|
||||||
|
percentage: 100,
|
||||||
|
role: "Compositor",
|
||||||
|
roleId: "role_comp",
|
||||||
|
dailyCostCents: 40000,
|
||||||
|
status: AllocationStatus.ACTIVE,
|
||||||
|
metadata: {},
|
||||||
|
resource: null,
|
||||||
|
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
|
||||||
|
roleEntity: null,
|
||||||
|
demandRequirement: null,
|
||||||
|
};
|
||||||
|
const db = {
|
||||||
|
assignment: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([assignment]),
|
||||||
|
},
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue(null),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const caller = createProtectedCallerWithOverrides(db, {
|
||||||
|
granted: [PermissionKey.VIEW_PLANNING],
|
||||||
|
});
|
||||||
|
const result = await caller.listAssignments({});
|
||||||
|
|
||||||
|
expect(result).toEqual([assignment]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat viewCosts as a substitute for viewPlanning on planning reads", async () => {
|
||||||
|
const caller = createProtectedCallerWithOverrides({}, {
|
||||||
|
granted: [PermissionKey.VIEW_COSTS],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(caller.listAssignments({})).rejects.toMatchObject({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Planning read access required",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ name: "list", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.list({}) },
|
{ name: "list", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.list({}) },
|
||||||
{ name: "listView", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listView({}) },
|
{ name: "listView", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listView({}) },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OrderType, AllocationType, ProjectStatus, SystemRole } from "@capakraken/shared";
|
import { OrderType, AllocationType, PermissionKey, ProjectStatus, SystemRole } from "@capakraken/shared";
|
||||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
import { invalidateDashboardCache } from "../lib/cache.js";
|
import { invalidateDashboardCache } from "../lib/cache.js";
|
||||||
import { logger } from "../lib/logger.js";
|
import { logger } from "../lib/logger.js";
|
||||||
@@ -110,6 +110,24 @@ function createProtectedCaller(db: Record<string, unknown>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createProtectedCallerWithOverrides(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
|
||||||
|
) {
|
||||||
|
return createCaller({
|
||||||
|
session: {
|
||||||
|
user: { email: "user@example.com", name: "User", image: null },
|
||||||
|
expires: "2099-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
db: db as never,
|
||||||
|
dbUser: {
|
||||||
|
id: "user_1",
|
||||||
|
systemRole: SystemRole.USER,
|
||||||
|
permissionOverrides: overrides,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const sampleProject = {
|
const sampleProject = {
|
||||||
id: "project_1",
|
id: "project_1",
|
||||||
shortCode: "PRJ-001",
|
shortCode: "PRJ-001",
|
||||||
@@ -748,6 +766,61 @@ describe("project router", () => {
|
|||||||
).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
|
).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows explicit viewPlanning overrides to access lightweight project search summaries", async () => {
|
||||||
|
const db = {
|
||||||
|
project: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "project_1",
|
||||||
|
shortCode: "GDM",
|
||||||
|
name: "Gelddruckmaschine",
|
||||||
|
status: ProjectStatus.ACTIVE,
|
||||||
|
startDate: new Date("2026-01-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-03-31T00:00:00.000Z"),
|
||||||
|
client: { name: "Acme Mobility" },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const caller = createProtectedCallerWithOverrides(db, {
|
||||||
|
granted: [PermissionKey.VIEW_PLANNING],
|
||||||
|
});
|
||||||
|
const result = await caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 });
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "project_1",
|
||||||
|
code: "GDM",
|
||||||
|
name: "Gelddruckmaschine",
|
||||||
|
status: "ACTIVE",
|
||||||
|
start: "2026-01-01",
|
||||||
|
end: "2026-03-31",
|
||||||
|
client: "Acme Mobility",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat viewCosts as a substitute for viewPlanning on lightweight project search summaries", async () => {
|
||||||
|
const db = {
|
||||||
|
project: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const caller = createProtectedCallerWithOverrides(db, {
|
||||||
|
granted: [PermissionKey.VIEW_COSTS],
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }),
|
||||||
|
).rejects.toThrow(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Planning read access required",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns lightweight project identifier reads from the canonical router", async () => {
|
it("returns lightweight project identifier reads from the canonical router", async () => {
|
||||||
const db = {
|
const db = {
|
||||||
project: {
|
project: {
|
||||||
|
|||||||
@@ -152,8 +152,7 @@ export const resourceOverviewProcedure = protectedProcedure.use(({ ctx, next })
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Planning read procedure — allows broad planning/project read access without opening it to all users.
|
* Planning read procedure — requires the explicit broad planning read audience.
|
||||||
* This is an interim audience gate until dedicated project-read permissions exist.
|
|
||||||
*/
|
*/
|
||||||
export const planningReadProcedure = protectedProcedure.use(({ ctx, next }) => {
|
export const planningReadProcedure = protectedProcedure.use(({ ctx, next }) => {
|
||||||
const user = ctx.dbUser;
|
const user = ctx.dbUser;
|
||||||
@@ -165,11 +164,7 @@ export const planningReadProcedure = protectedProcedure.use(({ ctx, next }) => {
|
|||||||
ctx.roleDefaults ?? undefined,
|
ctx.roleDefaults ?? undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!permissions.has(PermissionKey.VIEW_PLANNING)) {
|
||||||
!permissions.has(PermissionKey.VIEW_COSTS)
|
|
||||||
&& !permissions.has(PermissionKey.MANAGE_PROJECTS)
|
|
||||||
&& !permissions.has(PermissionKey.MANAGE_ALLOCATIONS)
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Planning read access required",
|
message: "Planning read access required",
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
INSERT INTO "system_role_configs" (
|
||||||
|
"role",
|
||||||
|
"label",
|
||||||
|
"description",
|
||||||
|
"defaultPermissions",
|
||||||
|
"color",
|
||||||
|
"sortOrder"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'ADMIN',
|
||||||
|
'Admin',
|
||||||
|
'Full platform administration and security management.',
|
||||||
|
'["viewPlanning","viewCosts","useAssistantAdvancedTools","exportData","importData","approveVacations","manageBlueprints","viewAllResources","manageResources","manageProjects","manageAllocations","manageRoles","manageUsers","viewScores"]'::jsonb,
|
||||||
|
'purple',
|
||||||
|
1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'MANAGER',
|
||||||
|
'Manager',
|
||||||
|
'Operational delivery management across resources, projects, and staffing.',
|
||||||
|
'["viewPlanning","viewCosts","exportData","importData","approveVacations","viewAllResources","manageResources","manageProjects","manageAllocations","manageRoles","viewScores"]'::jsonb,
|
||||||
|
'blue',
|
||||||
|
2
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'CONTROLLER',
|
||||||
|
'Controller',
|
||||||
|
'Read-heavy planning, resource, and financial oversight.',
|
||||||
|
'["viewPlanning","viewCosts","exportData","viewAllResources"]'::jsonb,
|
||||||
|
'amber',
|
||||||
|
3
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'USER',
|
||||||
|
'User',
|
||||||
|
'Standard authenticated access with self-service capabilities only.',
|
||||||
|
'[]'::jsonb,
|
||||||
|
'gray',
|
||||||
|
4
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'VIEWER',
|
||||||
|
'Viewer',
|
||||||
|
'Restricted read-only access for limited observation scenarios.',
|
||||||
|
'[]'::jsonb,
|
||||||
|
'gray',
|
||||||
|
5
|
||||||
|
)
|
||||||
|
ON CONFLICT ("role") DO NOTHING;
|
||||||
|
|
||||||
|
UPDATE "system_role_configs"
|
||||||
|
SET
|
||||||
|
"defaultPermissions" = CASE
|
||||||
|
WHEN jsonb_typeof("defaultPermissions") = 'array'
|
||||||
|
AND NOT ("defaultPermissions" @> '["viewPlanning"]'::jsonb)
|
||||||
|
THEN "defaultPermissions" || '["viewPlanning"]'::jsonb
|
||||||
|
WHEN "defaultPermissions" IS NULL
|
||||||
|
THEN '["viewPlanning"]'::jsonb
|
||||||
|
ELSE "defaultPermissions"
|
||||||
|
END,
|
||||||
|
"updatedAt" = CURRENT_TIMESTAMP
|
||||||
|
WHERE "role" IN ('ADMIN', 'MANAGER', 'CONTROLLER');
|
||||||
@@ -6,6 +6,7 @@ import { SystemRole } from "@capakraken/shared";
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { assertDestructiveDbAllowed } from "./destructive-db-guard.js";
|
import { assertDestructiveDbAllowed } from "./destructive-db-guard.js";
|
||||||
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
|
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
|
||||||
|
import { buildSystemRoleConfigSeedData } from "./system-role-config-defaults.js";
|
||||||
|
|
||||||
loadWorkspaceEnv();
|
loadWorkspaceEnv();
|
||||||
|
|
||||||
@@ -139,6 +140,20 @@ async function bootstrapPlatform(adminEmail: string, adminPassword: string, admi
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const config of buildSystemRoleConfigSeedData()) {
|
||||||
|
await prisma.systemRoleConfig.upsert({
|
||||||
|
where: { role: config.role },
|
||||||
|
update: {
|
||||||
|
label: config.label,
|
||||||
|
description: config.description,
|
||||||
|
defaultPermissions: config.defaultPermissions,
|
||||||
|
color: config.color,
|
||||||
|
sortOrder: config.sortOrder,
|
||||||
|
},
|
||||||
|
create: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return admin;
|
return admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,28 @@ import { hash } from "@node-rs/argon2";
|
|||||||
import { getHolidayDemoProfileForIndex } from "./holiday-demo-profiles.js";
|
import { getHolidayDemoProfileForIndex } from "./holiday-demo-profiles.js";
|
||||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||||
import { assertSafeSeedTarget } from "./safe-destructive-env.js";
|
import { assertSafeSeedTarget } from "./safe-destructive-env.js";
|
||||||
|
import { buildSystemRoleConfigSeedData } from "./system-role-config-defaults.js";
|
||||||
|
|
||||||
loadWorkspaceEnv();
|
loadWorkspaceEnv();
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function seedSystemRoleConfigs() {
|
||||||
|
for (const config of buildSystemRoleConfigSeedData()) {
|
||||||
|
await prisma.systemRoleConfig.upsert({
|
||||||
|
where: { role: config.role },
|
||||||
|
update: {
|
||||||
|
label: config.label,
|
||||||
|
description: config.description,
|
||||||
|
defaultPermissions: config.defaultPermissions,
|
||||||
|
color: config.color,
|
||||||
|
sortOrder: config.sortOrder,
|
||||||
|
},
|
||||||
|
create: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Skill helpers ─────────────────────────────────────────────────────────────
|
// ─── Skill helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface SkillEntry {
|
interface SkillEntry {
|
||||||
@@ -338,6 +355,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.warn(`Users: admin=${admin.id}, manager=${manager.id}, viewer=${viewer.id}`);
|
console.warn(`Users: admin=${admin.id}, manager=${manager.id}, viewer=${viewer.id}`);
|
||||||
|
await seedSystemRoleConfigs();
|
||||||
|
|
||||||
// ── 2b. Create Dispo v2 entities ──────────────────────────────────────────
|
// ── 2b. Create Dispo v2 entities ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ROLE_DEFAULT_PERMISSIONS, SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
export const SYSTEM_ROLE_CONFIG_DEFAULTS = [
|
||||||
|
{
|
||||||
|
role: SystemRole.ADMIN,
|
||||||
|
label: "Admin",
|
||||||
|
description: "Full platform administration and security management.",
|
||||||
|
color: "purple",
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: SystemRole.MANAGER,
|
||||||
|
label: "Manager",
|
||||||
|
description: "Operational delivery management across resources, projects, and staffing.",
|
||||||
|
color: "blue",
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: SystemRole.CONTROLLER,
|
||||||
|
label: "Controller",
|
||||||
|
description: "Read-heavy planning, resource, and financial oversight.",
|
||||||
|
color: "amber",
|
||||||
|
sortOrder: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: SystemRole.USER,
|
||||||
|
label: "User",
|
||||||
|
description: "Standard authenticated access with self-service capabilities only.",
|
||||||
|
color: "gray",
|
||||||
|
sortOrder: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: SystemRole.VIEWER,
|
||||||
|
label: "Viewer",
|
||||||
|
description: "Restricted read-only access for limited observation scenarios.",
|
||||||
|
color: "gray",
|
||||||
|
sortOrder: 5,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function buildSystemRoleConfigSeedData() {
|
||||||
|
return SYSTEM_ROLE_CONFIG_DEFAULTS.map((config) => ({
|
||||||
|
...config,
|
||||||
|
defaultPermissions: ROLE_DEFAULT_PERMISSIONS[config.role],
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SystemRole } from "./enums.js";
|
import { SystemRole } from "./enums.js";
|
||||||
|
|
||||||
export const PermissionKey = {
|
export const PermissionKey = {
|
||||||
|
VIEW_PLANNING: "viewPlanning",
|
||||||
VIEW_COSTS: "viewCosts",
|
VIEW_COSTS: "viewCosts",
|
||||||
USE_ASSISTANT_ADVANCED_TOOLS: "useAssistantAdvancedTools",
|
USE_ASSISTANT_ADVANCED_TOOLS: "useAssistantAdvancedTools",
|
||||||
EXPORT_DATA: "exportData",
|
EXPORT_DATA: "exportData",
|
||||||
@@ -28,6 +29,7 @@ export interface PermissionOverrides {
|
|||||||
export const ROLE_DEFAULT_PERMISSIONS: Record<SystemRole, PermissionKey[]> = {
|
export const ROLE_DEFAULT_PERMISSIONS: Record<SystemRole, PermissionKey[]> = {
|
||||||
ADMIN: Object.values(PermissionKey),
|
ADMIN: Object.values(PermissionKey),
|
||||||
MANAGER: [
|
MANAGER: [
|
||||||
|
PermissionKey.VIEW_PLANNING,
|
||||||
PermissionKey.VIEW_COSTS,
|
PermissionKey.VIEW_COSTS,
|
||||||
PermissionKey.EXPORT_DATA,
|
PermissionKey.EXPORT_DATA,
|
||||||
PermissionKey.IMPORT_DATA,
|
PermissionKey.IMPORT_DATA,
|
||||||
@@ -40,6 +42,7 @@ export const ROLE_DEFAULT_PERMISSIONS: Record<SystemRole, PermissionKey[]> = {
|
|||||||
PermissionKey.VIEW_SCORES,
|
PermissionKey.VIEW_SCORES,
|
||||||
],
|
],
|
||||||
CONTROLLER: [
|
CONTROLLER: [
|
||||||
|
PermissionKey.VIEW_PLANNING,
|
||||||
PermissionKey.VIEW_COSTS,
|
PermissionKey.VIEW_COSTS,
|
||||||
PermissionKey.EXPORT_DATA,
|
PermissionKey.EXPORT_DATA,
|
||||||
PermissionKey.VIEW_ALL_RESOURCES,
|
PermissionKey.VIEW_ALL_RESOURCES,
|
||||||
|
|||||||
Reference in New Issue
Block a user