Skip to content

fix(permissions): enforce Fleet-Ops permissions across the API and console - #345

Merged
roncodes merged 4 commits into
release/v0.6.70from
fix/permission-enforcement
Sep 28, 2026
Merged

roncodes merged 4 commits into
release/v0.6.70from
fix/permission-enforcement

Conversation

@roncodes

Copy link
Copy Markdown
Member

Summary

This PR comes out of an authorization audit of Fleet-Ops. Each finding was verified against a local stack as a non-admin (dispatcher) user.

Security

  • GET fleet-ops/navigator/link-app was outside the authenticated group and redirected anonymous callers with an API key for the first system admin's organization.
    • The console now requests a 30-minute HMAC-signed link, and only system admins can request one (get-link-app is behind fleetbase.protected).
    • link-app rejects unsigned, tampered or expired links with 403.
  • Entity editing settings are one platform-wide map keyed by order config, and any user's save replaced the whole map.
    • Saves now merge only the session company's order configs, keeping other organizations' entries, and reads return only the company's own.
    • Driver onboard settings are pinned to the session company instead of a request-supplied companyId.

Schema (Auth/Schemas/FleetOps.php)

  • Declares resources the UI already checked but that never existed, which left their API unguarded and their screens unreachable for non-admins: maintenance-schedule, device, sensor, device-event, telematic, warranty, purchase-rate, fuel-provider-connection, fuel-provider-transaction, fuel-provider-sync-run, analytics, scheduling-settings and tracking-settings. service-rate gains export.
  • Fixes 'action' → 'actions' on the settings resources, so fleet-ops onboard payments is actually created.
  • Grants the new resources in FleetManager, MaintenanceManager, ServiceRateManager, OperationsAdmin and DispatchManager.

Custom actions and non-resource controllers

  • AuthorizationGuard maps an unmatched method by HTTP verb. For example, bulk dispatch required create order, unassign-vehicle required create driver, and trailer attach required create trailer.
  • Controllers now declare $methodPermissions (method → schema permission). FleetOpsController registers it as controller middleware through the new AuthorizesMethods trait, and those methods carry #[SkipAuthorizationCheck].
  • Controllers outside the guard now enforce permissions:
    • Analytics and Metrics: view analytics.
    • Live map feeds: the list permission of each layer.
    • Orchestrator: optimize / import / assign-driver-for order.
    • Manifests: list / view / cancel / delete / update order.
    • Radar write actions: update driver or vehicle.
    • Customer portal logins: reset-credentials-for customer.
    • Stripe: onboard / view payments.
    • Settings writes: the matching *-settings permission. Admin map and tracking settings: system admins.

Console

  • Route guards: list guards on every index route that lacked one, and create / view / update guards on new, details and edit routes. Orders is the landing route, so a user without list order is sent to the first area they can open.
  • Buttons and menus:
    • New / Import / Export buttons, bulk actions and row actions pass their permission.
    • The order details menu hides actions the user cannot perform.
  • Navigation:
    • Sidebar hubs, header shortcuts and extension menu items (virtual route) are permission-aware.
    • Reports use the iam … report permissions the core reports API enforces.
  • Live map: layers checked plural names (list vehicles), so non-admins saw an empty map. They now use singular resource names.
  • Maintenance redirects: details and edit redirects used route names without the console.fleet-ops. prefix, and threw.

Behaviour changes to note

  • Dispatching now needs dispatch order; it used to need update order. The built-in OrderCoordinator policy has no dispatch.
  • FleetOps dashboards and metrics need view analytics.
  • Run php artisan fleetbase:create-permissions after deploying.

Companion PRs

fleetbase/core-api#278 (core authorization gaps, reports on iam), fleetbase/ember-ui#185 (bulk action and header permissions), fleetbase/ember-core#94 (shortcut permissions).

Test plan

  • Every server/tests file run through scripts/pest-runner.php. Two files fail, and they fail on release/v0.6.70 too: InternalImportExportControllerContractsTest and SmallControllerContractsTest.
  • New and updated tests: signed, unsigned, tampered and expired Navigator links; entity editing merge and tenant scoping; driver onboard company pinning.
  • Local stack as a dispatcher:
    • 200 on orders and live map.
    • 401 on devices, analytics, metrics, unassign-vehicle (assign-vehicle-for driver), trailer attach (attach-vehicle-for trailer), settings saves, orchestrator run and get-link-app.
    • Unsigned link-app → 403; signed → 302.
  • eslint and ember-template-lint on the changed addon files.

…mapped endpoints

Schema
- Declare resources the UI already checked but the schema never created, which
  left their API unguarded and their screens unreachable for non-admins:
  maintenance-schedule, device, sensor, device-event, telematic, warranty,
  purchase-rate, fuel-provider-connection/-transaction/-sync-run, analytics,
  scheduling-settings, tracking-settings. service-rate gains export.
- Fix the 'action' => 'actions' key on the settings resources, so
  `fleet-ops onboard payments` is actually created.
- Grant the new resources in the built-in policies (FleetManager,
  MaintenanceManager, ServiceRateManager, OperationsAdmin, DispatchManager).

Custom actions (Support\Authorization + #[SkipAuthorizationCheck])
- AuthorizationGuard maps unmatched method names by HTTP verb, so e.g.
  bulk-dispatch required `create order`, unassign-vehicle `create driver` and
  trailer attach `create trailer`. Map them to the schema actions: dispatch,
  cancel, schedule, import, assign-driver-for, update-route-for order;
  assign-order-for / assign-vehicle-for / update-user-for driver;
  attach-/detach-*-for trailer; and update on vehicle, vendor, device,
  telematic, maintenance-schedule, maintenance, work-order and
  fuel-provider-* sub-actions.

Controllers outside the guard
- Analytics and metrics require `view analytics`; live map feeds require list
  order/driver/vehicle/place; orchestrator, manifests, radar writes, customer
  portal logins and Stripe payments now require their permissions.
- Settings writes require the matching *-settings permission; the admin
  tracking/map settings require a system admin.
- Entity editing settings are one platform-wide map: a save now only writes
  the session company's order configs and keeps other companies' entries, and
  reads return only the company's own. Driver onboard settings are pinned to
  the session company instead of a request-supplied company id.
- Navigator link-app returned an API key to unauthenticated callers. The link
  is now a 30 minute signed URL issued only to system admins, and link-app
  rejects unsigned requests.
…ermission

- List guards on every index route that lacked one (orders, routes, trailers,
  fuel transactions, integrated vendors, all maintenance and connectivity
  routes, analytics, settings) and create/view/update guards on new, details
  and edit routes. Orders is the landing route, so it forwards users without
  `list order` to the first area they can open.
- Index New/Import/Export buttons, bulk actions and row actions pass their
  permission; the order details menu hides actions the user cannot perform.
- Sidebar hub items keep their permissions, branch defaults skip routes the
  user cannot open, and items use the schema's resource names (analytics,
  fuel-provider-*, list custom-field/avatar). Reports use the iam report
  permissions that the core reports API enforces.
- Header shortcuts carry their module permission; the virtual route honours
  menu item permissions.
- Live map layers check singular resource names (list vehicle, not vehicles),
  so non-admin users see their layers.
- Maintenance details/edit redirects used route names without the
  console.fleet-ops prefix and threw; fixed.
… controller middleware

- Move the explicit checks out of method bodies into a declarative map:
  FleetOpsController registers `$methodPermissions` (method => permission) as
  controller middleware via the AuthorizesMethods trait, and non-resource
  controllers call authorizeMethods() in their constructor. Behaviour through
  the router is unchanged; unit tests that call controller methods directly
  keep exercising the endpoint logic.
- Navigator links are signed with an HMAC over the expiry
  (NavigatorController::linkSignature) instead of Laravel's signed routes, so
  the link no longer depends on a named route or the URL generator.
- Tests: signed/unsigned/tampered/expired Navigator links; entity editing
  settings keep other companies' entries and ignore keys for foreign order
  configs; driver onboard settings ignore a request-supplied company id.
@roncodes
roncodes merged commit ee457c2 into release/v0.6.70 Sep 28, 2026
2 of 4 checks passed
@roncodes
roncodes deleted the fix/permission-enforcement branch September 28, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant