fix(freight:backoffice): remove coarse fleet.view/fleet.manage/admin fallbacks

Now that every fleet-resource page and settings page has its own
dedicated permission key (previous commit), the broad fallbacks are
redundant and over-grant: anyone holding only fleet:view/fleet:manage
or admin could reach every page in that whole section, not just one.

Removed fleet.view fallback from: Routes, Locomotives, Train Builder,
Wagons, Containers, Cargoes, Compliance & Alerts, Procurement, and the
Overview dashboard's Fleet KPI tab.

Removed fleet.manage fallback from: canFleetAction() (per-resource
fleet CRUD, lib/permissions.ts) and TrainBuilderDetailPage's wagon-
assignment check. Hard-delete already had no such fallback.

Removed admin fallback from: File settings, Dropdown settings,
Contract templates, Portal content, Trade access, Exchange rate.

Left untouched: Incidents (sole gate is fleet.view — no dedicated
edr_freight_app:incidents:* key exists on the backend yet, so there's
nothing to fall back FROM; removing it would make the page
super-admin-only).

Access-narrowing change: anyone currently relying on the coarse grant
without also holding the specific resource/settings key will lose
access to these pages until roles are updated to grant the specific
keys directly. Audit role assignments before this deploys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nathnael
2026-08-08 13:59:06 +00:00
parent a8591c0071
commit d337fa0d85
5 changed files with 36 additions and 99 deletions

View File

@@ -573,9 +573,7 @@ const App = () => {
<Route <Route
path="routes" path="routes"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.routes.view}>
permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}
>
<RoutesPage /> <RoutesPage />
</RequirePermission> </RequirePermission>
} }
@@ -583,12 +581,7 @@ const App = () => {
<Route <Route
path="locomotives" path="locomotives"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.locomotives.view}>
permission={[
FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.fleet.view,
]}
>
<FleetResourcePage /> <FleetResourcePage />
</RequirePermission> </RequirePermission>
} }
@@ -596,9 +589,7 @@ const App = () => {
<Route <Route
path="trains" path="trains"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.trains.view}>
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<FleetResourcePage /> <FleetResourcePage />
</RequirePermission> </RequirePermission>
} }
@@ -606,9 +597,7 @@ const App = () => {
<Route <Route
path="trains/:id" path="trains/:id"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.trains.view}>
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<TrainDetailPage /> <TrainDetailPage />
</RequirePermission> </RequirePermission>
} }
@@ -616,9 +605,7 @@ const App = () => {
<Route <Route
path="train-builder" path="train-builder"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.trains.view}>
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<TrainBuilderListPage /> <TrainBuilderListPage />
</RequirePermission> </RequirePermission>
} }
@@ -626,9 +613,7 @@ const App = () => {
<Route <Route
path="train-builder/:id" path="train-builder/:id"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.trains.view}>
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<TrainBuilderDetailPage /> <TrainBuilderDetailPage />
</RequirePermission> </RequirePermission>
} }
@@ -636,9 +621,7 @@ const App = () => {
<Route <Route
path="wagons" path="wagons"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.wagons.view}>
permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}
>
<FleetResourcePage /> <FleetResourcePage />
</RequirePermission> </RequirePermission>
} }
@@ -659,12 +642,7 @@ const App = () => {
<Route <Route
path="containers" path="containers"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.containers.view}>
permission={[
FREIGHT_PERMS.containers.view,
FREIGHT_PERMS.fleet.view,
]}
>
<FleetResourcePage /> <FleetResourcePage />
</RequirePermission> </RequirePermission>
} }
@@ -672,12 +650,7 @@ const App = () => {
<Route <Route
path="cargoes" path="cargoes"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.cargoes.view}>
permission={[
FREIGHT_PERMS.cargoes.view,
FREIGHT_PERMS.fleet.view,
]}
>
<FleetResourcePage /> <FleetResourcePage />
</RequirePermission> </RequirePermission>
} }
@@ -765,9 +738,7 @@ const App = () => {
<Route <Route
path="compliance" path="compliance"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.compliance.view}>
permission={[FREIGHT_PERMS.compliance.view, FREIGHT_PERMS.fleet.view]}
>
<CompliancePage /> <CompliancePage />
</RequirePermission> </RequirePermission>
} }
@@ -791,9 +762,7 @@ const App = () => {
<Route <Route
path="procurement" path="procurement"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.procurement.view}>
permission={[FREIGHT_PERMS.procurement.view, FREIGHT_PERMS.fleet.view]}
>
<ProcurementPage /> <ProcurementPage />
</RequirePermission> </RequirePermission>
} }
@@ -816,9 +785,7 @@ const App = () => {
<Route <Route
path="file-settings" path="file-settings"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.settings.fileUpload.view}>
permission={[FREIGHT_PERMS.settings.fileUpload.view, FREIGHT_PERMS.admin]}
>
<FileUploadSettingsPage /> <FileUploadSettingsPage />
</RequirePermission> </RequirePermission>
} }
@@ -826,9 +793,7 @@ const App = () => {
<Route <Route
path="dropdown-settings" path="dropdown-settings"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.settings.dropdown.view}>
permission={[FREIGHT_PERMS.settings.dropdown.view, FREIGHT_PERMS.admin]}
>
<DropdownSettingsPage /> <DropdownSettingsPage />
</RequirePermission> </RequirePermission>
} }
@@ -845,10 +810,7 @@ const App = () => {
path="contract-templates" path="contract-templates"
element={ element={
<RequirePermission <RequirePermission
permission={[ permission={FREIGHT_PERMS.settings.contractTemplates.view}
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
]}
> >
<ContractTemplatesPage /> <ContractTemplatesPage />
</RequirePermission> </RequirePermission>
@@ -858,10 +820,7 @@ const App = () => {
path="contract-templates/:code" path="contract-templates/:code"
element={ element={
<RequirePermission <RequirePermission
permission={[ permission={FREIGHT_PERMS.settings.contractTemplates.view}
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
]}
> >
<ContractTemplateEditorPage /> <ContractTemplateEditorPage />
</RequirePermission> </RequirePermission>
@@ -874,7 +833,6 @@ const App = () => {
permission={[ permission={[
FREIGHT_PERMS.settings.supportContent.view, FREIGHT_PERMS.settings.supportContent.view,
FREIGHT_PERMS.settings.supportContent.manage, FREIGHT_PERMS.settings.supportContent.manage,
FREIGHT_PERMS.admin,
]} ]}
> >
<PortalContentPage /> <PortalContentPage />
@@ -899,9 +857,7 @@ const App = () => {
<Route <Route
path="configuration/trade-access" path="configuration/trade-access"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.tradeAccess.view}>
permission={[FREIGHT_PERMS.tradeAccess.view, FREIGHT_PERMS.admin]}
>
<TradeAccessPage /> <TradeAccessPage />
</RequirePermission> </RequirePermission>
} }
@@ -909,9 +865,7 @@ const App = () => {
<Route <Route
path="configuration/exchange-rate" path="configuration/exchange-rate"
element={ element={
<RequirePermission <RequirePermission permission={FREIGHT_PERMS.settings.exchangeRate.view}>
permission={[FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin]}
>
<div className="p-4"> <div className="p-4">
<ExchangeRateSettingsCard /> <ExchangeRateSettingsCard />
</div> </div>

View File

@@ -196,22 +196,19 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Routes", label: "Routes",
href: "/dashboard/routes", href: "/dashboard/routes",
icon: <Network />, icon: <Network />,
permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view], permission: FREIGHT_PERMS.routes.view,
}, },
{ {
label: "Locomotives", label: "Locomotives",
href: "/dashboard/locomotives", href: "/dashboard/locomotives",
icon: <Train />, icon: <Train />,
permission: [ permission: FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.fleet.view,
],
}, },
{ {
label: "Train Builder", label: "Train Builder",
href: "/dashboard/train-builder", href: "/dashboard/train-builder",
icon: <Hammer />, icon: <Hammer />,
permission: [FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view], permission: FREIGHT_PERMS.trains.view,
}, },
// { // {
@@ -223,7 +220,7 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Wagons", label: "Wagons",
href: "/dashboard/wagons", href: "/dashboard/wagons",
icon: <Truck />, icon: <Truck />,
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view], permission: FREIGHT_PERMS.wagons.view,
}, },
{ {
label: "Wagon Transfers", label: "Wagon Transfers",
@@ -280,21 +277,23 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Compliance & Alerts", label: "Compliance & Alerts",
href: "/dashboard/compliance", href: "/dashboard/compliance",
icon: <ShieldCheck />, icon: <ShieldCheck />,
permission: [FREIGHT_PERMS.compliance.view, FREIGHT_PERMS.fleet.view], permission: FREIGHT_PERMS.compliance.view,
}, },
{ {
label: "Incidents", label: "Incidents",
href: "/dashboard/incidents", href: "/dashboard/incidents",
icon: <FileText />, icon: <FileText />,
// No dedicated backend key exists for incidents yet — stuck on the // No dedicated backend key exists for incidents yet. Not part of
// blanket fleet:view fallback until one is added. // the fleet.view/admin fallback cleanup — removing fleet.view
// here with nothing to replace it would lock the page to
// super-admin only, so it stays as the sole (if coarse) gate.
permission: FREIGHT_PERMS.fleet.view, permission: FREIGHT_PERMS.fleet.view,
}, },
{ {
label: "Procurement", label: "Procurement",
href: "/dashboard/procurement", href: "/dashboard/procurement",
icon: <Package />, icon: <Package />,
permission: [FREIGHT_PERMS.procurement.view, FREIGHT_PERMS.fleet.view], permission: FREIGHT_PERMS.procurement.view,
}, },
{ {
label: "Financial Reports", label: "Financial Reports",
@@ -479,23 +478,20 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "File settings", label: "File settings",
href: "/dashboard/file-settings", href: "/dashboard/file-settings",
icon: <Paperclip />, icon: <Paperclip />,
permission: [FREIGHT_PERMS.settings.fileUpload.view, FREIGHT_PERMS.admin], permission: FREIGHT_PERMS.settings.fileUpload.view,
}, },
{ {
label: "Dropdown settings", label: "Dropdown settings",
href: "/dashboard/dropdown-settings", href: "/dashboard/dropdown-settings",
icon: <Settings />, icon: <Settings />,
permission: [FREIGHT_PERMS.settings.dropdown.view, FREIGHT_PERMS.admin], permission: FREIGHT_PERMS.settings.dropdown.view,
}, },
{ {
label: "Contract templates", label: "Contract templates",
href: "/dashboard/contract-templates", href: "/dashboard/contract-templates",
icon: <ScrollText />, icon: <ScrollText />,
// `view` opens the page; `read` alone is API-only and shows no menu. // `view` opens the page; `read` alone is API-only and shows no menu.
permission: [ permission: FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
],
}, },
{ {
label: "Portal content", label: "Portal content",
@@ -504,7 +500,6 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
permission: [ permission: [
FREIGHT_PERMS.settings.supportContent.view, FREIGHT_PERMS.settings.supportContent.view,
FREIGHT_PERMS.settings.supportContent.manage, FREIGHT_PERMS.settings.supportContent.manage,
FREIGHT_PERMS.admin,
], ],
}, },
{ {
@@ -527,12 +522,12 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
{ {
label: "Trade access", label: "Trade access",
href: "/dashboard/configuration/trade-access", href: "/dashboard/configuration/trade-access",
permission: [FREIGHT_PERMS.tradeAccess.view, FREIGHT_PERMS.admin], permission: FREIGHT_PERMS.tradeAccess.view,
}, },
{ {
label: "Exchange rate", label: "Exchange rate",
href: "/dashboard/configuration/exchange-rate", href: "/dashboard/configuration/exchange-rate",
permission: [FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin], permission: FREIGHT_PERMS.settings.exchangeRate.view,
}, },
], ],
}, },

View File

@@ -658,19 +658,13 @@ export type FleetCrudResource =
| "vehicles" | "vehicles"
| "drivers"; | "drivers";
/** /** Per-resource fleet CRUD check — each resource needs its own grant. */
* Per-resource fleet CRUD check. The legacy coarse fleet:manage key still
* grants every action (mirrors the API's one-of guard fallback).
*/
export function canFleetAction( export function canFleetAction(
user: AuthUser | null | undefined, user: AuthUser | null | undefined,
resource: FleetCrudResource, resource: FleetCrudResource,
action: "create" | "update" | "delete", action: "create" | "update" | "delete",
): boolean { ): boolean {
return ( return hasPermission(user, FREIGHT_PERMS[resource][action]);
hasPermission(user, FREIGHT_PERMS[resource][action]) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage)
);
} }
/** /**

View File

@@ -87,11 +87,7 @@ const TAB_ITEMS: Array<{
icon: TrainFront, icon: TrainFront,
kpiKey: "operations", kpiKey: "operations",
metricKey: "wagonsAvailable", metricKey: "wagonsAvailable",
permission: [ permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.trainScheduling.view],
FREIGHT_PERMS.fleet.view,
FREIGHT_PERMS.wagons.view,
FREIGHT_PERMS.trainScheduling.view,
],
}, },
{ {
value: "customers", value: "customers",

View File

@@ -85,9 +85,7 @@ export default function TrainBuilderDetailPage() {
const { user } = useAuth(); const { user } = useAuth();
const canUpdate = canFleetAction(user, "trains", "update"); const canUpdate = canFleetAction(user, "trains", "update");
const canDelete = canFleetAction(user, "trains", "delete"); const canDelete = canFleetAction(user, "trains", "delete");
const canAssign = const canAssign = hasPermission(user, FREIGHT_PERMS.trains.assignWagons);
hasPermission(user, FREIGHT_PERMS.trains.assignWagons) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage);
const compositionQuery = useQuery( const compositionQuery = useQuery(
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }), api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),