Skip to content

Loading…

Initial ZF2 RBAC Component #2616

Closed
wants to merge 1 commit into from

3 participants

@ghost

No description provided.

@ghost Unknown referenced this pull request in zendframework/zf2-documentation
Closed

Initial RBAC writeup. #312

@HarryR

While this RBAC interface feels better designed and implemented than the ZF2 ACL module, I really do hope the SpiffySecurity firewall interface makes it into ZF2.

fnmatch() for route matches makes makes it very easy to use and a standard interface for 'Identities' (as implemented in SpiffySecurity) would provide a nice way to tie the less than stellar Zend\Authentication and ZfcUser components in with the rest of the framework.

$0.02.

@ghost

I'm not sure where or how that would be implemented. Furthermore, shouldn't firewalls be restricted to userland code?

@weierophinney
Zend Framework member

@HarryR Zend\Authentication and Zend\Permissions\Acl were designed from the outset to be standalone components that could work with or without the MVC. ACL is a very different paradigm than RBAC, and as such, it's neither better nor worse (the design is actually quite elegant, and one of the better ACL implementations I've seen. No I did not write it.).

I would prefer that RBAC follows the footsteps of Authentication and ACL, and be de-coupled from the MVC. If you want to implement something to do automated checks based on the current identity and/or route, the proper place to do that is at the integration layer -- in other words, a module.

@ghost

This is ready for review.

@weierophinney
Zend Framework member

Awesome -- thanks, @SpiffyJr !

@weierophinney weierophinney was assigned
@ghost Unknown pushed a commit that referenced this pull request
@weierophinney weierophinney Merge branch 'feature/rbac' into develop
Close #2616
5961247
@weierophinney weierophinney added a commit to zendframework/zend-permissions-rbac that referenced this pull request
@weierophinney weierophinney Merge branch 'feature/rbac' into develop 39ca271
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 29, 2012
  1. @spiffyjr

    Initial ZF2 RBAC Component

    spiffyjr committed
View
102 library/Zend/Permissions/Rbac/AbstractIterator.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac;
+
+use RecursiveIterator;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+abstract class AbstractIterator implements RecursiveIterator
+{
+ protected $index = 0;
+ protected $children = array();
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return mixed Can return any type.
+ */
+ public function current()
+ {
+ return $this->children[$this->index];
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next()
+ {
+ $this->index++;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return scalar scalar on success, or null on failure.
+ */
+ public function key()
+ {
+ return $this->index;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ */
+ public function valid()
+ {
+ return isset($this->children[$this->index]);
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind()
+ {
+ $this->index = 0;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.1.0)<br/>
+ * Returns if an iterator can be created fot the current entry.
+ * @link http://php.net/manual/en/recursiveiterator.haschildren.php
+ * @return bool true if the current entry can be iterated over, otherwise returns false.
+ */
+ public function hasChildren()
+ {
+ return count($this->children) > 0;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.1.0)<br/>
+ * Returns an iterator for the current entry.
+ * @link http://php.net/manual/en/recursiveiterator.getRoles.php
+ * @return RecursiveIterator An iterator for the current entry.
+ */
+ public function getChildren()
+ {
+ return $this->children[$this->index];
+ }
+}
View
124 library/Zend/Permissions/Rbac/AbstractRole.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac;
+
+use RecursiveIteratorIterator;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+abstract class AbstractRole extends AbstractIterator
+{
+ /**
+ * @var null|AbstractRole
+ */
+ protected $parent;
+
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @var array
+ */
+ protected $permissions = array();
+
+ /**
+ * Get the name of the role.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Add permission to the role.
+ *
+ * @param $name
+ * @return AbstractRole
+ */
+ public function addPermission($name)
+ {
+ $this->permissions[$name] = true;
+
+ return $this;
+ }
+
+ /**
+ * Checks if a permission exists for this role or any child roles.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function hasPermission($name)
+ {
+ if (isset($this->permissions[$name])) {
+ return true;
+ }
+
+ $it = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::CHILD_FIRST);
+ foreach ($it as $leaf) {
+ /** @var AbstractRole $leaf */
+ if ($leaf->hasPermission($name)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add a child.
+ *
+ * @param AbstractRole|string $child
+ * @return Role
+ */
+ public function addChild($child)
+ {
+ if (is_string($child)) {
+ $child = new Role($child);
+ }
+ if (!$child instanceof AbstractRole) {
+ throw new Exception\InvalidArgumentException(
+ 'Child must be a string or instance of Zend\Permissions\Rbac\AbstractRole'
+ );
+ }
+
+ $child->setParent($this);
+ $this->children[] = $child;
+
+ return $this;
+ }
+
+ /**
+ * @param AbstractRole $parent
+ * @return AbstractRole
+ */
+ public function setParent($parent)
+ {
+ $this->parent = $parent;
+
+ return $this;
+ }
+
+ /**
+ * @return null|AbstractRole
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+}
View
27 library/Zend/Permissions/Rbac/AssertionInterface.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+interface AssertionInterface
+{
+ /**
+ * Assertion method - must return a boolean.
+ *
+ * @param Rbac $bac
+ * @return boolean
+ */
+ public function assert(Rbac $rbac);
+}
View
19 library/Zend/Permissions/Rbac/Exception/ExceptionInterface.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac\Exception;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+interface ExceptionInterface
+{}
View
20 library/Zend/Permissions/Rbac/Exception/InvalidArgumentException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac\Exception;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements
+ ExceptionInterface
+{}
View
159 library/Zend/Permissions/Rbac/Rbac.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac;
+
+use RecursiveIteratorIterator;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+class Rbac extends AbstractIterator
+{
+ /**
+ * flag: whether or not to create roles automatically if
+ * they do not exist.
+ *
+ * @var bool
+ */
+ protected $createMissingRoles = false;
+
+ /**
+ * @param boolean $createMissingRoles
+ * @return \Zend\Permissions\Rbac\Rbac
+ */
+ public function setCreateMissingRoles($createMissingRoles)
+ {
+ $this->createMissingRoles = $createMissingRoles;
+
+ return $this;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function getCreateMissingRoles()
+ {
+ return $this->createMissingRoles;
+ }
+
+ /**
+ * Add a child.
+ *
+ * @param string|AbstractRole $child
+ * @return AbstractRole
+ * @throws Exception\InvalidArgumentException
+ */
+ public function addRole($child, $parents = null)
+ {
+ if (is_string($child)) {
+ $child = new Role($child);
+ }
+ if (!$child instanceof AbstractRole) {
+ throw new Exception\InvalidArgumentException(
+ 'Child must be a string or instance of Zend\Permissions\Rbac\AbstractRole'
+ );
+ }
+
+ if ($parents) {
+ if (!is_array($parents)) {
+ $parents = array($parents);
+ }
+ foreach ($parents as $parent) {
+ if ($this->createMissingRoles && !$this->hasRole($parent)) {
+ $this->addRole($parent);
+ }
+ $this->getRole($parent)->addChild($child);
+ }
+ }
+
+ $this->children[] = $child;
+
+ return $this;
+ }
+
+ /**
+ * Is a child with $name registered?
+ *
+ * @param \Zend\Permissions\Rbac\AbstractRole|string $objectOrName
+ * @return bool
+ */
+ public function hasRole($objectOrName)
+ {
+ try {
+ $this->getRole($objectOrName);
+
+ return true;
+ } catch (Exception\InvalidArgumentException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get a child.
+ *
+ * @param \Zend\Permissions\Rbac\AbstractRole|string $objectOrName
+ * @return AbstractRole
+ * @throws Exception\InvalidArgumentException
+ */
+ public function getRole($objectOrName)
+ {
+ if (!is_string($objectOrName) && !$objectOrName instanceof AbstractRole) {
+ throw new Exception\InvalidArgumentException(
+ 'Expected string or instance of \Zend\Permissions\Rbac\AbstractRole'
+ );
+ }
+
+ $it = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::CHILD_FIRST);
+ foreach ($it as $leaf) {
+ if ((is_string($objectOrName) && $leaf->getName() == $objectOrName) || $leaf == $objectOrName) {
+ return $leaf;
+ }
+ }
+
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'No child with name "%s" could be found',
+ is_object($objectOrName) ? $objectOrName->getName() : $objectOrName
+ ));
+ }
+
+ /**
+ * Determines if access is granted by checking the role and child roles for permission.
+ *
+ * @param string $permission
+ * @param \Zend\Permissions\Rbac\AssertionInterface|Callable|null $assert
+ */
+ public function isGranted($role, $permission, $assert = null)
+ {
+ if ($assert) {
+ if ($assert instanceof AssertionInterface) {
+ if (!$assert->assert($this)) {
+ return false;
+ }
+ } elseif (is_callable($assert)) {
+ if (!$assert($this)) {
+ return false;
+ }
+ } else {
+ throw new Exception\InvalidArgumentException(
+ 'Assertions must be a Callable or an instance of Zend\Permissions\Rbac\AssertionInterface'
+ );
+ }
+ }
+
+ if ($this->getRole($role)->hasPermission($permission)) {
+ return true;
+ }
+
+ return false;
+ }
+}
View
27 library/Zend/Permissions/Rbac/Role.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace Zend\Permissions\Rbac;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage Rbac
+ */
+class Role extends AbstractRole
+{
+ /**
+ * @param string $name
+ */
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+}
View
144 tests/ZendTest/Permissions/Rbac/RbacTest.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace ZendTest\Permissions\Rbac;
+
+use Zend\Permissions\Rbac;
+use ZendTest\Permissions\Rbac\TestAsset;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage UnitTests
+ * @group Zend_Rbac
+ */
+class RbacTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \Zend\Permissions\Rbac\Rbac
+ */
+ protected $rbac;
+
+ public function setUp()
+ {
+ $this->rbac = new Rbac\Rbac();
+ }
+
+ public function testIsGrantedAssertion()
+ {
+ $foo = new Rbac\Role('foo');
+ $bar = new Rbac\Role('bar');
+
+ $true = new TestAsset\SimpleTrueAssertion();
+ $false = new TestAsset\SimpleFalseAssertion();
+
+ $roleNoMatch = new TestAsset\RoleMustMatchAssertion($bar);
+ $roleMatch = new TestAsset\RoleMustMatchAssertion($foo);
+
+ $foo->addPermission('can.foo');
+ $bar->addPermission('can.bar');
+
+ $this->rbac->addRole($foo);
+ $this->rbac->addRole($bar);
+
+ $this->assertEquals(true, $this->rbac->isGranted($foo, 'can.foo', $true));
+ $this->assertEquals(false, $this->rbac->isGranted($bar, 'can.bar', $false));
+
+ $this->assertEquals(false, $this->rbac->isGranted($bar, 'can.bar', $roleNoMatch));
+ $this->assertEquals(false, $this->rbac->isGranted($bar, 'can.foo', $roleNoMatch));
+
+ $this->assertEquals(true, $this->rbac->isGranted($foo, 'can.foo', $roleMatch));
+ }
+
+ public function testIsGrantedSingleRole()
+ {
+ $foo = new Rbac\Role('foo');
+ $foo->addPermission('can.bar');
+
+ $this->rbac->addRole($foo);
+
+ $this->assertEquals(true, $this->rbac->isGranted('foo', 'can.bar'));
+ $this->assertEquals(false, $this->rbac->isGranted('foo', 'can.baz'));
+ }
+
+ public function testIsGrantedChildRoles()
+ {
+ $foo = new Rbac\Role('foo');
+ $bar = new Rbac\Role('bar');
+
+ $foo->addPermission('can.foo');
+ $bar->addPermission('can.bar');
+
+ $this->rbac->addRole($foo);
+ $this->rbac->addRole($bar, $foo);
+
+ $this->assertEquals(true, $this->rbac->isGranted('foo', 'can.bar'));
+ $this->assertEquals(true, $this->rbac->isGranted('foo', 'can.foo'));
+ $this->assertEquals(true, $this->rbac->isGranted('bar', 'can.bar'));
+
+ $this->assertEquals(false, $this->rbac->isGranted('foo', 'can.baz'));
+ $this->assertEquals(false, $this->rbac->isGranted('bar', 'can.baz'));
+ }
+
+ public function testHasRole()
+ {
+ $foo = new Rbac\Role('foo');
+
+ $this->rbac->addRole('bar');
+ $this->rbac->addRole($foo);
+
+ $this->assertEquals(true, $this->rbac->hasRole($foo));
+ $this->assertEquals(true, $this->rbac->hasRole('bar'));
+ $this->assertEquals(false, $this->rbac->hasRole('baz'));
+ }
+
+ public function testAddRoleFromString()
+ {
+ $this->rbac->addRole('foo');
+
+ $foo = $this->rbac->getRole('foo');
+ $this->assertInstanceOf('Zend\Permissions\Rbac\Role', $foo);
+ }
+
+ public function testAddRoleFromClass()
+ {
+ $foo = new Rbac\Role('foo');
+
+ $this->rbac->addRole('foo');
+ $foo2 = $this->rbac->getRole('foo');
+
+ $this->assertEquals($foo, $foo2);
+ $this->assertInstanceOf('Zend\Permissions\Rbac\Role', $foo2);
+ }
+
+ public function testAddRoleWithParentsUsingRbac()
+ {
+ $foo = new Rbac\Role('foo');
+ $bar = new Rbac\Role('bar');
+
+ $this->rbac->addRole($foo);
+ $this->rbac->addRole($bar, $foo);
+
+ $this->assertEquals($bar->getParent(), $foo);
+ $this->assertEquals(1, count($foo->getChildren()));
+ }
+
+ public function testAddRoleWithAutomaticParentsUsingRbac()
+ {
+ $foo = new Rbac\Role('foo');
+ $bar = new Rbac\Role('bar');
+
+ $this->rbac->setCreateMissingRoles(true);
+ $this->rbac->addRole($bar, $foo);
+
+ $this->assertEquals($bar->getParent(), $foo);
+ $this->assertEquals(1, count($foo->getChildren()));
+ }
+}
View
45 tests/ZendTest/Permissions/Rbac/TestAsset/RoleMustMatchAssertion.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace ZendTest\Permissions\Rbac\TestAsset;
+
+use Zend\Permissions\Rbac\AbstractRole;
+use Zend\Permissions\Rbac\AssertionInterface;
+use Zend\Permissions\Rbac\Rbac;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage UnitTests
+ * @group Zend_Rbac
+ */
+class RoleMustMatchAssertion implements AssertionInterface
+{
+ /**
+ * @var AbstractRole
+ */
+ protected $role;
+
+ public function __construct(AbstractRole $role)
+ {
+ $this->role = $role;
+ }
+
+ /**
+ * Assertion method - must return a boolean.
+ *
+ * @param Rbac $bac
+ * @return boolean
+ */
+ public function assert(Rbac $rbac)
+ {
+ return $this->role->getName() == 'foo';
+ }
+}
View
34 tests/ZendTest/Permissions/Rbac/TestAsset/SimpleFalseAssertion.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace ZendTest\Permissions\Rbac\TestAsset;
+
+use Zend\Permissions\Rbac\AssertionInterface;
+use Zend\Permissions\Rbac\Rbac;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage UnitTests
+ * @group Zend_Rbac
+ */
+class SimpleFalseAssertion implements AssertionInterface
+{
+ /**
+ * Assertion method - must return a boolean.
+ *
+ * @param Rbac $bac
+ * @return boolean
+ */
+ public function assert(Rbac $rbac)
+ {
+ return false;
+ }
+}
View
34 tests/ZendTest/Permissions/Rbac/TestAsset/SimpleTrueAssertion.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Permissions
+ */
+
+namespace ZendTest\Permissions\Rbac\TestAsset;
+
+use Zend\Permissions\Rbac\AssertionInterface;
+use Zend\Permissions\Rbac\Rbac;
+
+/**
+ * @category Zend
+ * @package Zend_Permissions
+ * @subpackage UnitTests
+ * @group Zend_Rbac
+ */
+class SimpleTrueAssertion implements AssertionInterface
+{
+ /**
+ * Assertion method - must return a boolean.
+ *
+ * @param Rbac $bac
+ * @return boolean
+ */
+ public function assert(Rbac $rbac)
+ {
+ return true;
+ }
+}
Something went wrong with that request. Please try again.