rotexsoft / versatile-acl
A simple light-weight and highly customizable package that can be used for implementing access control in any php application.
Requires
- php: >=8.1.0
Requires (Dev)
- php-coveralls/php-coveralls: ^2.0
- phpunit/phpunit: ^10.0
- rector/rector: ^1.0.0
- vimeo/psalm: ^5.4
README
A simple, highly flexible and customizable access control package for PHP applications.
Installation
Via composer: (Requires PHP 7.4+ or PHP 8.0+).
composer require rotexsoft/versatile-acl
Branching
These are the branches in this repository:
- master: contains code for the latest major version of this package.
- 4.x: contains code for the 4.x version of this package. No new features, only bug fixes.
- 1.X: contains code for the 1.X version of this package. Abandoned.
Introduction
A PHP application can use this package to define Permissionable Entities (e.g. application users or groups that users can belong to).
- Each entity is an instance of \VersatileAcl\Interfaces\PermissionableEntityInterface which is implemented by \VersatileAcl\GenericPermissionableEntity in this package.
- Each entity can be associated to another entity as a parent Entity.
- Each entity can have one or more permissions defined. These are direct permissions
- A permission in this package is an object that represents whether or not an action (represented by a case-insensitive string) can be performed by an entity on a resource (represented by a case-insensitive string).
- A permission is an instance of \VersatileAcl\Interfaces\PermissionInterface which is implemented by \VersatileAcl\GenericPermission in this package.
- Each entity also inherits permissions from its parent entities.
- The package allows you to give direct permissions a higher priority than inherited permissions (the default behavior) and also allows you to do the reverse, if you so desire.
Below is an overview of the classes in this package:
Click here to see the full Class Diagram for this package.
In your applications, you will be mostly be working with instances of VersatileAcl\VersatileAcl; this class exposes most of the functionality of the underlying classes in this package listed below:
-
\VersatileAcl\GenericPermissionableEntity : Represents an entity in your application
-
\VersatileAcl\GenericPermissionableEntitiesCollection : A collection class for storing one or more entities in your application
-
\VersatileAcl\GenericPermission : Represents a permission to be assigned to an entity in your application
-
\VersatileAcl\GenericPermissionsCollection : A collection class for storing one or more permissions belonging to a particular entity in your application. It is possible to assign the same instance of this class to more than one entity, but it is recommended that you maintain separate instances of this class for each entity in your application.
Example Real-world Usage
We will be using a blog application that has a users table containing information about registered blog users (the users in this table are also authors of blog posts and commentators on blog posts in the application), a posts table and a comments table. Below is the schema for the sample application:
Below are the relationship rules for the blog application
- A user can author many posts
- A user can make one or more comments on each post
- A post can have one or more comments associated with it
Below are some access control group definitions that are relevant to this sample blog application:
NOTE: the permissions associated with the comments-owners and posts-owners will require an assertion callback that further checks that members of the group can only perform actions on the comments or posts they own (not comments and posts owned by other users).
Let's model these groups and permissions using VersatileAcl\VersatileAcl.
<?php use VersatileAcl\VersatileAcl; $groupsVaclObj = new VersatileAcl(); $groupsVaclObj ->addEntity('admin') // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | admin | all | all | yes | // Permission below will allow an entity whose ID // is 'admin' to perform any action on any resource // in an application ->addPermission( 'admin', \VersatileAcl\GenericPermission::getAllActionsIdentifier(), \VersatileAcl\GenericPermission::getAllResourcesIdentifier(), true ); $groupsVaclObj ->addEntity('comments-moderators') // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | comments-moderators | comment | approve | yes | // Permission below allows an entity whose ID is // 'comments-moderators' to approve comments made // on a blog post ->addPermission('comments-moderators', 'approve', 'comment', true) // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | comments-moderators | comment | delete | yes | // Permission below allows an entity whose ID is // 'comments-moderators' to delete comments made // on a blog post ->addPermission('comments-moderators', 'delete', 'comment', true); $groupsVaclObj ->addEntity('posts-moderators') // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | posts-moderators | post | approve | yes | // Permission below allows an entity whose ID is // 'posts-moderators' to approve any blog post // created in your application ->addPermission('posts-moderators','approve', 'post', true) // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | posts-moderators | post | delete | yes | // Permission below allows an entity whose ID is // 'posts-moderators' to delete any blog post // created in your application ->addPermission('posts-moderators','delete', 'post', true); // We will create an owners group entity that will // contain permissions for the comments-owners and // the posts-owners groups $groupsVaclObj ->addEntity('owners') // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | comments-owners | comment | all | yes | // Permission below allows an entity to both // approve and delete comments made on blog // posts created by the entity whose ID is // 'owners' ->addPermission( 'owners', \VersatileAcl\GenericPermission::getAllActionsIdentifier(), 'comment', true, function(array $userRecord=[], array $commentRecord=[]){ return isset($userRecord['id']) && isset($commentRecord['commenter_id']) && $userRecord['id'] === $commentRecord['commenter_id']; } ) // | Group Name | Resource | Action | Allowed | // |---------------------|----------|---------|---------| // | posts-owners | post | all | yes | // Permission below allows an entity to both // approve and delete blog posts created by // the entity whose ID is 'owners' ->addPermission( 'owners', \VersatileAcl\GenericPermission::getAllActionsIdentifier(), 'post', true, function(array $userRecord=[], array $blogPostRecord=[]){ return isset($userRecord['id']) && isset($blogPostRecord['creators_id']) && $userRecord['id'] === $blogPostRecord['creators_id']; } );
NOTE: \VersatileAcl\GenericPermission::getAllActionsIdentifier() is a special string that represents all actions any entity can perform on each resource in your application.
NOTE: \VersatileAcl\GenericPermission::getAllResourcesIdentifier() is a special string that represents all available resources in your application.
Now that we have created entity objects for each group and added the necessary permissions to the appropriate entity objects, we are ready to go ahead with defining the entity objects that will represent the users below in the blog application.
Below is a list of userids of users in the application
- frankwhite
- ginawhite
- johndoe
- janedoe
- jackbauer
- jillbauer
Let's create and register entity objects for each user in our VersatileAcl object:
<?php $usersVaclObj = new VersatileAcl(); $usersVaclObj->addEntity('frankwhite') ->addEntity('ginawhite') ->addEntity('johndoe') ->addEntity('janedoe') ->addEntity('jackbauer') ->addEntity('jillbauer');
Below are the group membership definitions:
Let's model these relationships by adding the appropriate entity objects representing the groups as parent entities to the respective user entity objects:
<?php // add 'frankwhite' to the admin group $usersVaclObj->getEntity('frankwhite') ->addParent( $groupsVaclObj->getEntity('admin') ); // add 'ginawhite' to the comments-moderators group $usersVaclObj->getEntity('ginawhite') ->addParent( $groupsVaclObj->getEntity('comments-moderators') ); // add 'johndoe' to the comments-moderators group $usersVaclObj->getEntity('johndoe') ->addParent( $groupsVaclObj->getEntity('comments-moderators') ); // add 'janedoe' to the posts-moderators group $usersVaclObj->getEntity('janedoe') ->addParent( $groupsVaclObj->getEntity('posts-moderators') ); // Now let's model the two group memberships // below for each user // | Group | User | // |---------------------|-----------| // | comments-owners | all | // | posts-owners | all | $usersVaclObj->getEntity('frankwhite') ->addParent( $groupsVaclObj->getEntity('owners') ); // frankwhite's membership in the admin group // already grants him permission to perform any // action on any resource, so this membership is // redundant for him $usersVaclObj->getEntity('ginawhite') ->addParent( $groupsVaclObj->getEntity('owners') ); $usersVaclObj->getEntity('johndoe') ->addParent( $groupsVaclObj->getEntity('owners') ); $usersVaclObj->getEntity('janedoe') ->addParent( $groupsVaclObj->getEntity('owners') ); $usersVaclObj->getEntity('jackbauer') ->addParent( $groupsVaclObj->getEntity('owners') ); $usersVaclObj->getEntity('jillbauer') ->addParent( $groupsVaclObj->getEntity('owners') );
Now that we have set up our groups, users and permissions, let's see how to check if a user is allowed to perform an action on a resource in our application.
Let's start with the user 'frankwhite' that belongs to the 'admin' group. This user should be able to perform any action on any resource in the application:
<?php var_dump( $usersVaclObj->isAllowed('frankwhite', 'approve', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('frankwhite', 'delete', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('frankwhite', 'approve', 'post') ); // === true var_dump( $usersVaclObj->isAllowed('frankwhite', 'delete', 'post') ); // === true
Now let's continue with the user 'ginawhite' that belongs to the 'comments-moderators' group. This user should be able to only approve and delete comments in the application (the user should also be able to approve and delete posts they have created):
<?php var_dump( $usersVaclObj->isAllowed('ginawhite', 'approve', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('ginawhite', 'delete', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('ginawhite', 'approve', 'post') ); // === false var_dump( $usersVaclObj->isAllowed('ginawhite', 'delete', 'post') ); // === false // Assuming we have the post record below and the user record for 'ginawhite' below $postRecord = [ 'id' => 2, 'body' => 'Some random post', 'creators_id' => 'ginawhite', 'last_updaters_id' => 'ginawhite', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $userRecord = [ 'id' => 'ginawhite', 'password' => 'TydlfEUSqnVMu' ]; // Here's how we would check if 'ginawhite' can approve and delete posts she has created var_dump( $usersVaclObj->isAllowed( 'ginawhite', 'approve', 'post', null, $userRecord, $postRecord ) ); // === true var_dump( $usersVaclObj->isAllowed( 'ginawhite', 'delete', 'post', null, $userRecord, $postRecord ) ); // === true
Now let's continue with the user 'johndoe' that belongs to the 'comments-moderators' group. This user should be able to only approve and delete comments in the application (the user should also be able to approve and delete posts they have created):
<?php var_dump( $usersVaclObj->isAllowed('johndoe', 'approve', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('johndoe', 'delete', 'comment') ); // === true var_dump( $usersVaclObj->isAllowed('johndoe', 'approve', 'post') ); // === false var_dump( $usersVaclObj->isAllowed('johndoe', 'delete', 'post') ); // === false // Assuming we have the post record below and the user record for 'johndoe' below $postRecord2 = [ 'id' => 2, 'body' => 'Some random post', 'creators_id' => 'johndoe', 'last_updaters_id' => 'johndoe', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $userRecord2 = [ 'id' => 'johndoe', 'password' => 'TydlfEUSqnVMu' ]; var_dump( $usersVaclObj->isAllowed( 'johndoe', 'approve', 'post', null, $userRecord2, $postRecord2 ) ); // === true var_dump( $usersVaclObj->isAllowed( 'johndoe', 'delete', 'post', null, $userRecord2, $postRecord2 ) ); // === true
Now let's continue with the user 'janedoe' that belongs to the 'posts-moderators' group. This user should be able to only approve and delete posts in the application (the user should also be able to approve and delete comments they have created):
<?php var_dump( $usersVaclObj->isAllowed('janedoe', 'approve', 'comment') ); // === false var_dump( $usersVaclObj->isAllowed('janedoe', 'delete', 'comment') ); // === false var_dump( $usersVaclObj->isAllowed('janedoe', 'approve', 'post') ); // === true var_dump( $usersVaclObj->isAllowed('janedoe', 'delete', 'post') ); // === true // Assuming we have the comment record below and the user record for 'janedoe' below $commentRecord3 = [ 'id' => 1, 'post_id' => 2, 'commenter_id' => 'janedoe', 'comment' => 'Some random comment', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $userRecord3 = [ 'id' => 'janedoe', 'password' => 'TydlfEUSqnVMu' ]; var_dump( $usersVaclObj->isAllowed( 'janedoe', 'approve', 'comment', null, $userRecord3, $commentRecord3 ) ); // === true var_dump( $usersVaclObj->isAllowed( 'janedoe', 'delete', 'comment', null, $userRecord3, $commentRecord3 ) ); // === true
Now let's continue with the user 'jackbauer' that only belongs to the 'owners' group. This user should be able to ONLY approve and delete comments and posts they have created in the application:
<?php // all comments including those not owned by jackbauer var_dump( $usersVaclObj->isAllowed('jackbauer', 'approve', 'comment') ); // === false var_dump( $usersVaclObj->isAllowed('jackbauer', 'delete', 'comment') ); // === false // all posts including those not owned by jackbauer var_dump( $usersVaclObj->isAllowed('jackbauer', 'approve', 'post') ); // === false var_dump( $usersVaclObj->isAllowed('jackbauer', 'delete', 'post') ); // === false // Assuming we have the post and comment records below and the user record for 'jackbauer' below $commentRecord5 = [ 'id' => 1, 'post_id' => 2, 'commenter_id' => 'jackbauer', 'comment' => 'Some random comment', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $postRecord5 = [ 'id' => 2, 'body' => 'Some random post', 'creators_id' => 'jackbauer', 'last_updaters_id' => 'jackbauer', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $userRecord5 = [ 'id' => 'jackbauer', 'password' => 'TydlfEUSqnVMu' ]; // comment owned by jackbauer var_dump( $usersVaclObj->isAllowed( 'jackbauer', 'approve', 'comment', null, $userRecord5, $commentRecord5 ) ); // === true // comment owned by jackbauer var_dump( $usersVaclObj->isAllowed( 'jackbauer', 'delete', 'comment', null, $userRecord5, $commentRecord5 ) ); // === true // post owned by jackbauer var_dump( $usersVaclObj->isAllowed( 'jackbauer', 'approve', 'post', null, $userRecord5, $postRecord5 ) ); // === true // post owned by jackbauer var_dump( $usersVaclObj->isAllowed( 'jackbauer', 'delete', 'post', null, $userRecord5, $postRecord5 ) ); // === true
Finally, let's test the user 'jillbauer' that also only belongs to the 'owners' group. This user should be able to ONLY approve and delete comments and posts they have created in the application:
<?php // all comments including those not owned by jillbauer var_dump( $usersVaclObj->isAllowed('jillbauer', 'approve', 'comment') ); // === false var_dump( $usersVaclObj->isAllowed('jillbauer', 'delete', 'comment') ); // === false // all posts including those not owned by jillbauer var_dump( $usersVaclObj->isAllowed('jillbauer', 'approve', 'post') ); // === false var_dump( $usersVaclObj->isAllowed('jillbauer', 'delete', 'post') ); // === false // Assuming we have the post and comment records below and the user record for 'jackbauer' below $commentRecord6 = [ 'id' => 1, 'post_id' => 2, 'commenter_id' => 'jillbauer', 'comment' => 'Some random comment', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $postRecord6 = [ 'id' => 2, 'body' => 'Some random post', 'creators_id' => 'jillbauer', 'last_updaters_id' => 'jillbauer', 'date_created' => '2019-08-01 13:43:21', 'last_updated' => '2019-08-01 13:43:21', 'is_approved' => '0', ]; $userRecord6 = [ 'id' => 'jillbauer', 'password' => 'TydlfEUSqnVMu' ]; // comment owned by jillbauer var_dump( $usersVaclObj->isAllowed( 'jillbauer', 'approve', 'comment', null, $userRecord6, $commentRecord6 ) ); // === true // comment owned by jillbauer var_dump( $usersVaclObj->isAllowed( 'jillbauer', 'delete', 'comment', null, $userRecord6, $commentRecord6 ) ); // === true // post owned by jillbauer var_dump( $usersVaclObj->isAllowed( 'jillbauer', 'approve', 'post', null, $userRecord6, $postRecord6 ) ); // === true // post owned by jillbauer var_dump( $usersVaclObj->isAllowed( 'jillbauer', 'delete', 'post', null, $userRecord6, $postRecord6 ) ); // === true
NOTE: You can override the $additionalAssertions callable supplied to VersatileAcl::addPermission(...) by passing another callback as the fourth argument to VersatileAcl::isAllowed(...). In all the examples above, we passed null as the fourth argument to VersatileAcl::isAllowed(...), meaning that we want the $additionalAssertions callable supplied to VersatileAcl::addPermission(...) to be used instead (if present).
This is just an example of how this package can be used to enforce access control in an application. You can obviously come up with other more creative ways to adapt this package to your specific use case.
Study the class diagram for this package to get a better understanding of how the various classes interact.
Debugging
You can also get information about what's going under the hood of any instance of VersatileAcl\VersatileAcl by calling VersatileAcl::enableAuditTrail(bool $canAudit=true) with a value of true and then make calls to other methods on the instance of VersatileAcl\VersatileAcl and finally echo the contents of the string returned by VersatileAcl::getAuditTrail().
You can increase or decrease the amount of information returned by VersatileAcl::getAuditTrail() by calling VersatileAcl::enableVerboseAudit(bool $performVerboseAudit=true) with a value of true or false.
Finally, you can clear / empty / reset the string returned by VersatileAcl::getAuditTrail() by calling VersatileAcl::clearAuditTrail()
Here's an example below:
<?php $sAcl = new VersatileAcl(); // enable logging of internal activities $sAcl->enableAuditTrail(true); // enable verbose logging of internal activities $sAcl->enableVerboseAudit(true); // call some methods $sAcl->addEntity('jblow'); $sAcl->removeEntity('jblow'); echo 'Outputing verbose Audit Trail: ' . PHP_EOL . PHP_EOL; echo $sAcl->getAuditTrail(); // Clear the contents of the audit trail $sAcl->clearAuditTrail(); // disable verbose logging of internal activities $sAcl->enableVerboseAudit(false); // call some methods $sAcl->addEntity('jblow'); $sAcl->removeEntity('jblow'); echo 'Outputing non-verbose Audit Trail: ' . PHP_EOL . PHP_EOL; echo $sAcl->getAuditTrail();
Produces the output below:
Outputing verbose Audit Trail:
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::addEntity('jblow') trying to create and add a new entity whose ID will be `jblow`
[2020-12-04 13:51:06]: Entity created
[2020-12-04 13:51:06]: Successfully added the following entity:
VersatileAcl\GenericPermissionableEntity (0000000038640cc1000000007974da68)
{
id: `jblow`
parentEntities:
VersatileAcl\GenericPermissionableEntitiesCollection (0000000038640cdf000000007974da68)
{
}
permissions:
VersatileAcl\GenericPermissionsCollection (0000000038640cc0000000007974da68)
{
}
}
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::addEntity('jblow')
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::removeEntity('jblow') trying to remove the entity whose ID is `jblow`
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::getEntity('jblow') trying to retrieve the entity whose ID is `jblow`
[2020-12-04 13:51:06]: Retrieved the following item: VersatileAcl\GenericPermissionableEntity (0000000038640cc1000000007974da68)
{
id: `jblow`
parentEntities:
VersatileAcl\GenericPermissionableEntitiesCollection (0000000038640cdf000000007974da68)
{
}
permissions:
VersatileAcl\GenericPermissionsCollection (0000000038640cc0000000007974da68)
{
}
}
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::getEntity('jblow') with a return type of `object` that is an instance of `VersatileAcl\GenericPermissionableEntity` with the following string rep
resentation:
VersatileAcl\GenericPermissionableEntity (0000000038640cc1000000007974da68)
{
id: `jblow`
parentEntities:
VersatileAcl\GenericPermissionableEntitiesCollection (0000000038640cdf000000007974da68)
{
}
permissions:
VersatileAcl\GenericPermissionsCollection (0000000038640cc0000000007974da68)
{
}
}
[2020-12-04 13:51:06]: Successfully removed the entity whose ID is `jblow`.
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::removeEntity('jblow') with a return type of `object` that is an instance of `VersatileAcl\GenericPermissionableEntity` with the following string representation:
VersatileAcl\GenericPermissionableEntity (0000000038640cc1000000007974da68)
{
id: `jblow`
parentEntities:
VersatileAcl\GenericPermissionableEntitiesCollection (0000000038640cdf000000007974da68)
{
}
permissions:
VersatileAcl\GenericPermissionsCollection (0000000038640cc0000000007974da68)
{
}
}
Outputing non-verbose Audit Trail:
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::addEntity('jblow')
[2020-12-04 13:51:06]: Entity created
[2020-12-04 13:51:06]: Successfully added the entity whose ID is `jblow`
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::addEntity('jblow')
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::removeEntity('jblow')
[2020-12-04 13:51:06]: Entered VersatileAcl\VersatileAcl::getEntity('jblow')
[2020-12-04 13:51:06]: Successfully retrieved the desired entity.
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::getEntity('jblow')
[2020-12-04 13:51:06]: Successfully removed the entity whose ID is `jblow`.
[2020-12-04 13:51:06]: Exiting VersatileAcl\VersatileAcl::removeEntity('jblow')