Skip to content

Commit 275e549

Browse files
authored
Merge pull request #164 from dotkernel/authorization-config-and-inheritance
Replace authorization config example with shipped config in v7 docs
2 parents 53ed80d + fc3ed11 commit 275e549

1 file changed

Lines changed: 128 additions & 43 deletions

File tree

‎docs/book/v7/core-features/authorization.md‎

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
## Summary
44

55
Authorization decides whether an already-authenticated identity may reach a given resource.
6-
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.
6+
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.
7+
Inheritance runs from child to parent, so `superuser` inherits every permission granted to `admin` without declaring any of its own.
78

89
## Details
910

@@ -13,7 +14,7 @@ Authorization is the process by which a system takes a validated identity and ch
1314

1415
## How it works
1516

16-
In Dotkernel API each authenticatable entity (admin/user) comes with their `roles` table where you can define roles for each entity.
17+
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.
1718
RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource.
1819

1920
The authorization happens through the `Api\App\Middleware\AuthorizationMiddleware` middleware.
@@ -24,59 +25,129 @@ Dotkernel API makes use of `mezzio-authorization-rbac` and includes the full con
2425

2526
The configuration file for the role and permission definitions is `config/autoload/authorization.global.php`.
2627

28+
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.
29+
2730
```php
28-
'mezzio-authorization-rbac' => [
29-
'roles' => [
30-
AdminRole::ROLE_SUPERUSER => [],
31-
AdminRole::ROLE_ADMIN => [
32-
AdminRole::ROLE_SUPERUSER,
33-
],
34-
UserRole::ROLE_GUEST => [
35-
UserRole::ROLE_USER,
36-
],
37-
],
38-
'permissions' => [
39-
AdminRole::ROLE_SUPERUSER => [],
40-
AdminRole::ROLE_ADMIN => [
41-
'other.routes'
42-
'admin.list',
43-
'home'
44-
],
45-
UserRole::ROLE_USER => [
46-
'other.routes',
47-
'user.my-account.update',
48-
'user.my-account.view',
31+
use Core\Admin\Enum\AdminRoleEnum;
32+
use Core\User\Enum\UserRoleEnum;
33+
34+
return [
35+
'mezzio-authorization-rbac' => [
36+
'roles' => [
37+
AdminRoleEnum::Superuser->value => [],
38+
AdminRoleEnum::Admin->value => [
39+
AdminRoleEnum::Superuser->value,
40+
],
41+
UserRoleEnum::Guest->value => [
42+
UserRoleEnum::User->value,
43+
],
4944
],
50-
UserRole::ROLE_GUEST => [
51-
'other.routes',
52-
'security.refresh-token',
53-
'error.report',
54-
'home',
45+
'permissions' => [
46+
AdminRoleEnum::Superuser->value => [],
47+
AdminRoleEnum::Admin->value => [
48+
'admin::list-admin',
49+
'admin::create-admin',
50+
'admin::delete-admin',
51+
'admin::view-admin',
52+
'admin::update-admin',
53+
'admin::list-role',
54+
'admin::view-role',
55+
'admin::view-account',
56+
'admin::update-account',
57+
'user::list-user',
58+
'user::create-user',
59+
'user::delete-user',
60+
'user::view-user',
61+
'user::update-user',
62+
'user::delete-user-avatar',
63+
'user::view-user-avatar',
64+
'user::create-user-avatar',
65+
'user::list-role',
66+
'user::view-role',
67+
'user::activate-user',
68+
'user::deactivate-user',
69+
'app::create-error-report',
70+
'app::view-index',
71+
],
72+
UserRoleEnum::User->value => [
73+
'user::delete-account',
74+
'user::view-account',
75+
'user::update-account',
76+
'user::delete-account-avatar',
77+
'user::view-account-avatar',
78+
'user::create-account-avatar',
79+
],
80+
UserRoleEnum::Guest->value => [
81+
'app::create-error-report',
82+
'app::view-index',
83+
'user::activate-account',
84+
'user::request-activate-account',
85+
'user::recover-account',
86+
'user::check-account-reset-password',
87+
'user::update-account-reset-password',
88+
'user::create-account-reset-password',
89+
'user::create-account',
90+
'security::generate-token',
91+
'security::refresh-token',
92+
],
5593
],
5694
],
57-
],
95+
];
5896
```
5997

60-
> See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/)
61-
> for more information.
98+
That is the complete shipped configuration, not an excerpt.
99+
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.
100+
Only `app::view-index` and `app::create-error-report` are granted twice, to both `admin` and `guest`.
101+
102+
> See [mezzio-authorization-rbac](https://docs.mezzio.dev/mezzio-authorization-rbac/v1/basic-usage/) for more information.
62103
63104
## Usage
64105

65106
Based on the configuration file above, we have two admin roles (`superuser`, `admin`) and two user roles (`user`, `guest`).
66107

67-
Roles inherit the permissions from their parents:
108+
A permission in Dotkernel API is a **route name** — the third argument given to the route in a module's `RoutesDelegator`.
109+
To list the names you can grant, run `php ./bin/cli.php route:list`; see [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md).
110+
111+
### How inheritance works here
112+
113+
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.
114+
115+
So in the shipped configuration:
116+
117+
| Entry | Meaning |
118+
| --- | --- |
119+
| `superuser => []` | `superuser` has no parent |
120+
| `admin => [superuser]` | `superuser` is the parent of `admin`, so **`superuser` inherits everything granted to `admin`** |
121+
| `guest => [user]` | `user` is the parent of `guest`, so **`user` inherits everything granted to `guest`** |
68122

69-
- `superuser` has no parent
70-
- `admin` has `superuser` as a parent which means `superuser` also has `admin` permissions
71-
- `user` has no parent
72-
- `guest` has `user` as a parent which means `user` also has `guest` permissions
123+
That is why `superuser` needs no permissions of its own: its list is empty, yet it can reach all 23 routes granted to `admin`.
73124

74-
For each role we defined an array of permissions.
75-
A permission in Dotkernel API is basically a route name.
125+
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.
76126

77-
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.
127+
Effective totals, once inheritance is applied:
78128

79-
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.
129+
| Role | Own | Inherited | Effective |
130+
| --- | --- | --- | --- |
131+
| `superuser` | 0 | 23 from `admin` | 23 |
132+
| `admin` | 23 | — | 23 |
133+
| `user` | 6 | 11 from `guest` | 17 |
134+
| `guest` | 11 | — | 11 |
135+
136+
### How a request is authorized
137+
138+
`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`.
139+
140+
For each request it:
141+
142+
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`.
143+
An unrecognised client is rejected.
144+
2. Rejects an account that is inactive, or a user that has been deleted.
145+
3. Replaces the identity's roles with the role names read from that record.
146+
4. Calls `isGranted()` once per role and allows the request as soon as **any** role grants the route.
147+
148+
If no role grants it, the response is `403 Forbidden` with `You are not allowed to access this resource.`
149+
150+
> 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).
80151
81152
## FAQ
82153

@@ -98,15 +169,18 @@ A route with no permission entry is unreachable for that role.
98169
**Q: Which access control model is used?**
99170

100171
A: RBAC, via `mezzio-authorization-rbac` backed by `laminas-permissions-rbac`.
172+
`AuthorizationMiddleware` depends only on `Mezzio\Authorization\AuthorizationInterface`, so the adapter is selected by configuration rather than hardcoded.
101173

102174
**Q: How does role inheritance work here?**
103175

104-
A: A role listed inside another role's entry is its parent's beneficiary: because `admin` lists `superuser`, `superuser` receives everything granted to `admin`.
105-
That is why `superuser` needs no explicit permissions of its own.
176+
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.
177+
Because `admin` lists `superuser`, `superuser` receives everything granted to `admin`, which is why `superuser` needs no permissions of its own.
178+
Likewise `guest` lists `user`, so `user` inherits the guest permissions on top of its own.
106179

107180
**Q: Where are roles stored?**
108181

109-
A: Each authenticatable entity — admin or user — has its own `roles` table where its roles are defined.
182+
A: In `admin_role` and `user_role`, with `admin_roles` and `user_roles` as the join tables that assign them to accounts.
183+
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.
110184

111185
**Q: Which middleware enforces this?**
112186

@@ -116,3 +190,14 @@ See [Middleware flow](../flow/middleware-flow.md).
116190
**Q: Can I use ACL instead of RBAC?**
117191

118192
A: The ACL adapter ships with the project, but RBAC is what Dotkernel API is configured for; switching means replacing the authorization configuration.
193+
Because the middleware only knows `AuthorizationInterface`, no application code needs to change.
194+
195+
**Q: Do the permissions cover every route?**
196+
197+
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.
198+
`app::view-index` and `app::create-error-report` are the only two granted to two roles.
199+
200+
**Q: What does a rejected request look like?**
201+
202+
A: `403 Forbidden` with `You are not allowed to access this resource.`
203+
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.

0 commit comments

Comments
 (0)