mimmi20 / mezzio-generic-authorization-rbac
Provides a laminas-permissions-rbac adapter for mezzio-generic-authorization.
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- laminas/laminas-permissions-rbac: ^3.7.0
- mimmi20/mezzio-generic-authorization: ^3.0.7
- psr/container: ^1.1.2 || ^2.0.2
- psr/http-message: ^1.0.1 || ^2.0
Requires (Dev)
- ext-ctype: *
- ext-dom: *
- ext-simplexml: *
- ext-tokenizer: *
- ext-xml: *
- ext-xmlwriter: *
- infection/infection: ^0.29.8
- laminas/laminas-servicemanager: ^4.3.0
- mimmi20/coding-standard: ^6.0.0
- nikic/php-parser: ^5.3.1
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^1.12.9
- phpstan/phpstan-deprecation-rules: ^1.2.1
- phpstan/phpstan-phpunit: ^1.4.0
- phpunit/phpunit: ^11.4.4
- rector/rector: ^1.2.10
- rector/type-perfect: ^1.0.0
- symfony/process: ^7.2.0
- symplify/phpstan-rules: ^13.0.1
- tomasvotruba/cognitive-complexity: ^0.2.3
- tomasvotruba/type-coverage: ^1.0.0
- tomasvotruba/unused-public: ^1.0.0
Suggests
- laminas/laminas-servicemanager: to use the factories
Conflicts
README
Code Status
This library provides a laminas-rbac adapter for mezzio-generic-authorization.
Installation
You can install the mezzio-generic-authorization-rbac library with Composer:
composer require mimmi20/mezzio-generic-authorization-rbac
Introduction
This component provides Role-Based Access Control (RBAC) authorization abstraction for the mezzio-generic-authorization library.
RBAC is based on the idea of roles. In a web application, users have an identity (e.g. username, email, etc). Each identified user then has one or more roles (e.g. admin, editor, guest). Each role has a permission to perform one or more actions (e.g. access an URL, execute specific web API calls).
In a typical RBAC system:
- An identity has one or more roles.
- A role requests access to a permission.
- A permission is given to a role.
Thus, RBAC has the following model:
- Many-to-many relationship between identities and roles.
- Many-to-many relationship between roles and permissions.
- Roles can have a parent role.
The first requirement for an RBAC system is identities. In our scenario, the
users are generated by an authentication system, provided by
mezzio-authentication.
That library provides a PSR-7 request attribute named
Mezzio\Authentication\UserInterface
when a user is authenticated.
The RBAC system uses this instance to get information about the user's identity.
Configure an RBAC system
You can configure your RBAC using a configuration file, as follows:
// config/autoload/authorization.local.php return [ // ... 'mezzio-authorization-rbac' => [ 'roles' => [ 'administrator' => [], 'editor' => ['administrator'], 'contributor' => ['editor'], ], 'permissions' => [ 'contributor' => [ 'admin.dashboard', 'admin.posts', ], 'editor' => [ 'admin.publish', ], 'administrator' => [ 'admin.settings', ], ], ], ];
In the above example, we designed an RBAC system with 3 roles: administator
,
editor
, and contributor
. We defined a hierarchy of roles as follows:
administrator
has no parent role.editor
hasadministrator
as a parent. That meansadministrator
inherits the permissions of theeditor
.contributor
haseditor
as a parent. That meanseditor
inherits the permissions ofcontributor
, and following the chain,administator
inherits the permissions ofcontributor
.
For each role, we specified an array of permissions. As you can notice, a
permission is just a string; it can represent anything. In our implementation,
this string represents a route name. That means the contributor
role can
access the routes admin.dashboard
and admin.posts
but cannot access the
routes admin.publish
(assigned to editor
role) and admin.settings
(assigned to administrator
).
If you want to change the authorization logic for each permission, you can write
your own Mimmi20\Mezzio\GenericAuthorization\AuthorizationInterface
implementation.
That interface defines the following method:
public function isGranted(string $role, string $resource, ?string $privilege = null, ?\Psr\Http\Message\ServerRequestInterface\ServerRequestInterface $request = null): bool;
where $role
is the role, $resource
is the resource, $privilege
is an privilege and $request
is the PSR-7 HTTP request to authorize.
This library uses the laminas/laminas-permissions-rbac library to implement the RBAC system. Privileges are not supported in this RBAC implementation. If you want to know more about the usage of this library, read the blog post Manage permissions with laminas-permissions-rbac.
Dynamic Assertion
In some cases you will need to authorize a role based on a specific HTTP request. For instance, imagine that you have an "editor" role that can add/update/delete a page in a Content Management System (CMS). We want to prevent an "editor" from modifying pages they have not created.
These types of authorization are called dynamic assertions
and are implemented via the Laminas\Permissions\Rbac\AssertionInterface
of
laminas-permissions-rbac.
In order to use it, this package provides LaminasRbacAssertionInterface
,
which extends Laminas\Permissions\Rbac\AssertionInterface
:
namespace Mezzio\Authorization\Rbac; use Psr\Http\Message\ServerRequestInterface; use Laminas\Permissions\Rbac\AssertionInterface; interface LaminasRbacAssertionInterface extends AssertionInterface { public function setRequest(ServerRequestInterface $request) : void; }
The Laminas\Permissions\Rbac\AssertionInterface
defines the following:
namespace Laminas\Permissions\Rbac; interface AssertionInterface { public function assert(Rbac $rbac, RoleInterface $role, string $permission) : bool; }
Going back to our use case, we can build a class to manage the "editor" authorization requirements, as follows:
use Mimmi20\Mezzio\GenericAuthorization\Rbac\LaminasRbacAssertionInterface; use App\Service\Article; use Laminas\Permissions\Rbac\Rbac; use Laminas\Permissions\Rbac\RoleInterface; use Psr\Http\Message\ServerRequestInterface; class EditorAuth implements LaminasRbacAssertionInterface { public function __construct(Article $article) { $this->article = $article; } public function setRequest(ServerRequestInterface $request): void { $this->request = $request; } public function assert(Rbac $rbac, RoleInterface $role, string $permission): bool { $user = $this->request->getAttribute(UserInterface::class, false); return $this->article->isUserOwner($user->getIdentity(), $this->request); } }
Where Article
is a class that checks if the identified user is the owner of
the article referenced in the HTTP request.
If you manage articles using a SQL database, the implementation of
isUserOwner()
might look like the following:
public function isUserOwner(string $identity, ServerRequestInterface $request): bool { // get the article {article_id} attribute specified in the route $url = $request->getAttribute('article_id', false); if (! $url) { return false; } $sth = $this->pdo->prepare( 'SELECT * FROM article WHERE url = :url AND owner = :identity' ); $sth->bindParam(':url', $url); $sth->bindParam(':identity', $identity); if (! $sth->execute()) { return false; } $row = $sth->fetch(); return ! empty($row); }
To pass the Article
dependency to your assertion, you can use a Factory class
that generates the EditorAuth
class instance, as follows:
use App\Service\Article; class EditorAuthFactory { public function __invoke(ContainerInterface $container) : EditorAuth { return new EditorAuth( $container->get(Article::class) ); } }
And configure the service container to use EditorAuthFactory
to point to
EditorAuth
, using the following configuration:
return [ 'dependencies' => [ 'factories' => [ // ... EditorAuth::class => EditorAuthFactory::class ] ] ];
License
This package is licensed using the MIT License.
Please have a look at LICENSE.md
.