tobento / service-acl
A simple role and user-level access control system.
Requires
- php: >=8.0
- psr/container: ^1.0 || ^2.0
- tobento/service-helper-function: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- vimeo/psalm: ^4.0
README
The ACL Service is a simple role and user-level access control system.
Table of Contents
Getting started
Add the latest version of the Acl service running this command.
composer require tobento/service-acl
Requirements
- PHP 8.0 or greater
Highlights
- Framework-agnostic, will work with any project
- Customize permission behaviour
- Decoupled design
Simple Example
Here is a simple example of how to use the Acl service.
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\Authorizable; use Tobento\Service\Acl\AuthorizableAware; use Tobento\Service\Acl\Role; // User class example. class User implements Authorizable { use AuthorizableAware; public function __construct( protected string $name, ) {} } // Create Acl. $acl = new Acl(); // Adding rules. $acl->rule('articles.read') ->title('Article Read') ->description('If a user can read articles'); $acl->rule('articles.create'); $acl->rule('articles.update'); // Create role. $guestRole = new Role('guest'); // Adding permissions on role. $guestRole->addPermissions(['articles.read']); // Create and set user role. $user = (new User('Nick'))->setRole($guestRole); // Adding permissions on user. // If permissions are set on user, role permissions will not count anymore. $user->addPermissions(['articles.read']); // Set current user. $acl->setCurrentUser($user); // Adding additional permissions for the current user only. $acl->addPermissions(['articles.create']); // Check permissions for current user. if ($acl->can('articles.read')) { // user has permission to read articles. } // check permission for specific user. if ($acl->cant(key: 'articles.read', user: $user)) { // user has not permission to read articles. }
Documentation
Rules
Adding and getting rules.
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\RuleInterface; // Create Acl. $acl = new Acl(); // Add default rule. $acl->rule('articles.read'); // Add custom rule. $acl->addRule(RuleInterface $rule); // Get rules. foreach($acl->getRules() as $rule) { $key = $rule->getKey(); $inputKey = $rule->getInputKey(); $title = $rule->getTitle(); $description = $rule->getDescription(); $area = $rule->getArea(); } // get specific rules $rule = $acl->getRule('articles.read');
Default Rule
The default rule has the following permission behaviour:
use Tobento\Service\Acl\Acl; // Create Acl. $acl = new Acl(); $acl->rule('articles.read'); $acl->rule('articles.update'); // Create role. $role = new Role('guest'); // Create and set user role. $user = (new User('Nick'))->setRole($role); // Adding permissions on acl, only for current user. $acl->addPermissions(['articles.read']); // Adding permissions on role. $role->addPermissions(['articles.read']); // Adding permissions on user. // If permissions are set on user, role permissions will not count anymore. // Only acl and user specific permissions. $user->addPermissions(['articles.read']);
Areas bahviour:
use Tobento\Service\Acl\Acl; // Create Acl. $acl = new Acl(); $acl->rule('articles.read', 'frontend'); $acl->rule('articles.update', 'backend'); // Guest Role taking only frontend rules into account, // ignoring any permission from backend rules even if permission is given. $role = new Role('guest', ['frontend']); // Editor can have frontend and backend rules. $role = new Role('editor', ['frontend', 'backend']);
Default Rule Custom
You can easily add a custom handler for extending a specific rule behaviour.
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\Authorizable; use Tobento\Service\Acl\AuthorizableAware; use Tobento\Service\Acl\Role; // User class example. class User implements Authorizable { use AuthorizableAware; public function __construct( protected string $name, ) {} } // Article class example class Article { public function __construct( protected string $name, protected array $roles = [], protected null|Authorizable $user = null ) {} public function getName(): string { return $this->name; } public function getUser(): null|Authorizable { return $this->user; } public function getRoles(): array { return $this->roles; } } // Create Acl. $acl = new Acl(); // Rule to check if user is allowed to access a specific resource. $acl->rule('resource') ->needsPermission(false) ->handler(function(Authorizable $user, null|Authorizable $resourceUser): bool { if (is_null($resourceUser)) { return false; } return $user === $resourceUser; }); // Rule to check if user has role for a specific resource. $acl->rule('has_role') ->needsPermission(false) ->handler(function(Authorizable $user, array $roles = []) { if (empty($roles)) { return true; } return in_array($user->role()->key(), $roles); }); $user = (new User('Nick'))->setRole(new Role('editor')); $acl->setCurrentUser($user); $article = new Article('About us', ['editor'], $user); // Check resource access. if ($acl->can('resource', [$article->getUser()])) { // user can access about page. } // Check resource role access. if ($acl->can('has_role', [$article->getRoles()])) { // user has the right role to access this resource. }
Custom Rules
You can easily add a custom rule for a different permission strategy.
Your Rule must implement the following RuleInterface.
/** * RuleInterface */ interface RuleInterface { /** * Get the key. * * @return string The key such as 'user.create'. */ public function getKey(): string; /** * Get the input key. May be used for form input. * * @return string The key such as 'user_create'. */ public function getInputKey(): string; /** * Get the title. * * @return string The title */ public function getTitle(): string; /** * Get the description. * * @return string The description */ public function getDescription(): string; /** * Get the area. * * @return string */ public function getArea(): string; /** * If the rule requires permissions to match the rule. * * @return bool */ public function requiresPermission(): bool; /** * Return if the rule matches the criteria. * * @param AclInterface * @param string A permission key 'user.create'. * @param array Any parameters for custom handler * @param null|Authorizable * @return bool True if rule matches, otherwise false */ public function matches( AclInterface $acl, string $key, array $parameters = [], ?Authorizable $user = null ): bool; }
Lets make a custum rule for just letting user specific permissions have access ignoring acl and role permissions.
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\AclInterface; use Tobento\Service\Acl\RuleInterface; use Tobento\Service\Acl\Authorizable; use Tobento\Service\Acl\AuthorizableAware; use Tobento\Service\Acl\Role; // Custom rule class CustomRule implements RuleInterface { public function __construct( protected string $key, protected string $area, ) {} public function getKey(): string { return $this->key; } public function getInputKey(): string { return $this->key; } public function getTitle(): string { return $this->key; } public function getDescription(): string { return ''; } public function getArea(): string { return $this->area; } public function requiresPermission(): bool { return true; } public function matches( AclInterface $acl, string $key, array $parameters = [], ?Authorizable $user = null ): bool { $user = $user ?: $acl->getCurrentUser(); // not user at all if (is_null($user)) { return false; } // user needs a role. if (! $user->hasRole()) { return false; } // collect only user permissions. if (!$user->hasPermissions()) { return false; } $permissions = $user->getPermissions(); // permission check if (!in_array($key, $permissions)) { return false; } // area check if (!in_array($this->getArea(), $user->role()->areas())) { return false; } return true; } } // User class example. class User implements Authorizable { use AuthorizableAware; public function __construct( protected string $name, ) {} } // Create Acl. $acl = new Acl(); // Adding default rules. $acl->addRule(new CustomRule('articles.read', 'frontend')); $acl->addRule(new CustomRule('articles.create', 'frontend')); // Create role. $role = new Role('guest'); // Adding permissions on role does has no effect. $role->addPermissions(['articles.read']); // Create and set user role. $user = (new User('Nick'))->setRole($role); $user->addPermissions(['articles.create']); // Set current user. $acl->setCurrentUser($user); if ($acl->can('articles.create')) { // user has permission to read articles. }
Permissions
The following methods are available on objects implementing the Permissionable Interface or the Authorizable Interface.
- Tobento\Service\Acl\Acl::class
- Tobento\Service\Acl\Role::class
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\Permissionable; use Tobento\Service\Acl\Authorizable; // Create Acl. $acl = new Acl(); // Set all permissions. $acl->setPermissions(['user.create', 'user.update']); // Adding more permissions. $acl->addPermissions(['user.delete']); $permissions = $acl->getPermissions(); // ['user.create', 'user.update', 'user.delete'] // Has any permissions at all. $hasPermissions = $acl->hasPermissions(); // Removing permissions. $acl->removePermissions(['user.delete']); // Has specific permission. $hasPermission = $acl->hasPermission('user.update');
Available methods for checking permissions on acl:
use Tobento\Service\Acl\Acl; // Create Acl. $acl = new Acl(); // Check permissions for current user. if ($acl->can('articles.read')) { // user has permission to read articles. } // Check permission for specific user. if ($acl->cant(key: 'articles.read', user: $user)) { // user has not permission to read articles. } // You can check multiple permissions too. if ($acl->can('articles.create|articles.update')) { // user has permission to create and update articles. } // Multiple permissions with parameters. if ($acl->can('articles.create|resource', ['resource' => [$article->getUser()]])) { // user has permission to create and access the specific article. }
Checking permissions on Authorizable object:
For more information on Helper Function visit tobento/helper-function
use Tobento\Service\HelperFunction\Functions; use Psr\Container\ContainerInterface; use Tobento\Service\Di\Container; use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\AclInterface; use Tobento\Service\Acl\Authorizable; use Tobento\Service\Acl\AuthorizableAware; use Tobento\Service\Acl\Role; // create container. $container = new Container(); // Set up Helper Function acl() for supporting // checking permission directly on Authorizable objects. $functions = new Functions(); $functions->set(ContainerInterface::class, $container); // Register Acl functions. $functions->register('dir/to/acl/functions.php'); // User class example. class User implements Authorizable { use AuthorizableAware; public function __construct( protected string $name, ) {} } // Create Acl. $acl = new Acl(); // Add Acl to container. $container->set(AclInterface::class, $acl); // Adding rules. $acl->rule('articles.read'); // Create role. $guestRole = new Role('guest'); // Adding permissions on role. $guestRole->addPermissions(['articles.read']); // Create and set user role. $user = (new User('Nick'))->setRole($guestRole); // Check permissions on user. if ($user->can('articles.read')) { // user has permission to read articles. } // check permission for specific user. if ($user->cant('articles.read')) { // user has not permission to read articles. }
Roles
Working with roles.
use Tobento\Service\Acl\Acl; use Tobento\Service\Acl\Role; use Tobento\Service\Acl\RoleInterface; use Tobento\Service\Acl\Roles; use Tobento\Service\Acl\RolesInterface; // Create Acl. $acl = new Acl(); // Set roles on acl for later reusage if needed. $acl->setRoles([ new Role('guest'), new Role('editor'), ]); // or $acl->setRoles(new Roles( new Role('guest'), new Role('editor'), )); // Get roles: $roles = $acl->roles(); var_dump($roles instanceof RolesInterface); // bool(true) // Iterate roles: foreach($acl->roles() as $role) { $key = $role->key(); $active = $role->active(); $areas = $role->areas(); $name = $role->name(); } // Get Specific role: $role = $roles->get('editor'); $role = $acl->getRole('editor'); // or // null|RoleInterface // Check if role exists: var_dump($roles->has('editor')); var_dump($acl->hasRole('editor')); // or // bool(true) // Sort roles returning a new instance: $roles = $roles->sort(fn(RoleInterface $a, RoleInterface $b): int => $a->name() <=> $b->name()); // Filter roles returning a new instance: $roles = $roles->filter(fn(RoleInterface $role): bool => $role->active()); // Filter by area roles returning a new instance: $roles = $roles->area('frontend'); // Filter (in)active roles returning a new instance: $roles = $roles->active(); $roles = $roles->active(false); // Returns a new instance only the roles specified: $roles = $roles->only(['editor']); // Returns a new instance except the roles specified: $roles = $roles->except(['editor']); // Add a role returning a new instance: $roles = $roles->add(new Role('admin')); // Remove a role returning a new instance: $roles = $roles->remove('editor'); // Get first role: $role = $roles->first(); // null|RoleInterface // Get all roles: $roles = $roles->all(); $roles = $acl->getRoles(); // or // array<string, RoleInterface>