rotexsoft / versatile-acl
一个简单、轻量级且高度可定制的包,可用于实现任何PHP应用程序的访问控制。
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
适用于PHP应用程序的简单、高度灵活和可定制的访问控制包。
安装
通过composer安装:(需要PHP 7.4+或PHP 8.0+)。
composer require rotexsoft/versatile-acl
分支
以下为本存储库中的分支
- master:包含本包最新主要版本的代码。
- 4.x:包含本包4.x版本的代码。没有新功能,只有错误修复。
- 1.X:包含本包1.X版本的代码。已弃用。
简介
PHP应用程序可以使用此包来定义<强>可授权实体强>(例如:应用程序用户或用户可以属于的组)。
- 每个实体都是<强>\VersatileAcl\Interfaces\PermissionableEntityInterface强>的实例,该接口在本包中由<强>\VersatileAcl\GenericPermissionableEntity强>实现。
- 每个实体都可以作为父实体与另一个实体关联。
- 每个实体可以定义一个或多个权限。这些是<强>直接权限强>
- 在本包中,<强>权限强>是一个对象,表示一个实体是否可以执行一个<强>操作强>(由一个不区分大小写的字符串表示)在<强>资源强>上(由一个不区分大小写的字符串表示)。
- 权限是<强>\VersatileAcl\Interfaces\PermissionInterface强>的实例,该接口在本包中由<强>\VersatileAcl\GenericPermission强>实现。
- 每个实体也继承自其父实体的权限。
- 该包允许您将直接权限的优先级高于继承权限(默认行为),同时也允许您根据需要执行相反的操作。
以下是对本包中类的概述
点击这里查看本包的完整类图。
在您的应用程序中,您将主要与<强>VersatileAcl\VersatileAcl强>的实例一起工作;此类公开了本包以下列出的底层类的大多数功能
-
\VersatileAcl\GenericPermissionableEntity强>:代表您应用程序中的实体
-
\VersatileAcl\GenericPermissionableEntitiesCollection强>:用于存储您应用程序中的一个或多个实体的集合类
-
\VersatileAcl\GenericPermission强>:代表分配给您应用程序中实体的权限
-
\VersatileAcl\GenericPermissionsCollection:一个用于存储应用程序中特定实体一个或多个权限的集合类。可以将此类的同一实例分配给多个实体,但建议您为应用程序中的每个实体维护此类的一个单独实例。
示例实际应用
我们将使用一个博客应用程序,该应用程序有一个用户表,包含有关注册博客用户的信息(此表中的用户也是博客帖子的作者和应用程序中博客帖子的评论者),还有一个帖子表和一个评论表。以下是示例应用程序的架构
以下是博客应用程序的关系规则
- 一个用户可以发表许多帖子
- 一个用户可以在每篇帖子上发表一个或多个评论
- 一篇帖子可以与一个或多个评论相关联
以下是与此示例博客应用程序相关的某些访问控制组定义
注意:与comments-owners和posts-owners相关的权限需要断言回调,进一步检查该组的成员只能对其拥有的评论或帖子执行操作(而不是其他用户的评论和帖子)。
让我们使用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']; } );
注意:\VersatileAcl\GenericPermission::getAllActionsIdentifier()是一个特殊字符串,代表应用程序中任何实体可以对每个资源执行的所有操作。
注意:\VersatileAcl\GenericPermission::getAllResourcesIdentifier()是一个特殊字符串,代表应用程序中所有可用的资源。
现在我们已经为每个组创建了实体对象,并将必要的权限添加到适当的实体对象中,我们准备定义在博客应用程序中代表以下用户的实体对象。
以下是应用程序中用户用户ID的列表
- frankwhite
- ginawhite
- johndoe
- janedoe
- jackbauer
- jillbauer
让我们在我们的VersatileAcl对象中为每个用户创建和注册实体对象
<?php $usersVaclObj = new VersatileAcl(); $usersVaclObj->addEntity('frankwhite') ->addEntity('ginawhite') ->addEntity('johndoe') ->addEntity('janedoe') ->addEntity('jackbauer') ->addEntity('jillbauer');
以下是组成员定义
让我们通过将代表这些组的适当实体对象作为父实体添加到相应的用户实体对象中,来模拟这些关系
<?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') );
现在我们已经设置了我们的组、用户和权限,让我们看看如何在应用程序中检查用户是否有权对资源执行操作。
让我们从属于'admin'组的用户'frankwhite'开始。这个用户应该能够在应用程序中的任何资源上执行任何操作
<?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
现在让我们继续处理属于'comments-moderators'组的用户'ginawhite'。这个用户应该只能批准和删除应用程序中的评论(该用户还应能够批准和删除他们创建的帖子)
<?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
现在让我们继续处理属于'comments-moderators'组的用户'johndoe'。这个用户应该只能批准和删除应用程序中的评论(该用户还应能够批准和删除他们创建的帖子)
<?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
现在让我们继续处理属于'posts-moderators'组的用户'janedoe'。这个用户应该只能批准和删除应用程序中的帖子(该用户还应能够批准和删除他们创建的评论)
<?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
现在让我们继续处理仅属于'owners'组的用户'jackbauer'。这个用户应该只能批准和删除他们在应用程序中创建的评论和帖子
<?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
最后,让我们测试用户 'jillbauer',该用户也仅属于 'owners' 组。这个用户应该只能批准和删除他们在应用程序中创建的评论和帖子
<?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
注意:您可以通过将另一个回调作为 VersatileAcl::isAllowed(...) 的第四个参数传递来覆盖传递给 VersatileAcl::addPermission(...) 的 $additionalAssertions 可调用函数。在上述所有示例中,我们都将 null 作为 VersatileAcl::isAllowed(...) 的第四个参数传递,这意味着我们希望使用传递给 VersatileAcl::addPermission(...) 的 $additionalAssertions 可调用函数(如果存在)。
这只是这个包可以用来在应用程序中实施访问控制的示例之一。显然,您可以为特定用例提出其他更具创造性的方法来适配此包。
研究这个包的 类图 以更好地了解各个类之间的交互。
调试
您还可以通过调用 VersatileAcl::enableAuditTrail(bool $canAudit=true) 并将其值设为 true 来获取关于 VersatileAcl\VersatileAcl 任何实例底层运作的信息,然后调用 VersatileAcl\VersatileAcl 实例的其他方法,最后输出 VersatileAcl::getAuditTrail() 返回的字符串内容。
您可以通过调用 VersatileAcl::enableVerboseAudit(bool $performVerboseAudit=true) 并使用值 true 或 false 来增加或减少 VersatileAcl::getAuditTrail() 返回的信息量。
最后,您可以通过调用 VersatileAcl::clearAuditTrail() 来清除、清空或重置 VersatileAcl::getAuditTrail() 返回的字符串。
下面是一个示例
<?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();
产生以下输出
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')