From da08a9b0859da246d7c609cf44f467e73498291c Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 7 Aug 2026 08:42:53 +0000 Subject: [PATCH] fix(auth): list every route key on the class-level guard Nest runs class and method guards together, so a class gate naming only the view key silently required view AND action. Staff granted just an action were denied before their key was checked. Each class gate now names every key its routes use, and FleetView accepts an array so the fleet controllers keep their coarse fallback. Drops the one-off grant mapping SQL with it: already applied to dev, and this fix removes the companion-view rule that was its recurring part. --- .../src/common/booking-guards.ts | 9 +- .../src/modules/billing/billing.controller.ts | 7 +- .../src/modules/cargoes/cargoes.controller.ts | 9 +- .../compliance/compliance.controller.ts | 7 +- .../consignments/consignments.controller.ts | 7 +- .../containers.controller.ts | 9 +- .../src/modules/drivers/drivers.controller.ts | 9 +- .../facilities/facilities.controller.ts | 7 +- .../gps-tracking/gps-tracking.controller.ts | 7 +- .../modules/incidents/incidents.controller.ts | 9 +- .../interchange-documents.controller.ts | 9 +- .../procurement/procurement.controller.ts | 9 +- .../src/modules/routes/routes.controller.ts | 10 +- .../trains/train-builder.controller.ts | 10 +- .../src/modules/trains/trains.controller.ts | 9 +- .../modules/vehicles/vehicles.controller.ts | 9 +- .../warehouse-inspection.controller.ts | 4 + .../warehouses/warehouse-zones.controller.ts | 8 +- deploy/position-type-grant-mapping.sql | 119 ------------------ 19 files changed, 130 insertions(+), 137 deletions(-) delete mode 100644 deploy/position-type-grant-mapping.sql diff --git a/apps/edr-freight-api/src/common/booking-guards.ts b/apps/edr-freight-api/src/common/booking-guards.ts index 49bd31c61..d1b5364c3 100644 --- a/apps/edr-freight-api/src/common/booking-guards.ts +++ b/apps/edr-freight-api/src/common/booking-guards.ts @@ -95,9 +95,14 @@ export const TrainSchedulingRulesManage = () => * wagons:delete, …). The legacy coarse fleet:view / fleet:manage keys remain * valid as a one-of fallback so existing role grants keep working. */ -export const FleetView = (granular?: string) => +export const FleetView = (granular?: string | string[]) => BookingStaff( - granular ? [granular, FREIGHT_PERMS.fleet.view] : FREIGHT_PERMS.fleet.view, + granular + ? [ + ...(Array.isArray(granular) ? granular : [granular]), + FREIGHT_PERMS.fleet.view, + ] + : FREIGHT_PERMS.fleet.view, ); export const FleetManage = (granular?: string) => diff --git a/apps/edr-freight-api/src/modules/billing/billing.controller.ts b/apps/edr-freight-api/src/modules/billing/billing.controller.ts index d72528310..ea43c9c74 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.controller.ts @@ -20,7 +20,12 @@ import { FilterInvoiceDto } from "./dto/filter-invoice.dto"; @ApiTags("billing") @Controller("billing") -@BookingStaff(FREIGHT_PERMS.invoices.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.invoices.view, + FREIGHT_PERMS.invoices.export, +]) @ApiBearerAuth() export class BillingController { constructor( diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts index ac3adb7e8..09c9b677a 100644 --- a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts @@ -20,7 +20,14 @@ import { CargoesService } from './cargoes.service'; @ApiTags('cargoes') @Controller('cargoes') -@FleetView(FREIGHT_PERMS.cargoes.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.cargoes.view, + FREIGHT_PERMS.cargoes.create, + FREIGHT_PERMS.cargoes.update, + FREIGHT_PERMS.cargoes.delete, +]) export class CargoesController { constructor(private readonly cargoesService: CargoesService) {} diff --git a/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts b/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts index 0e5949300..25d010253 100644 --- a/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts +++ b/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts @@ -11,7 +11,12 @@ import { ComplianceType } from './entities/compliance-record.entity'; @ApiTags('Vehicle Compliance') @Controller('compliance') -@BookingStaff(FREIGHT_PERMS.compliance.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.compliance.view, + FREIGHT_PERMS.compliance.manage, +]) export class ComplianceController { constructor(private readonly complianceService: ComplianceService) {} diff --git a/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts index 579b9ee26..a9aaa73e3 100644 --- a/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts +++ b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts @@ -17,7 +17,12 @@ import { FilterConsignmentDto } from "./dto/filter-consignment.dto"; @ApiTags("consignments") @Controller("consignments") -@FleetView(FREIGHT_PERMS.consignments.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.consignments.view, + FREIGHT_PERMS.consignments.create, +]) export class ConsignmentsController { constructor(private readonly consignmentsService: ConsignmentsService) {} diff --git a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts index 0a5e6bb0f..a209e2609 100644 --- a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts +++ b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts @@ -19,7 +19,14 @@ import { ContainersService } from './containers.service'; @ApiTags('containers') @Controller('containers') -@FleetView(FREIGHT_PERMS.containers.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.containers.view, + FREIGHT_PERMS.containers.create, + FREIGHT_PERMS.containers.update, + FREIGHT_PERMS.containers.delete, +]) export class ContainersController { constructor(private readonly containersService: ContainersService) {} diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts b/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts index e5b6ae146..8d2be55df 100644 --- a/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts +++ b/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts @@ -24,7 +24,14 @@ import { FleetHistoryService } from '../fleet-history/fleet-history.service'; @ApiTags('drivers') @ApiBearerAuth() @Controller('drivers') -@BookingStaff(FREIGHT_PERMS.drivers.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.drivers.view, + FREIGHT_PERMS.drivers.create, + FREIGHT_PERMS.drivers.update, + FREIGHT_PERMS.drivers.delete, +]) export class DriversController { constructor( private readonly driversService: DriversService, diff --git a/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts b/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts index 451c4d03b..7db35d432 100644 --- a/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts +++ b/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts @@ -10,7 +10,12 @@ import { FacilitiesService } from './facilities.service'; @ApiTags('Facilities') @Controller('facilities') -@BookingStaff(FREIGHT_PERMS.facilities.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.facilities.view, + FREIGHT_PERMS.facilities.manage, +]) export class FacilitiesController { constructor(private readonly facilitiesService: FacilitiesService) {} diff --git a/apps/edr-freight-api/src/modules/gps-tracking/gps-tracking.controller.ts b/apps/edr-freight-api/src/modules/gps-tracking/gps-tracking.controller.ts index 8bd4a31d8..d0cc2d1b4 100644 --- a/apps/edr-freight-api/src/modules/gps-tracking/gps-tracking.controller.ts +++ b/apps/edr-freight-api/src/modules/gps-tracking/gps-tracking.controller.ts @@ -19,7 +19,12 @@ import { RegisterDeviceDto, UpdateDeviceDto } from './dto/gps-device.dto'; @ApiTags('gps-tracking') @ApiBearerAuth() @Controller('gps') -@BookingStaff(FREIGHT_PERMS.tracking.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.tracking.view, + FREIGHT_PERMS.tracking.manage, +]) export class GpsTrackingController { constructor(private readonly gps: GpsTrackingService) {} diff --git a/apps/edr-freight-api/src/modules/incidents/incidents.controller.ts b/apps/edr-freight-api/src/modules/incidents/incidents.controller.ts index b9cebd653..2e9dfb169 100644 --- a/apps/edr-freight-api/src/modules/incidents/incidents.controller.ts +++ b/apps/edr-freight-api/src/modules/incidents/incidents.controller.ts @@ -22,7 +22,14 @@ import { IncidentStatus, IncidentType } from './entities/incident.entity'; // No incidents-specific permission exists in the registry, so this reuses the // (real) drivers.* fleet-road keys — incident records are driver-safety data // (driver stats / incident history). TODO: add a dedicated incidents:* key. -@BookingStaff(FREIGHT_PERMS.drivers.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.drivers.view, + FREIGHT_PERMS.drivers.create, + FREIGHT_PERMS.drivers.update, + FREIGHT_PERMS.drivers.delete, +]) export class IncidentsController { constructor(private readonly incidentsService: IncidentsService) {} diff --git a/apps/edr-freight-api/src/modules/interchange-documents/interchange-documents.controller.ts b/apps/edr-freight-api/src/modules/interchange-documents/interchange-documents.controller.ts index 61a13744e..b5e92ca39 100644 --- a/apps/edr-freight-api/src/modules/interchange-documents/interchange-documents.controller.ts +++ b/apps/edr-freight-api/src/modules/interchange-documents/interchange-documents.controller.ts @@ -15,7 +15,14 @@ import { InterchangeDocumentsService } from './interchange-documents.service'; @ApiBearerAuth() @Controller('interchange-documents') // Class-level view guard; each write route adds its own manage permission below. -@BookingStaff(FREIGHT_PERMS.interchangeDocuments.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.interchangeDocuments.view, + FREIGHT_PERMS.interchangeDocuments.generate, + FREIGHT_PERMS.interchangeDocuments.acknowledge, + FREIGHT_PERMS.interchangeDocuments.dispute, +]) export class InterchangeDocumentsController { constructor(private readonly service: InterchangeDocumentsService) {} diff --git a/apps/edr-freight-api/src/modules/procurement/procurement.controller.ts b/apps/edr-freight-api/src/modules/procurement/procurement.controller.ts index 374b2bd53..e96b9dc74 100644 --- a/apps/edr-freight-api/src/modules/procurement/procurement.controller.ts +++ b/apps/edr-freight-api/src/modules/procurement/procurement.controller.ts @@ -13,7 +13,14 @@ import { @ApiTags('Procurement & Asset Lifecycle') @Controller('procurement') -@BookingStaff(FREIGHT_PERMS.procurement.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.procurement.view, + FREIGHT_PERMS.procurement.vendorManage, + FREIGHT_PERMS.procurement.acquisitionManage, + FREIGHT_PERMS.procurement.disposalManage, +]) export class ProcurementController { constructor(private readonly procurementService: ProcurementService) {} diff --git a/apps/edr-freight-api/src/modules/routes/routes.controller.ts b/apps/edr-freight-api/src/modules/routes/routes.controller.ts index ed61f08b0..96812275d 100644 --- a/apps/edr-freight-api/src/modules/routes/routes.controller.ts +++ b/apps/edr-freight-api/src/modules/routes/routes.controller.ts @@ -27,7 +27,15 @@ import { RoutesService } from './routes.service'; @ApiTags('routes') @ApiBearerAuth() @Controller('routes') -@FleetView(FREIGHT_PERMS.routes.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.routes.view, + FREIGHT_PERMS.routes.create, + FREIGHT_PERMS.routes.update, + FREIGHT_PERMS.routes.hardDelete, + FREIGHT_PERMS.routes.delete, +]) export class RoutesController { constructor(private readonly routesService: RoutesService) {} diff --git a/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts b/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts index a5cd1ff5c..834663170 100644 --- a/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts +++ b/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts @@ -31,7 +31,15 @@ import { TrainBuilderService } from './train-builder.service'; @ApiTags('train-builder') @ApiBearerAuth() @Controller('train-builder') -@FleetView(FREIGHT_PERMS.trains.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.trains.view, + FREIGHT_PERMS.trains.create, + FREIGHT_PERMS.trains.update, + FREIGHT_PERMS.trains.assignWagons, + FREIGHT_PERMS.trains.delete, +]) export class TrainBuilderController { constructor(private readonly trainBuilderService: TrainBuilderService) {} diff --git a/apps/edr-freight-api/src/modules/trains/trains.controller.ts b/apps/edr-freight-api/src/modules/trains/trains.controller.ts index 173a738fa..ede540ce2 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.controller.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.controller.ts @@ -19,7 +19,14 @@ import { TrainsService } from "./trains.service"; @ApiTags("trains") @Controller("trains") -@FleetView(FREIGHT_PERMS.trains.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@FleetView([ + FREIGHT_PERMS.trains.view, + FREIGHT_PERMS.trains.create, + FREIGHT_PERMS.trains.update, + FREIGHT_PERMS.trains.delete, +]) export class TrainsController { constructor(private readonly trainsService: TrainsService) {} diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts index 737574dd9..d9e2ba224 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts @@ -20,7 +20,14 @@ import { FleetHistoryService } from '../fleet-history/fleet-history.service'; @ApiTags('vehicles') @ApiBearerAuth() @Controller('vehicles') -@BookingStaff(FREIGHT_PERMS.vehicles.view) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.vehicles.view, + FREIGHT_PERMS.vehicles.create, + FREIGHT_PERMS.vehicles.update, + FREIGHT_PERMS.vehicles.delete, +]) export class VehiclesController { constructor( private readonly vehiclesService: VehiclesService, diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts index cdf34cd66..551f840b3 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts @@ -23,9 +23,13 @@ import { WarehouseInspectionService } from './warehouse-inspection.service'; // Baseline read: inspection reports are opened from inventory screens too — // either view permission grants reads; writes stack their own per route. @Controller() +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. @BookingStaff([ FREIGHT_PERMS.warehouseInspectionReports.view, FREIGHT_PERMS.warehouseInventory.view, + FREIGHT_PERMS.warehouseInspectionReports.create, + FREIGHT_PERMS.warehouseInspectionReports.update, ]) export class WarehouseInspectionController { constructor(private readonly inspectionService: WarehouseInspectionService) {} diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts index 594fd7a6f..04cbacd28 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts @@ -12,7 +12,13 @@ import { WarehouseZonesService } from './warehouse-zones.service'; // receive/move pickers) — either view permission grants reads; writes stack // their specific permission per route. @Controller('warehouse-zones') -@BookingStaff([FREIGHT_PERMS.warehouseZones.view, FREIGHT_PERMS.warehouseInventory.view]) +// Class gate lists every key its routes use: Nest runs class AND method +// guards, so a key missing here would deny before the route's own key runs. +@BookingStaff([ + FREIGHT_PERMS.warehouseZones.view, + FREIGHT_PERMS.warehouseInventory.view, + FREIGHT_PERMS.warehouseZones.update, +]) export class WarehouseZonesController { constructor(private readonly zonesService: WarehouseZonesService) {} diff --git a/deploy/position-type-grant-mapping.sql b/deploy/position-type-grant-mapping.sql deleted file mode 100644 index 901191dd4..000000000 --- a/deploy/position-type-grant-mapping.sql +++ /dev/null @@ -1,119 +0,0 @@ --- Position-type grant mapping for the granular permission system rollout. --- Grants the NEW granular keys to every hand-curated position type that holds --- the old broad key whose routes the new keys took over. Idempotent (unique --- constraint on (position_type_id, permission_id) + ON CONFLICT DO NOTHING). --- --- PREREQUISITE: run the seeded API once first (SEED_EDR_ORG=true) so --- EdrOrgSeeder has created the new permission rows this script references. --- Running it too early is not destructive but silently under-applies: keys that --- do not exist yet simply match nothing (measured: 11 of 42 rows land pre-seed, --- because invoices:view/export and payments:view already exist on dev). Re-run --- after seeding — it is safe to run any number of times. --- --- Verified 2026-08-07 on a virgin restore of the live dev DB: seeded boot, then --- this script → 42 rows inserted, second run → 0 rows, final per-key grant --- counts identical to the reference environment. Per-user API probes across 20 --- departmental test accounts confirm the keys resolve through /me and gate --- routes correctly. - -BEGIN; - --- Helper shape used throughout: --- holders of => also grant - --- 1. bookings:view holders => dashboard/read keys that replaced blanket access -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pnew.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pold ON pold.id = ptp.permission_id - AND pold.key = 'edr_freight_app:bookings:view' -JOIN iam.permissions pnew ON pnew.key IN ( - 'edr_freight_app:overview:view', - 'edr_freight_app:reports:view', - 'edr_freight_app:invoices:view', - 'edr_freight_app:invoices:export', - 'edr_freight_app:payments:view' -) -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 2. train_scheduling:update holders => the write actions split out of it -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pnew.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pold ON pold.id = ptp.permission_id - AND pold.key = 'edr_freight_app:train_scheduling:update' -JOIN iam.permissions pnew ON pnew.key IN ( - 'edr_freight_app:train_scheduling:dispatch', - 'edr_freight_app:train_scheduling:mark_paid', - 'edr_freight_app:train_scheduling:expire_booking' -) -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 3. GL Djibouti clearance holders => final-invoice raise + confirm -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pnew.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pold ON pold.id = ptp.permission_id - AND pold.key = 'edr_freight_app:contracts:clearance_dj_actions' -JOIN iam.permissions pnew ON pnew.key IN ( - 'edr_freight_app:contracts:final_invoice_raise', - 'edr_freight_app:contracts:final_invoice_confirm' -) -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 4. GL Ethiopia clearance holders => final-invoice confirm -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pnew.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pold ON pold.id = ptp.permission_id - AND pold.key = 'edr_freight_app:contracts:clearance_et_actions' -JOIN iam.permissions pnew ON pnew.key = 'edr_freight_app:contracts:final_invoice_confirm' -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 5. Contract-intake holders (any staff_accept flavour) => edit_document -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pnew.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pold ON pold.id = ptp.permission_id - AND pold.key IN ( - 'edr_freight_app:bookings:staff_accept', - 'edr_freight_app:contracts:staff_accept:bulk', - 'edr_freight_app:contracts:staff_accept:container' - ) -JOIN iam.permissions pnew ON pnew.key = 'edr_freight_app:contracts:edit_document' -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 6. Support inbox ownership (decision 2026-08-07): marketing department types -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT pt.id, p.id -FROM iam.position_types pt -CROSS JOIN iam.permissions p -WHERE (pt.name::text ILIKE '%marketing%' OR pt.key ILIKE '%marketing%') - AND p.key IN ( - 'edr_freight_app:support:agent_view', - 'edr_freight_app:support:agent_send' - ) -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - --- 7. Companion view keys. --- Most freight controllers carry a class-level `:view` guard, and Nest --- runs class AND method guards — so a type holding only `:` is --- denied before the action key is ever checked. Grant the module's view key --- alongside every action key the type already holds. View-only, so it widens --- reads within a module the type already operates in, never across modules. -INSERT INTO iam.position_type_permissions (position_type_id, permission_id) -SELECT DISTINCT ptp.position_type_id, pview.id -FROM iam.position_type_permissions ptp -JOIN iam.permissions pact ON pact.id = ptp.permission_id - AND pact.key LIKE 'edr_freight_app:%' -JOIN iam.permissions pview ON pview.key = regexp_replace(pact.key, ':[^:]+$', ':view') -ON CONFLICT (position_type_id, permission_id) DO NOTHING; - -COMMIT; - --- Verification: expected non-zero counts per new key after running. --- SELECT p.key, count(*) FROM iam.position_type_permissions ptp --- JOIN iam.permissions p ON p.id = ptp.permission_id --- WHERE p.key IN ('edr_freight_app:overview:view','edr_freight_app:support:agent_view', --- 'edr_freight_app:train_scheduling:dispatch','edr_freight_app:contracts:final_invoice_raise') --- GROUP BY p.key;