From f76decbe7f7cffbe482139dfd53db571f2a7266e Mon Sep 17 00:00:00 2001 From: arhimede Date: Fri, 4 Sep 2026 22:43:29 +0300 Subject: [PATCH 1/2] Replace authorization config example with shipped config in v7 docs The example used pre-6.0 AdminRole::ROLE_* constants and invented dot-notation permissions, and was invalid PHP. Documents the real enum-keyed config, the child-to-parent inheritance direction, and how AuthorizationMiddleware resolves a request. Signed-off-by: arhimede --- docs/book/v7/core-features/authorization.md | 184 +++++++++++++++----- 1 file changed, 142 insertions(+), 42 deletions(-) diff --git a/docs/book/v7/core-features/authorization.md b/docs/book/v7/core-features/authorization.md index cc1b3b5..84f50e4 100644 --- a/docs/book/v7/core-features/authorization.md +++ b/docs/book/v7/core-features/authorization.md @@ -3,7 +3,8 @@ ## Summary Authorization decides whether an already-authenticated identity may reach a given resource. -Dotkernel API implements it with role-based access control through `Mezzio\Authorization\Rbac\LaminasRbac`, applied by `AuthorizationMiddleware` and configured in `config/autoload/authorization.global.php`, where each permission is a route name and roles inherit from their parents. +Dotkernel API implements it with role-based access control through `Mezzio\Authorization\Rbac\LaminasRbac`, applied by `AuthorizationMiddleware` and configured in `config/autoload/authorization.global.php`, where each permission is a route name. +Inheritance runs from child to parent, so `superuser` inherits every permission granted to `admin` without declaring any of its own. ## Details @@ -13,7 +14,7 @@ Authorization is the process by which a system takes a validated identity and ch ## How it works -In Dotkernel API each authenticatable entity (admin/user) comes with their `roles` table where you can define roles for each entity. +In Dotkernel API each authenticatable entity (admin/user) has its own role table — `admin_role` and `user_role` — plus a join table, `admin_roles` and `user_roles`, assigning roles to accounts. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the `Api\App\Middleware\AuthorizationMiddleware` middleware. @@ -24,59 +25,144 @@ Dotkernel API makes use of `mezzio-authorization-rbac` and includes the full con The configuration file for the role and permission definitions is `config/autoload/authorization.global.php`. +Roles are the backed enums `Core\Admin\Enum\AdminRoleEnum` (`superuser`, `admin`) and +`Core\User\Enum\UserRoleEnum` (`user`, `guest`), so the array keys are their `->value` strings. + ```php -'mezzio-authorization-rbac' => [ - 'roles' => [ - AdminRole::ROLE_SUPERUSER => [], - AdminRole::ROLE_ADMIN => [ - AdminRole::ROLE_SUPERUSER, - ], - UserRole::ROLE_GUEST => [ - UserRole::ROLE_USER, - ], - ], - 'permissions' => [ - AdminRole::ROLE_SUPERUSER => [], - AdminRole::ROLE_ADMIN => [ - 'other.routes' - 'admin.list', - 'home' - ], - UserRole::ROLE_USER => [ - 'other.routes', - 'user.my-account.update', - 'user.my-account.view', +use Core\Admin\Enum\AdminRoleEnum; +use Core\User\Enum\UserRoleEnum; + +return [ + 'mezzio-authorization-rbac' => [ + 'roles' => [ + AdminRoleEnum::Superuser->value => [], + AdminRoleEnum::Admin->value => [ + AdminRoleEnum::Superuser->value, + ], + UserRoleEnum::Guest->value => [ + UserRoleEnum::User->value, + ], ], - UserRole::ROLE_GUEST => [ - 'other.routes', - 'security.refresh-token', - 'error.report', - 'home', + 'permissions' => [ + AdminRoleEnum::Superuser->value => [], + AdminRoleEnum::Admin->value => [ + 'admin::list-admin', + 'admin::create-admin', + 'admin::delete-admin', + 'admin::view-admin', + 'admin::update-admin', + 'admin::list-role', + 'admin::view-role', + 'admin::view-account', + 'admin::update-account', + 'user::list-user', + 'user::create-user', + 'user::delete-user', + 'user::view-user', + 'user::update-user', + 'user::delete-user-avatar', + 'user::view-user-avatar', + 'user::create-user-avatar', + 'user::list-role', + 'user::view-role', + 'user::activate-user', + 'user::deactivate-user', + 'app::create-error-report', + 'app::view-index', + ], + UserRoleEnum::User->value => [ + 'user::delete-account', + 'user::view-account', + 'user::update-account', + 'user::delete-account-avatar', + 'user::view-account-avatar', + 'user::create-account-avatar', + ], + UserRoleEnum::Guest->value => [ + 'app::create-error-report', + 'app::view-index', + 'user::activate-account', + 'user::request-activate-account', + 'user::recover-account', + 'user::check-account-reset-password', + 'user::update-account-reset-password', + 'user::create-account-reset-password', + 'user::create-account', + 'security::generate-token', + 'security::refresh-token', + ], ], ], -], +]; ``` +That is the complete shipped configuration, not an excerpt. +Between them the three populated roles grant **38 permissions covering all 38 routes**: every route +the application declares is reachable by at least one role, and no permission names a route that +does not exist. +Only `app::view-index` and `app::create-error-report` are granted twice, to both `admin` and `guest`. + > See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/) > for more information. ## Usage -Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user roles (`user`, `guest`). +Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user +roles (`user`, `guest`). + +A permission in Dotkernel API is a **route name** — the third argument given to the route in a +module's `RoutesDelegator`. To list the names you can grant, run +`php ./bin/cli.php route:list`; see [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md). + +### How inheritance works here + +The array under `roles` maps a role to its **parents**, and inheritance runs in the direction that +often surprises people: a **parent receives the permissions of its children**, because +`laminas-permissions-rbac` resolves `hasPermission()` by walking down into child roles. + +So in the shipped configuration: -Roles inherit the permissions from their parents: +| Entry | Meaning | +| --- | --- | +| `superuser => []` | `superuser` has no parent | +| `admin => [superuser]` | `superuser` is the parent of `admin`, so **`superuser` inherits everything granted to `admin`** | +| `guest => [user]` | `user` is the parent of `guest`, so **`user` inherits everything granted to `guest`** | -- `superuser` has no parent -- `admin` has `superuser` as a parent which means `superuser` also has `admin` permissions -- `user` has no parent -- `guest` has `user` as a parent which means `user` also has `guest` permissions +That is why `superuser` needs no permissions of its own: its list is empty, yet it can reach all 23 +routes granted to `admin`. -For each role we defined an array of permissions. -A permission in Dotkernel API is basically a route name. +It is also why `user` ends up with 17 effective permissions — its own 6 plus the 11 granted to +`guest` — while `guest` keeps only its own 11 and cannot reach the account routes reserved for a +signed-in user. -As you can see, the `superuser` does not have its own permissions, because it gains all the permissions from `admin`, no need to define explicit permissions. +Effective totals, once inheritance is applied: -The `user` role, gains all the permission from `guest` so no need to define that `user` can access `home` route, but `guest` cannot access user-specific routes. +| Role | Own | Inherited | Effective | +| --- | --- | --- | --- | +| `superuser` | 0 | 23 from `admin` | 23 | +| `admin` | 23 | — | 23 | +| `user` | 6 | 11 from `guest` | 17 | +| `guest` | 11 | — | 11 | + +### How a request is authorized + +`AuthorizationMiddleware` injects `Mezzio\Authorization\AuthorizationInterface` rather than an RBAC +class directly — the RBAC adapter is bound by `Mezzio\Authorization\Rbac\ConfigProvider`, registered +in `config/config.php`. + +For each request it: + +1. Reads `oauth_client_id` from the authenticated identity and loads the matching record — `admin` from the `admin` table, `frontend` from the `user` table, or a `Guest` instance when the client is `guest`. An unrecognised client is rejected. +2. Rejects an account that is inactive, or a user that has been deleted. +3. Replaces the identity's roles with the role names read from that record. +4. Calls `isGranted()` once per role and allows the request as soon as **any** role grants the route. + +If no role grants it, the response is `403 Forbidden` with +`You are not allowed to access this resource.` + +> Note this middleware returns a plain JSON error body rather than a Problem Details document, so an +> authorization failure does not look like the errors described in +> [Problem details](../extended-features/problem-details.md). ## FAQ @@ -98,15 +184,18 @@ A route with no permission entry is unreachable for that role. **Q: Which access control model is used?** A: RBAC, via `mezzio-authorization-rbac` backed by `laminas-permissions-rbac`. +`AuthorizationMiddleware` depends only on `Mezzio\Authorization\AuthorizationInterface`, so the adapter is selected by configuration rather than hardcoded. **Q: How does role inheritance work here?** -A: A role listed inside another role's entry is its parent's beneficiary: because `admin` lists `superuser`, `superuser` receives everything granted to `admin`. -That is why `superuser` needs no explicit permissions of its own. +A: The values listed against a role are its parents, and a parent inherits from its children — `laminas-permissions-rbac` resolves a permission by walking down into child roles. +Because `admin` lists `superuser`, `superuser` receives everything granted to `admin`, which is why `superuser` needs no permissions of its own. +Likewise `guest` lists `user`, so `user` inherits the guest permissions on top of its own. **Q: Where are roles stored?** -A: Each authenticatable entity — admin or user — has its own `roles` table where its roles are defined. +A: In `admin_role` and `user_role`, with `admin_roles` and `user_roles` as the join tables that assign them to accounts. +The role names themselves come from the `AdminRoleEnum` and `UserRoleEnum` backed enums, so adding a role means adding an enum case as well as a row. **Q: Which middleware enforces this?** @@ -116,3 +205,14 @@ See [Middleware flow](../flow/middleware-flow.md). **Q: Can I use ACL instead of RBAC?** A: The ACL adapter ships with the project, but RBAC is what Dotkernel API is configured for; switching means replacing the authorization configuration. +Because the middleware only knows `AuthorizationInterface`, no application code needs to change. + +**Q: Do the permissions cover every route?** + +A: Yes, exactly. The three populated roles grant 38 permissions across the 38 declared routes, with no route ungranted and no permission naming a route that does not exist. +`app::view-index` and `app::create-error-report` are the only two granted to two roles. + +**Q: What does a rejected request look like?** + +A: `403 Forbidden` with `You are not allowed to access this resource.` +The same status is returned when the account is inactive, the user was deleted, or the OAuth client is unrecognised, each with its own message. From fc3ed11fe17520baf4fb14cde36126798717ab02 Mon Sep 17 00:00:00 2001 From: arhimede Date: Sat, 5 Sep 2026 18:18:43 +0300 Subject: [PATCH 2/2] Use one sentence per line in authorization docs Unwraps ten sentences and splits two shared lines. No wording changes. Signed-off-by: arhimede --- docs/book/v7/core-features/authorization.md | 43 +++++++-------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/docs/book/v7/core-features/authorization.md b/docs/book/v7/core-features/authorization.md index 84f50e4..5a50e99 100644 --- a/docs/book/v7/core-features/authorization.md +++ b/docs/book/v7/core-features/authorization.md @@ -25,8 +25,7 @@ Dotkernel API makes use of `mezzio-authorization-rbac` and includes the full con The configuration file for the role and permission definitions is `config/autoload/authorization.global.php`. -Roles are the backed enums `Core\Admin\Enum\AdminRoleEnum` (`superuser`, `admin`) and -`Core\User\Enum\UserRoleEnum` (`user`, `guest`), so the array keys are their `->value` strings. +Roles are the backed enums `Core\Admin\Enum\AdminRoleEnum` (`superuser`, `admin`) and `Core\User\Enum\UserRoleEnum` (`user`, `guest`), so the array keys are their `->value` strings. ```php use Core\Admin\Enum\AdminRoleEnum; @@ -97,28 +96,21 @@ return [ ``` That is the complete shipped configuration, not an excerpt. -Between them the three populated roles grant **38 permissions covering all 38 routes**: every route -the application declares is reachable by at least one role, and no permission names a route that -does not exist. +Between them the three populated roles grant **38 permissions covering all 38 routes**: every route the application declares is reachable by at least one role, and no permission names a route that does not exist. Only `app::view-index` and `app::create-error-report` are granted twice, to both `admin` and `guest`. -> See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/) -> for more information. +> See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/) for more information. ## Usage -Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user -roles (`user`, `guest`). +Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user roles (`user`, `guest`). -A permission in Dotkernel API is a **route name** — the third argument given to the route in a -module's `RoutesDelegator`. To list the names you can grant, run -`php ./bin/cli.php route:list`; see [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md). +A permission in Dotkernel API is a **route name** — the third argument given to the route in a module's `RoutesDelegator`. +To list the names you can grant, run `php ./bin/cli.php route:list`; see [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md). ### How inheritance works here -The array under `roles` maps a role to its **parents**, and inheritance runs in the direction that -often surprises people: a **parent receives the permissions of its children**, because -`laminas-permissions-rbac` resolves `hasPermission()` by walking down into child roles. +The array under `roles` maps a role to its **parents**, and inheritance runs in the direction that often surprises people: a **parent receives the permissions of its children**, because `laminas-permissions-rbac` resolves `hasPermission()` by walking down into child roles. So in the shipped configuration: @@ -128,12 +120,9 @@ So in the shipped configuration: | `admin => [superuser]` | `superuser` is the parent of `admin`, so **`superuser` inherits everything granted to `admin`** | | `guest => [user]` | `user` is the parent of `guest`, so **`user` inherits everything granted to `guest`** | -That is why `superuser` needs no permissions of its own: its list is empty, yet it can reach all 23 -routes granted to `admin`. +That is why `superuser` needs no permissions of its own: its list is empty, yet it can reach all 23 routes granted to `admin`. -It is also why `user` ends up with 17 effective permissions — its own 6 plus the 11 granted to -`guest` — while `guest` keeps only its own 11 and cannot reach the account routes reserved for a -signed-in user. +It is also why `user` ends up with 17 effective permissions — its own 6 plus the 11 granted to `guest` — while `guest` keeps only its own 11 and cannot reach the account routes reserved for a signed-in user. Effective totals, once inheritance is applied: @@ -146,23 +135,19 @@ Effective totals, once inheritance is applied: ### How a request is authorized -`AuthorizationMiddleware` injects `Mezzio\Authorization\AuthorizationInterface` rather than an RBAC -class directly — the RBAC adapter is bound by `Mezzio\Authorization\Rbac\ConfigProvider`, registered -in `config/config.php`. +`AuthorizationMiddleware` injects `Mezzio\Authorization\AuthorizationInterface` rather than an RBAC class directly — the RBAC adapter is bound by `Mezzio\Authorization\Rbac\ConfigProvider`, registered in `config/config.php`. For each request it: -1. Reads `oauth_client_id` from the authenticated identity and loads the matching record — `admin` from the `admin` table, `frontend` from the `user` table, or a `Guest` instance when the client is `guest`. An unrecognised client is rejected. +1. Reads `oauth_client_id` from the authenticated identity and loads the matching record — `admin` from the `admin` table, `frontend` from the `user` table, or a `Guest` instance when the client is `guest`. + An unrecognised client is rejected. 2. Rejects an account that is inactive, or a user that has been deleted. 3. Replaces the identity's roles with the role names read from that record. 4. Calls `isGranted()` once per role and allows the request as soon as **any** role grants the route. -If no role grants it, the response is `403 Forbidden` with -`You are not allowed to access this resource.` +If no role grants it, the response is `403 Forbidden` with `You are not allowed to access this resource.` -> Note this middleware returns a plain JSON error body rather than a Problem Details document, so an -> authorization failure does not look like the errors described in -> [Problem details](../extended-features/problem-details.md). +> Note this middleware returns a plain JSON error body rather than a Problem Details document, so an authorization failure does not look like the errors described in [Problem details](../extended-features/problem-details.md). ## FAQ