Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/rest-middleware.md for this page in Markdown format</div>
REST Middleware & RBAC
This guide covers the middleware pipeline that runs before every REST controller action, how authentication and authorization policies are configured, and how response filtering and relation scoping work.
Overview
Every REST request passes through version resolution and then a four-middleware pipeline in RouterDispatcher::dispatch() before the controller action executes. The pipeline runs unconditionally — it cannot be opted out of per-controller. Policy configuration in application/config/rest_policies.php controls the behavior of each middleware for each controller and method.
Request
→ MY_Router (strips /v{N}/ segment, captures apiVersion)
→ RouterDispatcher (resolves version, injects headers, swaps override controllers)
→ AuthenticationMiddleware (extracts JWT, populates RequestContext)
→ AuthorizationMiddleware (enforces policy auth type + roles, halts on 401/403)
→ ResourceContextMiddleware (sets ResourceContext on controller)
→ RelationFilterMiddleware (strips disallowed ?with= relations per scope)
→ Controller actionPre-pipeline: Version Resolution
Before the middleware pipeline runs, RouterDispatcher resolves the API version at lines 21-59:
MY_Router::_parse_routes()extracts the version number from/rest/v{N}/...URLs and rewrites the URI to strip the version segment, sorest_routes.phpneeds no per-version routes (application/core/MY_Router.php:31-37).VersionResolver::resolve()maps the version to aVersionResultDTO with lifecycle status, deprecation dates, and controller overrides (src/Rest/Versioning/VersionResolver.php:35-56).- Unknown versions return 400 ("Invalid API version"). Obsolete versions return 410 Gone with
Deprecation,Sunset, andLinkheaders pointing to the latest version. - If a controller override exists for the requested version,
RouterDispatcherswaps the controller class before DI instantiation (RouterDispatcher.php:52-54). - Version response headers (
Api-Version, and optionallyDeprecation/Sunset/Link) are injected on all responses. - The resolved
apiVersionis passed intoRequestContextand is available to middleware and controllers.
See SY-31 REST API Versioning for the full flow documentation.
Middleware Chain
1. AuthenticationMiddleware
Reads the Authorization: Bearer <token> header, validates the JWT, and populates a RequestContext object with the authenticated identity and roles. This middleware never halts the request — if no token is present or the token is invalid, it produces an anonymous context. Halting on missing/invalid tokens is the responsibility of AuthorizationMiddleware.
2. AuthorizationMiddleware
Resolves the effective policy for the incoming controller + method combination and enforces it. It halts with:
- 401 Unauthorized — when the policy requires authentication but the request context is anonymous.
- 403 Forbidden — when the request is authenticated but the identity lacks the required role.
Auth types:
| Auth type | Behavior |
|---|---|
none | No check — all requests pass through, even with no token. |
guest | Same as none — public endpoint, no token required. |
any | Requires a valid JWT of any type (customer or backend). |
customer | Requires a valid customer JWT. |
backend | Requires a valid backend (admin) JWT. |
legacy_guard | Delegates to the controller's own guard trait (backwards compatibility). |
3. ResourceContextMiddleware
After authorization passes, this middleware sets a ResourceContext instance on the controller. The context carries the resolved scope (public, customer, backend) and is available to Resource transformers for response field filtering. This runs for all controllers automatically — no opt-in required.
4. RelationFilterMiddleware
Reads the ?with= query parameter and strips any relation names that are not in the allowed list for the current scope. Stripping is silent — no error is returned for disallowed relations, they are simply removed before the controller sees the parameter. This prevents clients from leaking data via unintended eager-loaded relations.
Policy Configuration
Policies are defined in application/config/rest_policies.php. The file has two top-level keys: defaults for global fallback behavior, and controllers for per-controller configuration.
Structure
php
return [
'defaults' => [
'auth' => 'backend', // fallback auth type for any controller not listed
'roles' => [], // no role restriction by default (any backend user)
],
'controllers' => [
// Legacy: Adv_products_admin → ADVISABLE, ADMIN, PRODUCTS
\Advisable\Rest\Product\Controllers\Product::class => [
'defaults' => [
'auth' => 'backend',
'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS],
],
'methods' => [
'index' => ['auth' => 'guest'],
'show' => ['auth' => 'guest'],
'item' => ['auth' => 'guest'],
],
],
],
];Resolution Order
For any incoming request, the effective policy is resolved in this order (first match wins):
- Method-level —
controllers[ControllerClass]['methods'][action] - Controller-level defaults —
controllers[ControllerClass]['defaults'] - Global defaults —
defaults
This means you can make read endpoints public (guest) while keeping write endpoints backend-only, without repeating the backend policy on every write method.
Example: Public reads, role-restricted writes
php
// Legacy: Adv_blog_admin → ADVISABLE, ADMIN, CMS
BlogArticle::class => [
'defaults' => ['auth' => 'backend', 'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_CMS]],
'methods' => [
'index' => ['auth' => 'guest'],
'show' => ['auth' => 'guest'],
'item' => ['auth' => 'guest'],
],
],Role System
The RBAC role system mirrors the legacy admin role system. Roles are integers stored as constants (application/config/constants.php) and embedded in the JWT payload at login time. They are checked only when the policy roles array is non-empty.
| Constant | Value | Description |
|---|---|---|
AUTH_ROLE_ADVISABLE | 1 | Superuser — bypasses all role checks in middleware |
AUTH_ROLE_DEVELOPER | 2 | Developer / technical settings |
AUTH_ROLE_ADMIN | 3 | General admin (high-privilege, but NOT auto-bypass) |
AUTH_ROLE_CMS | 4 | CMS / content management |
AUTH_ROLE_PRODUCTS | 5 | Product catalog management |
AUTH_ROLE_ORDERS | 6 | Order management |
AUTH_ROLE_REPORTING | 7 | Reporting and analytics |
AUTH_ROLE_MARKETING | 8 | Marketing / campaigns / promotions |
AUTH_ROLE_MEDIA | 9 | Media / sliders / assets |
Role hierarchy
AUTH_ROLE_ADVISABLE(1) is the only automatic bypass. Any JWT carrying this role skips all further role checks inAuthorizationMiddleware.AUTH_ROLE_ADMIN(3) is high-privilege but does not auto-bypass. It must be explicitly listed in eachrolesarray where allowed. Some routes (e.g. Audit, AI Settings) are ADVISABLE-only and intentionally exclude ADMIN.- Domain roles (2, 4–9) grant access to specific functional areas.
Roles are assigned to admin users via the user_role database table and embedded in the JWT at login. A user may have multiple roles. The policy roles array is an allowlist — the authenticated identity must have at least one of the listed roles (or AUTH_ROLE_ADVISABLE) to proceed.
Legacy alignment
Every roles array in rest_policies.php has been verified against the corresponding legacy admin controller's allowRole() check. Each entry includes a legacy provenance comment documenting the source:
php
// Legacy: Adv_sliders_admin → ADVISABLE, ADMIN, MARKETING, MEDIA
Slider::class => [
'defaults' => ['auth' => 'backend', 'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA]],
...
],Convention: list all allowed roles explicitly (except ADVISABLE, which auto-bypasses). When adding new REST controllers, check the legacy admin controller's allowRole() and replicate the same roles.
Response Filtering
ResourceContextMiddleware sets a ResourceContext on every controller before the action runs. Resource transformers receive this context and conditionally include or exclude fields based on the caller's scope.
Scope-based field filtering
All resources with mixed public/backend access implement field filtering. Public and customer callers receive only storefront-safe fields; backend callers see everything.
Pattern:
php
protected function resource(): array
{
$data = [
'id' => $this->resource->id,
'price' => $this->resource->price,
// ... always-public fields
];
if ($this->context?->isBackend()) {
$data['wholesalePrice'] = $this->resource->wholesale_price;
$data['adminComments'] = $this->resource->admin_comments;
// ... backend-only fields
}
return $this->addRelationToData($data);
}Fields filtered per resource
| Resource | Backend-only fields (hidden from guest/customer) |
|---|---|
| Product | wholesalePrice, acquisitionValue, pointFactor, softDelete, negativeStock, hits, productView, hsCode, dateChanged, skroutzName, vendorCode, shelfcodeId, active, cartProductCustomization* |
| Category | published, isSensitive, asProductsDetails, menuColor, order, sliderId, menuSliderId, extraSliderId |
| Customer | isInsecure, totalPoints, hasAccess, isGuest, isSanitized, adminComments, erpId + relations: campaigns, messageHistory, smsMarketing, tags, audiences |
| Order | adminComments, adminOrderCheck, ipAddress, userAgent |
| BlogArticle | published, promo, hits, sliderId |
| BlogCategory | active, priority |
| BlogTag | active, priority |
| BlogAuthor | priority |
| Document | active, position |
| Video | isPromo |
| Page | menu, pageView, order, published, createdAt |
| Offer | ord |
| Event | active, dateChanged |
| Slider | groupPriority, slidesLimit |
| Slide | dateStart, dateEnd, audienceId, themeClass, priority |
Resources that are fully public (Cookie, Subcontent, Country, County, Currency, Vat) or fully backend-only (Coupon, Gift, all Transporter settings) do not need filtering.
Available context methods
| Method | True when |
|---|---|
isPublic() | No token or guest/none auth |
isCustomer() | Valid customer JWT |
isBackend() | Valid backend (admin) JWT |
isAuthenticated() | Either customer or backend |
getUserId() | Returns authenticated user's ID |
hasRole(int $roleId) | Check for a specific role |
Context is also available in write actions (store, update), allowing controllers to accept or reject request body fields based on the caller's scope.
Relation Filtering
The relations key in a controller's policy maps scope names to lists of allowed relation names for that scope:
php
'relations' => [
'guest' => ['category', 'images'],
'customer' => ['category', 'images', 'variants'],
'backend' => ['category', 'images', 'variants', 'attributes', 'vendor'],
],When a request arrives with ?with=attributes,images, RelationFilterMiddleware resolves the current scope from RequestContext and strips any relation not in that scope's list. In the example above, a guest request for ?with=attributes,images will receive only images — attributes is silently removed.
If no relations key is defined for a controller, no stripping occurs and the full ?with= value is passed through.
Adding a New Controller
By default, any controller not listed in rest_policies.php inherits the global defaults — which is backend auth with no role restriction. This is the safe default: new endpoints are backend-only until explicitly relaxed.
To make read endpoints public:
php
// application/config/rest_policies.php
\Advisable\Rest\MyContext\Controllers\MyEntity::class => [
'defaults' => ['auth' => 'backend'],
'methods' => [
'index' => ['auth' => 'guest'],
'show' => ['auth' => 'guest'],
],
],To add role requirements (check legacy admin controller's allowRole() and replicate):
php
// Legacy: Adv_products_admin → ADVISABLE, ADMIN, PRODUCTS
'defaults' => ['auth' => 'backend', 'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS]],To expose an endpoint to authenticated customers:
php
'index' => ['auth' => 'customer'],Client Repos
Client repos can override application/config/rest_policies.php to add policies for custom controllers registered under the Custom\Rest\ namespace. The file is a plain PHP array return — replace or merge it as needed. When a custom controller is not listed, it inherits the global defaults (backend auth), which is the recommended safe starting point.
Key Files
| File | Purpose |
|---|---|
application/config/rest_policies.php | Centralized policy configuration (auth types + roles per controller/method) |
application/config/constants.php | Role constant definitions (AUTH_ROLE_*) |
src/Rest/Middleware/AuthenticationMiddleware.php | JWT extraction and RequestContext population |
src/Rest/Middleware/AuthorizationMiddleware.php | Policy enforcement, 401/403 responses |
src/Rest/Middleware/ResourceContextMiddleware.php | Sets ResourceContext on controller |
src/Rest/Middleware/RelationFilterMiddleware.php | Strips disallowed ?with= relations |
src/Rest/Middleware/RequestContext.php | Carries authenticated identity, roles, and resolved policy |
src/Rest/Support/Resources/ResourceContext.php | Carries scope (public/customer/backend) for field filtering |
application/controllers/RouterDispatcher.php | Runs the middleware pipeline before controller dispatch |