alexshelkov / SimpleAcl

Simple ACL for PHP

Home Page:https://github.com/alexshelkov/SimpleAcl/wiki/Small-usage-guide

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Is it possible set rule depend on role and resource properties?

lichtner opened this issue · comments

Hi,
your acl looks good, but I am missing one important feature.

Can I define this rules?

  1. admin - can edit all posts
  2. author - can edit only herself posts?

In other word can I add rule something like:

return $postModel->author_id == $signedUser->id;

Thanks

Hi.

Yes it was possible, but now since 2.0.15 it even easy (thx to you, because I planned to implement this, but was a bit lazy to add these small function and also nobody ask about it).

All you need to do is:

class UserModel implements RoleAggregateInterface
{
    public $login;

    public function __construct($login)
    {
        $this->login = $login;
    }

    public function getRolesNames()
    {
        return array('author');
    }
}

class PostModel implements ResourceAggregateInterface
{
    public $content;

    public function __construct($content)
    {
        $this->content = $content;
    }

    public function getResourcesNames()
    {
        return array('post');
    }
}

$role = new Role('author');
$resource = new Resource('post');

$acl = new Acl();

$acl->addRule($role, $resource, 'View', function (RuleResult $r) {
    /** @var UserModel $user */
    $user = $r->getRoleAggregate();
    /** @var PostModel $post */
    $post = $r->getResourceAggregate();

    // ensure that these rule works only when we have some post and user
    if ( $user instanceof UserModel && $post instanceof PostModel ) {
        return strpos($post->content, $user->login) !== false;    
    }

    return null;
});

$user = new UserModel('user@test.com');
$post = new PostModel('These post was written by user@test.com');

var_dump($acl->isAllowed($user, $post, 'View'));

$user2 = new UserModel('not_author@test.com');
$post2 = new PostModel('These post was written by not_author@test.com');

var_dump($acl->isAllowed($user, $post2, 'View'));
var_dump($acl->isAllowed($user2, $post2, 'View'));

Hope it helps.

Thanks for your answer, your ACL look very good.

I found two solution for my example (better then zero, isn't it? ;-) but what do you think what is better aproach?

First use two roles with two rules:

class UserModel implements Role\RoleAggregateInterface {

    public $id, $role;

    public function __construct($id, $role) {
        $this->id = $id;
        $this->role = $role;
    }

    public function getRolesNames() {
        return array('author','admin');  // this is good approach to return two roles?
    }
}

class PostModel implements Resource\ResourceAggregateInterface {

    public $user_id, $title;

    public function __construct($user_id, $title) {
        $this->user_id = $user_id;
        $this->title = $title;
    }

    public function getResourcesNames() {
        return array('post');
    }
}

$author = new Role('author');
$admin = new Role('admin');
$author->addChild($admin);

$post = new Resource('post');

$acl = new Acl();
$acl->addRule($author, $post, 'edit', function(RuleResult $r) {
    /** @var UserModel $user */
    $user = $r->getRoleAggregate();
    /** @var PostModel $post */
    $post = $r->getResourceAggregate();

    if ($user instanceof UserModel && $post instanceof PostModel) {
        /**
         * at first I add this "if" here but then I realize it is useless
         * because this rule is valid also for admins
         */
        //if ($user->role == 'author') {
            return $user->id == $post->user_id;
        //}
    }
    return null;
});
$acl->addRule($admin, $post, 'edit', function(RuleResult $r) {
    /** @var UserModel $user */
    $user = $r->getRoleAggregate();
    /** @var PostModel $post */
    $post = $r->getResourceAggregate();

    if ($user instanceof UserModel && $post instanceof PostModel) {
        /**
         * Is it good approach how to test if it is admin?
         * IMHO: May be it looks little bit confusing because I created this rule
         * only for admin, but I have to test again if $user->role == 'admin'
         * is it correct or I missed something? 
         */
        if ($user->role == 'admin') {
            return true;
        }
    }
    return null;
});

$user1 = new UserModel(1, 'admin');
$user2 = new UserModel(2, 'author');
$post1 = new PostModel(1, 'Post 1');
$post2 = new PostModel(2, 'Post 2');

echo "user1(admin):post1 = " ; var_dump($acl->isAllowed($user1, $post1, 'edit'));
echo "user1(admin):post2 = " ; var_dump($acl->isAllowed($user1, $post2, 'edit'));
echo "user2(author):post2 = " ; var_dump($acl->isAllowed($user2, $post2, 'edit'));
echo "user2(author):post1 = " ; var_dump($acl->isAllowed($user2, $post1, 'edit'));

I added some question in the code. After I wrote this code I realized then these two addRule could be compressed in one. So I created second example is use only one role and one rule.

class UserModel implements Role\RoleAggregateInterface {

    public $id, $role;

    public function __construct($id, $role) {
        $this->id = $id;
        $this->role = $role;
    }

    public function getRolesNames() {
        return array('user');
    }
}

class PostModel implements Resource\ResourceAggregateInterface {

    public $user_id, $title;

    public function __construct($user_id, $title) {
        $this->user_id = $user_id;
        $this->title = $title;
    }

    public function getResourcesNames() {
        return array('post');
    }
}

$user = new Role('user');
$post = new Resource('post');

$acl = new Acl();
$acl->addRule($user, $post, 'edit', function(RuleResult $r) {
    /** @var UserModel $user */
    $user = $r->getRoleAggregate();
    /** @var PostModel $post */
    $post = $r->getResourceAggregate();

    if ($user instanceof UserModel && $post instanceof PostModel) {
        if ($user->role == 'author') {
            return $user->id == $post->user_id;
        } elseif ($user->role == 'admin') {
            return true;
        }
    }
    return null;
});

$user1 = new UserModel(1, 'admin');
$user2 = new UserModel(2, 'author');
$post1 = new PostModel(1, 'Post 1');
$post2 = new PostModel(2, 'Post 2');

echo "user1(admin):post1 = " ; var_dump($acl->isAllowed($user1, $post1, 'edit'));
echo "user1(admin):post2 = " ; var_dump($acl->isAllowed($user1, $post2, 'edit'));
echo "user2(author):post2 = " ; var_dump($acl->isAllowed($user2, $post2, 'edit'));
echo "user2(author):post1 = " ; var_dump($acl->isAllowed($user2, $post1, 'edit'));

Both work correctly. What do you think what is better approach? Or you have third better? Thank for your answer.

I am sure I don't recommend second one. Because it overcomlicated a bit. You add one more field in your model which called Role. And Acl also have Roles, so its too much Roles in total. And then you check it manually in your function (that is the main problem, you add one more unnecessary "layer" to whole system). Too hard. I strongly don't reccomend it, in furure you would be confused, there is much better way to do it.

As for question in first example. Yes it is aboolutly OK to return multiple Role names (same for resources). But not in your case. Because you are already use roles inheritance (when add child).

Here is the best way to do it (at least as I think :)

class UserModel implements Role\RoleAggregateInterface
{
    public $id, $role;

    public function __construct($id, $role)
    {
        $this->id = $id;
        $this->role = $role;
    }

    public function getRolesNames()
    {
        return array($this->role); // note a change here, it because you use inertness
                                   // all your admin users already authors
    }
}

class PostModel implements Resource\ResourceAggregateInterface
{
    public $user_id, $title;

    public function __construct($user_id, $title)
    {
        $this->user_id = $user_id;
        $this->title = $title;
    }

    public function getResourcesNames()
    {
        return array('post');
    }
}

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

$author->addChild($admin); // here you span all rules which apply on authors to admins.

$post = new Resource('post');

$acl = new Acl();

// so all rules on authors works on admins too
$acl->addRule($author, $post, 'edit', function (RuleResult $r) {
    /** @var UserModel $user */
    $user = $r->getRoleAggregate();
    /** @var PostModel $post */
    $post = $r->getResourceAggregate();

    if ( $user instanceof UserModel && $post instanceof PostModel ) {
        return $user->id == $post->user_id;
    }

    return null;
});

// and now we overwrite prev. rule for admins (pls check the wiki you will find small desc. why these happens)
// try to comment these rule and you will see that when you
// check access for admin it would be the same as for author
$acl->addRule($admin, $post, 'edit', true);

$user1 = new UserModel(1, 'admin');
$user2 = new UserModel(2, 'author');

$post1 = new PostModel(1, 'Post 1');
$post2 = new PostModel(2, 'Post 2');

echo "user1(admin):post1 = ";
var_dump($acl->isAllowed($user1, $post1, 'edit')); // $user1 is admin so its is true

echo "user1(admin):post2 = ";
var_dump($acl->isAllowed($user1, $post2, 'edit')); // $user1 is admin so its is true

echo "user2(author):post2 = ";
var_dump($acl->isAllowed($user2, $post2, 'edit')); // $user1 is not admin but he wrote post 2, so it is true

echo "user2(author):post1 = ";
var_dump($acl->isAllowed($user2, $post1, 'edit')); // $user1 is not admin and not wrote post 1, so it is false

Excellent this is exactly what I want to do but I don't know how ;-) Thanks I will check wiki.

May be you can copy this example into wiki IMHO it is very useful.