14 KiB
14 KiB
Route Access Matrix
Date: 2026-03-30
Purpose: Make high-sensitivity API audiences explicit and reduce ambiguous protectedProcedure usage on broad read routes.
Audience Classes
self-service: authenticated users can only read or mutate data that belongs to their linked resource or accountauthenticated-safe-lookup: authenticated users can access a deliberately narrow, identity-safe lookup surfaceresource-overview: users withviewAllResourcesormanageResourcesplanning-read: users withviewPlanningcontroller-finance: controller, manager, or admin throughcontrollerProceduremanager-write: manager or admin throughmanagerProcedureadmin-only: admin throughadminProcedure
Current Classification
packages/api/src/router/resource.ts
getMyResource:self-servicegetById,getByEid,getHoverCard,getByIdentifier,getByIdentifierDetail,resolveByIdentifier,getChargeabilitySummary:self-serviceunless the caller also hasresource-overviewdirectory:authenticated-safe-lookupchapters:authenticated-safe-lookuplistSummaries,listSummariesDetail,listStaff,resolveResponsiblePersonName:resource-overviewgetSkillsAnalytics,searchBySkills,listWithUtilization,getChargeabilityStats,getSkillMarketplace:controller-financeimportSkillMatrix:self-service- create, update, deactivate, batch update, imports for other users:
manager-writeoradmin-only
packages/api/src/router/project.ts
resolveByIdentifier,searchSummaries,getByIdentifier:planning-readsearchSummariesDetail,list,getById,getByIdentifierDetail,getShoringRatio,listWithCosts:controller-finance- create, update, status changes, cover mutations:
manager-write - delete and batch delete:
admin-only isImageGenConfigured,isDalleConfigured: authenticated low-risk configuration checks
packages/api/src/router/timeline.ts
getMyEntriesView,getMyHolidayOverlays:self-service- timeline-wide planning reads and shift previews:
controller-finance - allocation updates, quick-assign, project shifts:
manager-write
packages/api/src/router/allocation.ts
list,listView,listDemands,listAssignments,getAssignmentById,resolveAssignment,getDemandRequirementById,checkResourceAvailability,getResourceAvailabilityView,getResourceAvailabilitySummary:planning-read- mutations already sit behind
manager-write
packages/api/src/router/dashboard.ts
- all current routes are
controller-finance
packages/api/src/router/role.ts
resolveByIdentifier:authenticated-safe-lookuplist,getByIdentifier,getById:planning-read- create, update, delete:
manager-write
Reasoning:
resolveByIdentifierreturns a narrow lookup shape without planning countslist,getByIdentifier, andgetByIdattach planning-linked usage counts, so they must not remain broadprotectedProcedurereads
packages/api/src/router/scenario.ts
getProjectBaseline:planning-readplus explicitviewCosts
Reasoning:
- the route combines staffing baseline data with commercial totals, so both planning and cost audiences are required
packages/api/src/router/estimate.ts
list:controller-finance- drafting, versioning, export generation, and approval writes:
manager-write
packages/api/src/router/comment.ts
list,listMentionCandidates,count,create,resolve,delete:entity-scoped
Reasoning:
- comments must inherit the audience of the backing entity, not the comment row itself
- supported entity types are currently
estimateandresource - estimate comments inherit the estimate workspace audience: controller, manager, or admin
- resource comments inherit the resource detail audience: self-service for the caller's own linked resource, plus broad access for users who already have resource overview visibility
- mention autocomplete uses the same entity-scoped access check instead of reusing assignment-oriented user directory routes
- the registry keeps router policy, assistant metadata, and web comment targets on the same supported-entity definition
- future entity types must be added through an explicit registry with per-entity access checks, assistant parity, and router tests in the same slice
packages/api/src/router/system-role-config.ts
- all reads and writes:
admin-only
Reasoning:
- system role defaults define the effective permission model and therefore belong to the smallest operational audience
packages/api/src/router/settings.ts
getSystemSettings,updateSystemSettings, connection tests,getAiConfigured:admin-only
Reasoning:
- even the boolean AI readiness check leaks whether admin-managed infrastructure is wired and available
- the route has no current web consumer outside admin operations, so narrowing it does not block normal user workflows
packages/api/src/router/country.ts
list,resolveByIdentifier,getCityById:authenticated-safe-lookupgetByIdentifier,getById:resource-overview- create, update, metro-city writes:
admin-only
Reasoning:
- minimal country lookups are needed broadly for forms, filters, and location resolution
- detailed country reads include metro-city detail plus
_count.resources, so they should align with broad people-directory visibility
packages/api/src/router/org-unit.ts
resolveByIdentifier:authenticated-safe-lookuplist,getTree,getByIdentifier,getById:resource-overview- create, update, deactivate:
admin-only
Reasoning:
resolveByIdentifierstays narrow enough for low-risk lookup flowslistand especiallygetTreeexpose the internal org hierarchy, parent links, sort order, and structure metadata, so they should not remain broad authenticated reads- detailed org-unit reads also expose
_count.resourcesand parent/child context that maps the staffing structure
packages/api/src/router/client.ts
resolveByIdentifier:authenticated-safe-lookuplist,getTree,getByIdentifier,getById:planning-read- create and update:
manager-write - delete:
admin-only
Reasoning:
resolveByIdentifierreturns a deliberately narrow lookup shape for code/name resolutionlistalready exposes_count.childrenand_count.projects, andgetTreereveals the full client hierarchy used in planning and reporting flows- detailed client reads add parent/child structure plus project counts, so they should align with the explicit planning audience instead of broad authenticated access
packages/api/src/router/utilization-category.ts
list,getById:planning-read- create and update:
admin-only
Reasoning:
- the categories feed project configuration and planning/reporting workflows instead of broad self-service screens
getByIdincludes_count.projects, so the detailed read should not remain a generic authenticated route
packages/api/src/router/management-level.ts
listGroups,getGroupById:planning-read- create, update, delete:
admin-only
Reasoning:
- management-level groups carry chargeability targets and resource-linked counts that feed planning and reporting workflows, so they should not stay on broad authenticated reads
- the list is consumed by resource editing, reporting filters, and admin configuration, which all fit the explicit planning audience better than generic
protectedProcedure
packages/api/src/router/blueprint.ts
listSummaries,list,getById,getByIdentifier,getGlobalFieldDefs:planning-readresolveByIdentifier:authenticated-safe-lookup- create, update, delete, global-flag writes:
admin-only
Reasoning:
listSummariesexposes_count.projects, so the assistant-facing summary list should not remain a broad authenticated readresolveByIdentifieralready returns a narrow lookup shape suitable for low-risk name/id resolution- the broader blueprint reads expose full template configuration such as field definitions, defaults, and validation rules that belong to planning workflows rather than generic authenticated access
getGlobalFieldDefsaggregates active global field definitions across blueprints, so it belongs with the same planning configuration audience rather than a broad authenticated read
packages/api/src/router/holiday-calendar.ts
listCalendars,listCalendarsDetail,getCalendarByIdentifier,getCalendarByIdentifierDetail,getCalendarById:admin-only- create, update, delete calendar and entry mutations:
admin-only previewResolvedHolidays,previewResolvedHolidaysDetail,resolveHolidays,resolveHolidaysDetail:authenticated-safe-lookupresolveResourceHolidays,resolveResourceHolidaysDetail:self-servicefor the caller's own resource, with elevated cross-resource reads for manager and admin roles
Reasoning:
- the calendar catalog is currently consumed in the web app only by the admin vacation editor, so broad authenticated reads expose internal configuration without a product need
- region-based resolution helpers remain a narrow lookup surface because callers provide the location context directly instead of enumerating internal resource data
- resource-based holiday resolution derives sensitive location context from a specific employee record, so it must follow the same self-service ownership model as other resource-scoped absence reads
packages/api/src/router/entitlement.ts
getBalance,getBalanceDetail:self-servicefor the caller's own resource, with elevated cross-resource reads for controller, manager, and admin rolesget,set,getYearSummary,getYearSummaryDetail:manager-writebulkSet:admin-only
Reasoning:
- regular users can inspect only their own holiday-aware balance, and the route enforces that by checking resource ownership before loading entitlement data
- cross-resource balance reads and year summaries are operational planning and approval workflows, so they stay with controller/manager/admin audiences rather than broad authenticated access
- bulk entitlement changes affect many users at once and should remain restricted to the smallest administrative audience
packages/api/src/router/vacation.ts
previewRequest,list,getById,getForResource,getTeamOverlap,getTeamOverlapDetail,cancel:self-servicefor the caller's own resource, with elevated cross-resource reads for manager and admin rolescreate:self-servicefor the caller's own resource, with elevated creation for manager and admin rolesapprove,reject,getPendingApprovals,updateStatusapproval paths:manager-writebatchCreatePublicHolidays:admin-only
Reasoning:
- the employee-facing vacation flows are valid self-service features, but they must not reveal holiday context, overlap data, or request details for arbitrary resources
- manager and admin roles already handle approval and operational cross-resource workflows, so they retain broader access where the route logic explicitly allows it
- bulk public-holiday generation changes organization-wide absence data and therefore belongs to the smallest administrative audience
packages/api/src/router/notification.ts
list,unreadCount,markRead, task detail/status routes, reminder routes, anddelete:self-servicecreate,createBroadcast,listBroadcasts,getBroadcastById,createTask,assignTask:manager-write
Reasoning:
- the self-service surface is already constrained to the caller's own notifications, reminders, tasks, or assignee visibility
- broadcast and task-assignment flows can affect other users and organization-wide messaging, so they must stay on explicit manager-or-admin procedures
packages/api/src/router/user.ts
me, dashboard layout/preferences, favorites, MFA setup/status:self-servicelistAssignable:manager-writelist,activeCount, create/update role and permissions, resource linking,getEffectivePermissions,disableTotp:admin-onlyverifyTotp:publicfor the login flow
Reasoning:
- self-service user routes only expose or mutate the authenticated account's own preferences and MFA state
listAssignableis an operational lookup for delegation and assignment flows, which fits manager and admin audiences- user administration and effective-permission inspection expose high-sensitivity identity and authorization state and therefore should remain admin-only
packages/api/src/router/assistant.ts
listPendingApprovals:self-servicechat: authenticated shell with tool-level audience enforcement
Reasoning:
listPendingApprovalsreads pending approvals byctx.dbUser.id, so it is a self-service view of the caller's own approval queuechatrequires authentication, but the effective data audience is enforced by assistant tool selection and backing router permissions rather than by a single broad router audience on the chat endpoint itself
Assistant Parity Rule
- assistant tool visibility must never widen the audience of the backing router
- router audience is the source of truth; assistant gating mirrors it
- when a route becomes narrower, update assistant visibility in the same hardening slice
search_resourcesmust followresourceOverviewProcedure, not broad authenticated accesssearch_by_skillmust followcontrollerProcedure, not broad authenticated or planning-only access- if
assistant-tools.tsalready has unrelated local edits, prefer updatingpackages/api/src/router/assistant.tsand parity tests first instead of mixing concerns into the tool implementation file
Rollout Discipline
For audience-scoping changes, use this order:
- narrow the backing router procedure first
- add or tighten authorization tests on the router
- align assistant visibility in
packages/api/src/router/assistant.ts - update assistant parity tests
- ship in small isolated commits so regressions can be reverted without undoing unrelated hardening
Immediate Follow-Ups
- monitor whether
viewPlanningshould later split into narrower project-read vs allocation-read audiences - split
allocationfurther 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