Authorization

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. Inheritance runs from child to parent, so superuser inherits every permission granted to admin without declaring any of its own.

Details

Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource.

Dotkernel API's implementation of authorization uses Mezzio\Authorization\Rbac\LaminasRbac as a model of Role-Based Access Control (RBAC).

How it works

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.

Configuration

Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration.

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.

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,
            ],
        ],
        '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 for more information.

Usage

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.

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:

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

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.

Effective totals, once inheritance is applied:

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.

FAQ

Q: How does authorization differ from authentication?

A: Authentication establishes who the caller is; authorization checks what that established identity is allowed to do. See Authentication.

Q: What exactly is a permission in Dotkernel API?

A: A route name. Granting a role a permission means granting it access to the route of that name.

Q: Where do I add permissions for a route I just created?

A: To the relevant role's array in config/autoload/authorization.global.php. 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: 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: 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?

A: Api\App\Middleware\AuthorizationMiddleware. See Middleware flow.

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.