diff --git a/composer.json b/composer.json index 624e9cde..44c376c3 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,10 @@ { "name": "zendframework/zend-permissions-rbac", - "description": "Zend\\Permissions\\Rbac component", + "description": "provides a role-based access control management", "license": "BSD-3-Clause", "keywords": [ "zf2", - "permissions-rbac" + "Rbac" ], "homepage": "https://github.com/zendframework/zend-permissions-rbac", "autoload": { @@ -13,22 +13,22 @@ } }, "require": { - "php": ">=5.3.23" + "php": ">=5.3.3" }, "extra": { "branch-alias": { - "dev-master": "2.4-dev", - "dev-develop": "2.5-dev" + "dev-master": "2.2-dev", + "dev-develop": "2.3-dev" } }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "satooshi/php-coveralls": "dev-master", - "phpunit/PHPUnit": "~4.0" - }, "autoload-dev": { "psr-4": { "ZendTest\\Permissions\\Rbac\\": "test/" } + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" } } \ No newline at end of file diff --git a/src/AbstractIterator.php b/src/AbstractIterator.php new file mode 100644 index 00000000..4f922ec1 --- /dev/null +++ b/src/AbstractIterator.php @@ -0,0 +1,100 @@ + + * 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 >= 5.0.0)
+ * 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 >= 5.0.0)
+ * 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 >= 5.0.0)
+ * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return bool 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 >= 5.0.0)
+ * 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 >= 5.1.0)
+ * 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() + { + if ($this->valid() && ($this->current() instanceof RecursiveIterator)) { + return true; + } + + return false; + } + + /** + * (PHP 5 >= 5.1.0)
+ * 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]; + } +} diff --git a/src/AbstractRole.php b/src/AbstractRole.php new file mode 100644 index 00000000..fff1a9ac --- /dev/null +++ b/src/AbstractRole.php @@ -0,0 +1,118 @@ +name; + } + + /** + * Add permission to the role. + * + * @param $name + * @return RoleInterface + */ + 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 RoleInterface $leaf */ + if ($leaf->hasPermission($name)) { + return true; + } + } + + return false; + } + + /** + * Add a child. + * + * @param RoleInterface|string $child + * @return Role + */ + public function addChild($child) + { + if (is_string($child)) { + $child = new Role($child); + } + if (!$child instanceof RoleInterface) { + throw new Exception\InvalidArgumentException( + 'Child must be a string or implement Zend\Permissions\Rbac\RoleInterface' + ); + } + + $child->setParent($this); + $this->children[] = $child; + + return $this; + } + + /** + * @param RoleInterface $parent + * @return RoleInterface + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * @return null|RoleInterface + */ + public function getParent() + { + return $this->parent; + } +} diff --git a/src/AssertionInterface.php b/src/AssertionInterface.php new file mode 100644 index 00000000..b94e0aa5 --- /dev/null +++ b/src/AssertionInterface.php @@ -0,0 +1,21 @@ +createMissingRoles = $createMissingRoles; + + return $this; + } + + /** + * @return bool + */ + public function getCreateMissingRoles() + { + return $this->createMissingRoles; + } + + /** + * Add a child. + * + * @param string|RoleInterface $child + * @param array|RoleInterface|null $parents + * @return self + * @throws Exception\InvalidArgumentException + */ + public function addRole($child, $parents = null) + { + if (is_string($child)) { + $child = new Role($child); + } + if (!$child instanceof RoleInterface) { + throw new Exception\InvalidArgumentException( + 'Child must be a string or implement Zend\Permissions\Rbac\RoleInterface' + ); + } + + 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\RoleInterface|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\RoleInterface|string $objectOrName + * @return RoleInterface + * @throws Exception\InvalidArgumentException + */ + public function getRole($objectOrName) + { + if (!is_string($objectOrName) && !$objectOrName instanceof RoleInterface) { + throw new Exception\InvalidArgumentException( + 'Expected string or implement \Zend\Permissions\Rbac\RoleInterface' + ); + } + + $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 RoleInterface|string $role + * @param string $permission + * @param AssertionInterface|Callable|null $assert + * @return bool + */ + 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; + } +} diff --git a/src/Role.php b/src/Role.php new file mode 100644 index 00000000..212bd0d0 --- /dev/null +++ b/src/Role.php @@ -0,0 +1,21 @@ +name = $name; + } +} diff --git a/src/RoleInterface.php b/src/RoleInterface.php new file mode 100644 index 00000000..6b9d734c --- /dev/null +++ b/src/RoleInterface.php @@ -0,0 +1,55 @@ +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())); + } + + /** + * @tesdox Test adding custom child roles works + */ + public function testAddCustomChildRole() + { + $role = $this->getMockForAbstractClass('Zend\Permissions\Rbac\RoleInterface'); + $this->rbac->setCreateMissingRoles(true)->addRole($role, array('parent')); + + $role->expects($this->any()) + ->method('getName') + ->will($this->returnValue('customchild')); + + $role->expects($this->once()) + ->method('hasPermission') + ->with('test') + ->will($this->returnValue(true)); + + $this->assertTrue($this->rbac->isGranted('parent', 'test')); + } +} diff --git a/test/TestAsset/RoleMustMatchAssertion.php b/test/TestAsset/RoleMustMatchAssertion.php new file mode 100644 index 00000000..5cefa212 --- /dev/null +++ b/test/TestAsset/RoleMustMatchAssertion.php @@ -0,0 +1,45 @@ +role = $role; + } + + /** + * Assertion method - must return a boolean. + * + * @param Rbac $bac + * @return bool + */ + public function assert(Rbac $rbac) + { + return $this->role->getName() == 'foo'; + } +} diff --git a/test/TestAsset/SimpleFalseAssertion.php b/test/TestAsset/SimpleFalseAssertion.php new file mode 100644 index 00000000..637e1df4 --- /dev/null +++ b/test/TestAsset/SimpleFalseAssertion.php @@ -0,0 +1,34 @@ +