-
Notifications
You must be signed in to change notification settings - Fork 24
Small usage guide
When you want to control access to certain protected objects by other requesting objects you can use ACL.
ACL — is a list of permissions attached to an object. An ACL specifies which users are granted access to objects, as well as what operations are allowed on given objects.
For example you have a blog and you want users create posts and leave comments in it. But you want restrict access and allow create posts only for registered users. Also you want that anybody can view posts in the blog.
In the case of the blog you have Post as a resource and User (registered user) and Guest (not registered user) as role. Lets create few rules.
First we want allow view blog post to anybody:
<?php
$acl = new Acl(); // create an acl
$user = new Role('User'); // create role for our registered user or just user
$guest = new Role('Guest'); // same for not registered or guest user
$guest->addChild($user); // our user will inherits all privileges from guest
$post = new Resource('Post'); // our post becomes resource
$acl->addRule($guest, $post, 'View', true); // Rule #1, allow access for guest and all of his children
Note that parent object span rules on their children, so everything allowed for Guest becomes allowed for User too (later you will know how you can change it by adding new rules).
Now we can check who is allowed to view posts:
<?php
var_dump($acl->isAllowed('Guest', 'Post', 'View')); // true
var_dump($acl->isAllowed('User', 'Post', 'View')); // true
Here is rules for creating posts:
<?php
$acl->addRule($user, $post, 'Create', true); // Rule #2
var_dump($acl->isAllowed('Guest', 'Post', 'Create')); // false, guest not allowed to create posts
var_dump($acl->isAllowed('User', 'Post', 'Create')); // true
Imagine now that your blog becomes popular and you want limit access to certain post (StarredPost) for only premium users.
<?php
$premiumUser = new Role('PremiumUser'); // add new role
$user->addChild($premiumUser);
$starredPost = new Resource('StarredPost'); // and new post type
$post->addChild($starredPost);
$acl->addRule($premiumUser, $starredPost, 'View', true); // Rule #3, allow access to StarredPost for PremiumUser
$acl->addRule($guest, $starredPost, 'View', false); // Rule #4, and deny for anybody else
var_dump($acl->isAllowed('Guest', 'StarredPost', 'View')); // false
var_dump($acl->isAllowed('User', 'StarredPost', 'View')); // false
var_dump($acl->isAllowed('PremiumUser', 'StarredPost', 'View')); // true
As for now we have following ACL structure:
As you remember for our blog we already add Rule #1 which allow View Post for Guest and because Guest has User as a child User is also allowed to View Post (same works for StarredPost who is child of Post). So if we don't add Rule #4 everybody was allowed to View StarredPost.
So how to know what rules would be used when checking access? Why Rule #4 overwrite Rule #1 when we checking access for View StarredPost for Guest? Lets talk about priorities.
Every rule added to ACL have default priority equals to 0. If rules have same priority then last added win. You can change the order of applying of rules using Rule::setPriority, by default all rules have 0 rule priority. Don't be confused, there 2 types of priority: (1) "rule priority" -- control the order in which rule are applied and (2) "priority" -- control rule hierarchy. Each times rules goes deeper into hierarchy (remember that Roles and Resources can expand rules to their children using addChild() method) its priority decreases by 1.
Lets leave our blog and see how it works on more simple example:
<?php
$acl = new Acl();
$guest = new Role('Guest');
$user = new Role('User');
$guest->addChild($user);
$post = new Resource('Post');
$view1 = new Rule('View');
$view1->setId('Rule #6'); // by default ID generated automatically using uniqid()
$view2 = new Rule('View');
$view2->setId('Rule #7');
$view3 = new Rule('View');
$view3->setId('Rule #5');
$acl->addRule($user, $post, $view3, false); // Rule #5
$acl->addRule($guest, $post, $view1, false); // Rule #6
$acl->addRule($guest, $post, $view2, true); // Rule #7
var_dump($acl->isAllowed('Guest', 'Post', 'View')); // Check #1, true
var_dump($acl->isAllowed('User', 'Post', 'View')); // Check #2, false
Same on the diagram:
For Check #1 Rule #6 and Rule #7 was inspected and as Rule #6 was added later it wins and access is allowed. In Check #2 Rule #6 and #7 are also used, but as they was created not for User role, but for Guest its priority equals -1.
We can check what rules are applied and priority used:
<?php
foreach ($acl->isAllowedReturnResult('User', 'Post', 'View') as $result) {
echo "Priority: {$result->getPriority()}\n";
echo "Rule ID: {$result->getRule()->getId()}\n\n";
}
// Output:
// Priority: 0
// Rule ID: Rule #5
// Priority: -1
// Rule ID: Rule #7
// Priority: -1
// Rule ID: Rule #6
We learn how change priorities later, now lets go back to our blog.
It is good that now we can check access to resource for role according to rule, but in your application you more likely not have role and resources, you would have your domain objects. So how connect your domain objects with Roles and Resources? For this case we have RoleAggregateInterface and ResourceAggregateInterface. Lets imagine that we have UserModel and PostModel in our blog application.
<?php
class UserModel implements RoleAggregateInterface
{
public function __construct($who)
{
$this->who = $who;
}
protected $who;
/**
* Return array of names for registered roles.
*
* @return array
*/
public function getRolesNames()
{
return array($this->who);
}
}
class PostModel implements ResourceAggregateInterface
{
public function __construct($who)
{
$this->who = $who;
}
protected $who;
/**
* Return array of names for registered resources.
*
* @return array
*/
public function getResourcesNames()
{
return array($this->who);
}
}
Now we can use UserModel and PostModel when checking access, more importantly your different domains models (different instances of UserModel for example) may return different roles. Thus it is possible create any type of relationship between domain models and roles (or resources): one User role may have many UserModels, and one UserModel may be User role and Some_other_thing role at the same time.
Lets check it on example:
<?php
var_dump($acl->isAllowed(new UserModel('Guest'), new PostModel('Post'), 'View')); // true
var_dump($acl->isAllowed(new UserModel('Guest'), new PostModel('Post'), 'Create')); // false
var_dump($acl->isAllowed(new UserModel('PremiumUser'), new PostModel('StarredPost'), 'View')); // true
Everything looks good now, but what if your users want to edit their posts? And you want restrict post editing only for owners of the post or administrators. In that case we need to use callbacks. But first we change our models a bit.
<?php
class UserModel implements RoleAggregateInterface
{
public $name;
public function __construct($name, $who)
{
$this->name = $name;
$this->who = $who;
}
// ... the remaining code stays the same
}
class PostModel implements ResourceAggregateInterface
{
// used to identify who wrote the post
public $writer;
public function __construct($writer, $who)
{
$this->writer = $writer;
$this->who = $who;
}
// ... the remaining code stays the same
}
Now lets added new role:
<?php
$admin = new Role('Admin');
$premiumUser->addChild($admin); // as admin child of $premiumUser it have all access which was available to
// it, so he can view and create both Post and StarredPost
Next we need create new rule, which allow to edit all posts for admins:
<?php
$acl->addRule($admin, $post, 'Edit', true);
var_dump($acl->isAllowed(new UserModel('Alex', 'Admin'), new PostModel('Jon', 'Post'), 'Edit')); // true
var_dump($acl->isAllowed(new UserModel('Alex', 'Admin'), new PostModel('Jon', 'StarredPost'), 'Edit')); // true
But what we need to do next? We want allow Users edit post only if they own it. We can tell who is the owner of the post by looking at $writer property of our post model. But how we can use this knowledge when creating the rules? Here the callbacks and some of their abilities comes into play.
<?php
$acl->addRule($user, $post, 'Edit', function(\SimpleAcl\RuleResult $r){
// with help of \SimpleAcl\RuleResult we can get access to our models when creating the rules
// and decide is access allowed or not
if ( ($u = $r->getRoleAggregate()) instanceof UserModel && ($p = $r->getResourceAggregate()) instanceof PostModel ) {
return $u->name == $p->writer;
}
// rule not works
return null;
});
Callbacks works just the same as simple boolean conditions, but they can deal with more complex situations, in which you need somehow to get access to your models (or other things, for example, you can create rule which allow something in 50% of the cases:).
Callbacks make it possible to have access not only to models, but also for other things which related to Acl itself. Lets see it on other example.
<?php
$acl = new Acl();
$user = new Role('User');
$guest = new Role('Guest');
$guest->addChild($user);
$post = new Resource('Post');
$view = new Rule('View');
$acl->addRule($guest, $post, $view, function() {
return true;
});
// or this is also possible
$view->setAction(function() {
return true;
});
var_dump($acl->isAllowed('User', 'Post', 'View')); // true
$acl->addRule($guest, $post, 'View', function (\SimpleAcl\RuleResult $ruleResult) {
echo $ruleResult->getNeedRoleName() . "\n";
echo $ruleResult->getNeedResourceName() . "\n";
echo $ruleResult->getPriority() . "\n";
echo $ruleResult->getRule()->getRole()->getName() . "\n";
echo $ruleResult->getRule()->getResource()->getName() . "\n";
});
var_dump($acl->isAllowed('User', 'Post', 'View')); // true
// Output:
// User
// Post
// -1
// Guest
// Post
Small note here: if callback or action itself return null it removed from the final result and not count when checking access, that is why last check return true.
With the help of callbacks is possible to change priorities of the rules, but you should be careful because rule may work deeper in the hierarchy, and you should remember about it.
<?php
$acl->addRule($user, $post, 'View', false);
var_dump($acl->isAllowed('Guest', 'Post', 'View')); // true
var_dump($acl->isAllowed('User', 'Post', 'View')); // false
$view->setAction(function(\SimpleAcl\RuleResult $ruleResult) {
$ruleResult->setPriority(1);
return true;
});
var_dump($acl->isAllowed('Guest', 'Post', 'View')); // true
var_dump($acl->isAllowed('User', 'Post', 'View')); // true
It is better to check if rule applied to rule's roles (or resources) or one of their children. It is possible to do that easily:
<?php
$view->setAction(function (\SimpleAcl\RuleResult $ruleResult) {
// if priority less then zero then rule match one of the children
if ( $ruleResult->getPriority() == 0 ) {
$ruleResult->setPriority(1);
}
// or as an equivalent
// if ( $ruleResult->getNeedRoleName() == '...' &&
// $ruleResult->getNeedResourceName() == '...' ) {
// $ruleResult->setPriority(1);
// }
return true;
});