fix(auth): keep secondary positions in permission checks

IAM lets an employee hold several positions, but the vendored JwtGuard
collapses employee.positions[] down to a single employee.position and
drops the rest. Non-delegate secondary positions vanished entirely, so
staff on two posts resolved to one post's permissions and every check
on the other rejected them.

FreightJwtGuard re-attaches the full list from the same session
snapshot the parent guard already read, so nothing extra is fetched
per request beyond a cached session lookup. employee.position is left
untouched, keeping audit logging and delegation unaffected.
collectPermissionKeys and collectPositionTypeKeys now union across
every position, and /me returns them all.

Verified against a real two-position user (djibouti-gl-director +
djibouti-gl-chief) on the local dev database:

  /me positions                     1   -> 2
  /me permissionKeys                17  -> 28
  GET /api/interchange-documents    403 -> 200
  GET /api/trains                   403 -> 200

11 permissions recovered, none lost. Six single-position users return
byte-identical payloads before and after.
This commit is contained in:
Nathnael
2026-08-25 12:00:43 +00:00
parent 0a85dbe60d
commit 01ea05f013
12 changed files with 265 additions and 67 deletions

View File

@@ -1,5 +1,5 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
import { FreightJwtGuard } from './freight-jwt.guard';
import {
FreightPermissionGuard,
@@ -11,7 +11,7 @@ import { FREIGHT_PERMS } from '../seed/freight-permissions.registry';
export const BookingStaff = (permission: string | string[]) =>
applyDecorators(
UseGuards(
JwtGuard,
FreightJwtGuard,
FreightPermissionGuard(
Array.isArray(permission) ? permission : [permission],
),
@@ -26,11 +26,11 @@ export const BookingStaff = (permission: string | string[]) =>
* BookingStaff(<view key>) or MixedAudience(); kept for routes not yet swept.
*/
export const StaffReference = () =>
applyDecorators(UseGuards(JwtGuard, FreightPermissionGuard([])));
applyDecorators(UseGuards(FreightJwtGuard, FreightPermissionGuard([])));
/** Portal routes: customer accounts only; ownership scoping stays in services. */
export const PortalCustomer = () =>
applyDecorators(UseGuards(JwtGuard, PortalCustomerGuard));
applyDecorators(UseGuards(FreightJwtGuard, PortalCustomerGuard));
/**
* Routes both audiences call (sign, shared document reads, handover): staff
@@ -40,7 +40,7 @@ export const PortalCustomer = () =>
export const MixedAudience = (permission: string | string[]) =>
applyDecorators(
UseGuards(
JwtGuard,
FreightJwtGuard,
MixedAudienceGuard(
Array.isArray(permission) ? permission : [permission],
),