mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) }),
|
||||||
|
|||||||
Reference in New Issue
Block a user