popphp/pop-acl

Pop ACL Component for Pop PHP Framework

3.3.0 2020-01-07 17:16 UTC

This package is auto-updated.

Last update: 2020-07-07 18:17:56 UTC


README

Build Status Coverage Status

OVERVIEW

pop-acl is a full-featured "hybrid" between the standard ACL and RBAC user access concepts. Beyond allowing or denying basic user access, it provides support for roles, resources, permissions as well as assertions and policies for fine-grain access-control.

pop-acl is a component of the Pop PHP Framework.

INSTALL

Install pop-acl using Composer.

composer require popphp/pop-acl

BASIC USAGE

use Pop\Acl\Acl;
use Pop\Acl\AclRole as Role;
use Pop\Acl\AclResource as Resource;

$acl = new Acl();

$admin  = new Role('admin');
$editor = new Role('editor');
$reader = new Role('reader');

$page = new Resource('page');

$acl->addRoles([$admin, $editor, $reader]);
$acl->addResource($page);

$acl->allow('admin', 'page')           // Admin can do anything to a page
    ->allow('editor', 'page', 'edit')  // Editor can only edit a page
    ->allow('reader', 'page', 'read'); // Editor can only edit a page

if ($acl->isAllowed('admin', 'page', 'add'))   { } // Returns true
if ($acl->isAllowed('editor', 'page', 'edit')) { } // Returns true
if ($acl->isAllowed('editor', 'page', 'add'))  { } // Returns false
if ($acl->isAllowed('reader', 'page', 'edit')) { } // Returns false
if ($acl->isAllowed('reader', 'page', 'read')) { } // Returns true

ROLE INHERITANCE

You can have roles inherit access rules as well.

use Pop\Acl\Acl;
use Pop\Acl\AclRole as Role;
use Pop\Acl\AclResource as Resource;

$acl = new Acl();

$editor = new Role('editor');
$reader = new Role('reader');

// Add the $reader role as a child role of $editor.
// The role $reader will now inherit the access rules
// of the role $editor, unless explicitly overridden.
$editor->addChild($reader);

$page = new Resource('page');

$acl->addRoles([$editor, $reader]);
$acl->addResource($page);

// Neither the editor or reader can add a page
$acl->deny('editor', 'page', 'add');

// The editor can edit a page
$acl->allow('editor', 'page', 'edit');

// Both the editor or reader can read a page
$acl->allow('editor', 'page', 'read');

// Over-riding deny rule so that a reader cannot edit a page
$acl->deny('reader', 'page', 'edit');

if ($acl->isAllowed('editor', 'page', 'add'))  { } // Returns false
if ($acl->isAllowed('reader', 'page', 'add'))  { } // Returns false
if ($acl->isAllowed('editor', 'page', 'edit')) { } // Returns true
if ($acl->isAllowed('reader', 'page', 'edit')) { } // Returns false
if ($acl->isAllowed('editor', 'page', 'read')) { } // Returns true
if ($acl->isAllowed('reader', 'page', 'read')) { } // Returns true

USING ASSERTIONS

If you want more fine-grain control over permissions and who is allowed to do what, you can use assertions. First, define the assertion class, which implements the Pop\Acl\Assertion\AssertionInterface. In this example, we want to check that the user "owns" the resource via a matching user ID.

use Pop\Acl\Acl;
use Pop\Acl\AclRole;
use Pop\Acl\AclResource;
use Pop\Acl\Assertion\AssertionInterface;

class UserCanEditPage implements AssertionInterface
{

    public function assert(
        Acl $acl, AclRole $role,
        AclResource $resource = null,
        $permission = null
    )
    {
        return ((null !== $resource) && ($role->id == $resource->user_id));
    }

}

Then, within the application, you can use the assertions like this:

use Pop\Acl\Acl;
use Pop\Acl\AclRole as Role;
use Pop\Acl\AclResource as Resource;

$acl = new Acl();

$admin  = new Role('admin');
$editor = new Role('editor');

$page = new Resource('page');

$admin->id     = 1001;
$editor->id    = 1002;
$page->user_id = 1001;

$acl->addRoles([$admin, $editor]);
$acl->addResource($page);

$acl->allow('admin', 'page', 'add')
    ->allow('admin', 'page', 'edit', new UserCanEditPage())
    ->allow('editor', 'page', 'edit', new UserCanEditPage())

// Returns true because the assertion passes,
// the admin's ID matches the page's user ID
if ($acl->isAllowed('admin', 'page', 'edit')) { }

// Although editors can edit pages, this returns false
// because the assertion fails, as this editor's ID
// does not match the page's user ID
if ($acl->isAllowed('editor', 'page', 'edit')) { }

USING POLICIES

An alternate way to achieve even more specific fine-grain control is to use policies. Similar to assertions, you have to write the policy class and it needs to use the Pop\Acl\Policy\PolicyTrait. Unlike assertions that are centered around the single assert() method, policies allow you to write separate methods that will be called and evaluated via the can() method in the PolicyTrait. Consider the following simple policy class:

use Pop\Acl\AclResource;

class User
{

    use Pop\Acl\Policy\PolicyTrait;

    public $id      = null;
    public $isAdmin = null;

    public function __construct($id, $isAdmin)
    {
        $this->id      = (int)$id;
        $this->isAdmin = (bool)$isAdmin;
    }

    public function create(User $user, AclResource $page)
    {
        return (($user->isAdmin) && ($page->getName() == 'page'));
    }

    public function update(User $user, AclResource $page)
    {
        return ($user->id === $page->user_id);
    }

    public function delete(User $user, AclResource $page)
    {
        return (($user->isAdmin) || ($user->id === $page->user_id));
    }

}

The above policy class can enforce whether or not a user can create, update or delete a page resource.

$page   = new AclResource('page', ['id' => 2001, 'user_id' => 1002]);
$admin  = new User(1001, true);
$editor = new User(1002, false);

var_dump($admin->can('create', $page));   // Returns true, because the user is an admin
var_dump($editor->can('create', $page));  // Returns false, because the user is an editor (not an admin)
var_dump($admin->can('update', $page));   // Returns false, because the admin doesn't "own" the page
var_dump($editor->can('update', $page));  // Returns true, because the editor does "own" the page

For a more advanced example, the policy class can be a role class, which extends the Pop\Acl\AclRole class. This allows the main Acl object to evaluate any policies added to it.

use Pop\Acl\Acl;
use Pop\Acl\AclRole;
use Pop\Acl\AclResource;

class User extends AclRole
{

    use Pop\Acl\Policy\PolicyTrait;

    public function __construct($name, $id, $isAdmin)
    {
        parent::__construct($name, ['id' => $id, 'isAdmin' => $isAdmin]);
    }

    public function create(User $user, AclResource $page)
    {
        return (($user->isAdmin) && ($page->getName() == 'page'));
    }

    public function update(User $user, AclResource $page)
    {
        return ($user->id === $page->user_id);
    }

    public function delete(User $user, AclResource $page)
    {
        return (($user->isAdmin) || ($user->id === $page->user_id));
    }

}

Then the user role and policy can be added to the main Acl object:

$page   = new AclResource('page', ['id' => 2001, 'user_id' => 1002]);
$admin  = new User('admin', 1001, true);
$editor = new User('editor', 1002, false);

$acl = new Acl();
$acl->addRoles([$admin, $editor]);
$acl->addResource($page);
$acl->addPolicy('create', 'admin', 'page');
$acl->addPolicy('create', 'editor', 'page');
$acl->addPolicy('update', 'admin', 'page');
$acl->addPolicy('update', 'editor', 'page');

// Returns true, because the user is an admin
var_dump($acl->evaluatePolicy('create', 'admin', 'page'));  

// Returns false, because the user is an editor (not an admin)
var_dump($acl->evaluatePolicy('create', 'editor', 'page')); 

// Returns false, because the admin doesn't "own" the page
var_dump($acl->evaluatePolicy('update', 'admin', 'page'));  

// Returns true, because the editor does "own" the page
var_dump($acl->evaluatePolicy('update', 'editor', 'page')); 

The above example demonstrates a direct evaluation of policies by calling the evaluatePolicy() method. However, if the Acl object has policies added to it, they will be evaluated when the isAllowed() and isDenied() methods are called, based on the role and resource being passed into those methods. Using the same set up as above, you can call the following and it will behave similarly as above:

// Returns true, because the user is an admin
var_dump($acl->isAllowed('admin', 'page', 'create'));  

// Returns false, because the user is an editor (not an admin)
var_dump($acl->isAllowed('editor', 'page', 'create')); 

// Returns false, because the admin doesn't "own" the page
var_dump($acl->isAllowed('admin', 'page', 'update'));  

// Returns true, because the editor does "own" the page
var_dump($acl->isAllowed('editor', 'page', 'update'));