diff --git a/.travis.yml b/.travis.yml index 9b5b4872..b89c98ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,37 +18,17 @@ env: matrix: include: - - php: 5.6 - env: - - DEPS=lowest - - php: 5.6 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit" - - php: 5.6 - env: - - DEPS=latest - - TEST_COVERAGE=true - - php: 7 - env: - - DEPS=lowest - - php: 7 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit" - - CHECK_CS=true - - php: 7 - env: - - DEPS=latest - php: 7.1 env: - DEPS=lowest - php: 7.1 env: - DEPS=locked + - CHECK_CS=true - php: 7.1 env: - DEPS=latest + - TEST_COVERAGE=true - php: 7.2 env: - DEPS=lowest diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f695262..5b58d2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,49 +2,59 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. -## 2.7.0 - TBD +## 3.0.0 - TBD ### Added -- Nothing. +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) adds + checks for circular references in the role hierarchy when using the + `Role::addChild()` and `Role::addParent()` methods. ### Changed -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- Nothing. - -### Fixed +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) updates + the `Role::addChild(RoleInterface $child)` method to accept only a `RoleInterface` parameter; + strings are no longer accepted. -- Nothing. +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) updates + the `Zend\Permissions\Rbac\AssertionInterface`, adding two parameters to the + `assert()` definition and defining a return type, so that it now reads as + follows: -## 2.6.1 - TBD + ```php + public function assert( + Rbac $rbac, + RoleInterface $role, + string $permission + ) : bool + ``` -### Added +### Deprecated - Nothing. -### Changed - -- Nothing. +### Removed -### Deprecated +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) removes + support for PHP versions prior to 7.1. -- Nothing. +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) removes + the [AbstractIterator](https://github.com/zendframework/zend-permissions-rbac/blob/release-2.6.0/src/AbstractIterator.php) + class. The role hierarchy no longer relies on a `RecursiveIterator`. -### Removed +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) removes + the [AbstractRole](https://github.com/zendframework/zend-permissions-rbac/blob/release-2.6.0/src/AbstractRole.php) + class. All its functions have been merged to the `Zend\Permissions\Rbac\Role` + class. -- Nothing. +- [#34](https://github.com/zendframework/zend-permissions-rbac/pull/34) removes + the method `Role::setParent()`; use `Role::addParent()` instead. ### Fixed -- Nothing. +- [#30](https://github.com/zendframework/zend-permissions-rbac/issues/30) fixes + circular references within the `Role::addChild()` and `Role::addParent()` + algorithms. ## 2.6.0 - 2018-02-01 diff --git a/LICENSE.md b/LICENSE.md index dbb1b49c..67c914a6 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2005-2015, Zend Technologies USA, Inc. +Copyright (c) 2005-2018, Zend Technologies USA, Inc. All rights reserved. diff --git a/README.md b/README.md index 958d7b95..a1a49be4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![Build Status](https://secure.travis-ci.org/zendframework/zend-permissions-rbac.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-permissions-rbac) [![Coverage Status](https://coveralls.io/repos/zendframework/zend-permissions-rbac/badge.svg?branch=master)](https://coveralls.io/r/zendframework/zend-permissions-rbac?branch=master) -Provides role-based access control (RBAC) permissions management. +Provides [Role-Based Access Control](https://it.wikipedia.org/wiki/Role-based_access_control) +(RBAC) permissions management. -- File issues at https://github.com/zendframework/zend-permissions-rbac -- Documentation is at https://zendframework.github.io/zend-permissions-rbac/ +- File issues at https://github.com/zendframework/zend-permissions-rbac/issues +- Documentation is at https://docs.zendframework.com/zend-permissions-rbac/ diff --git a/composer.json b/composer.json index d9ed34ac..ee4a597f 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,12 @@ { "name": "zendframework/zend-permissions-rbac", - "description": "provides a role-based access control management", + "description": "Provides a role-based access control management", "license": "BSD-3-Clause", "keywords": [ - "zf2", - "Rbac" + "zendframework", + "zend-permssions-rbac", + "rbac", + "authorization" ], "homepage": "https://github.com/zendframework/zend-permissions-rbac", "autoload": { @@ -13,7 +15,7 @@ } }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "minimum-stability": "dev", "prefer-stable": true, @@ -29,7 +31,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^5.7.15|| ^6.2.1", + "phpunit/phpunit": "^7.0.1", "zendframework/zend-coding-standard": "~1.0.0" }, "scripts": { diff --git a/composer.lock b/composer.lock index dac7565f..e27fe4d8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "2daefbdcd3df58ce5d4db6cd1e272273", + "content-hash": "60dda7c6ebcec6ecc57fe765479fd341", "packages": [], "packages-dev": [ { @@ -362,16 +362,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.3", + "version": "1.7.5", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", - "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", "shasum": "" }, "require": { @@ -383,7 +383,7 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" }, "type": "library", "extra": { @@ -421,44 +421,44 @@ "spy", "stub" ], - "time": "2017-11-24T13:59:53+00:00" + "time": "2018-02-19T10:16:54+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "5.3.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8ca4b604baf23dab89d87773c28cc07405189ba", + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.0", + "php": "^7.1", "phpunit/php-file-iterator": "^1.4.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", + "phpunit/php-token-stream": "^3.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", "sebastian/environment": "^3.0", "sebastian/version": "^2.0.1", "theseer/tokenizer": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.5" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3.x-dev" + "dev-master": "6.0-dev" } }, "autoload": { @@ -484,7 +484,7 @@ "testing", "xunit" ], - "time": "2017-12-06T09:29:45+00:00" + "time": "2018-02-02T07:01:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -576,28 +576,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -612,7 +612,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -621,33 +621,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2018-02-01T13:07:23+00:00" }, { "name": "phpunit/php-token-stream", - "version": "2.0.2", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -670,20 +670,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-11-27T05:48:46+00:00" + "time": "2018-02-01T13:16:43+00:00" }, { "name": "phpunit/phpunit", - "version": "6.5.5", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "83d27937a310f2984fd575686138597147bdc7df" + "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/83d27937a310f2984fd575686138597147bdc7df", - "reference": "83d27937a310f2984fd575686138597147bdc7df", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", + "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", "shasum": "" }, "require": { @@ -695,15 +695,15 @@ "myclabs/deep-copy": "^1.6.1", "phar-io/manifest": "^1.0.1", "phar-io/version": "^1.0", - "php": "^7.0", + "php": "^7.1", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", + "phpunit/php-code-coverage": "^6.0", "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", + "phpunit/php-timer": "^2.0", + "phpunit/phpunit-mock-objects": "^6.0", "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", + "sebastian/diff": "^3.0", "sebastian/environment": "^3.1", "sebastian/exporter": "^3.1", "sebastian/global-state": "^2.0", @@ -711,16 +711,12 @@ "sebastian/resource-operations": "^1.0", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -728,7 +724,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -754,33 +750,30 @@ "testing", "xunit" ], - "time": "2017-12-17T06:31:19+00:00" + "time": "2018-02-26T07:03:12+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.6", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e3249dedc2d99259ccae6affbc2684eac37c2e53", + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", - "php": "^7.0", + "php": "^7.1", "phpunit/php-text-template": "^1.2.1", "sebastian/exporter": "^3.1" }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, "require-dev": { - "phpunit/phpunit": "^6.5" + "phpunit/phpunit": "^7.0" }, "suggest": { "ext-soap": "*" @@ -788,7 +781,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "6.0.x-dev" } }, "autoload": { @@ -813,7 +806,7 @@ "mock", "xunit" ], - "time": "2018-01-06T05:45:45+00:00" + "time": "2018-02-15T05:27:38+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -862,21 +855,21 @@ }, { "name": "sebastian/comparator", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "11c07feade1d65453e06df3b3b90171d6d982087" + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/11c07feade1d65453e06df3b3b90171d6d982087", - "reference": "11c07feade1d65453e06df3b3b90171d6d982087", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", "shasum": "" }, "require": { "php": "^7.0", - "sebastian/diff": "^2.0", + "sebastian/diff": "^2.0 || ^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { @@ -922,32 +915,33 @@ "compare", "equality" ], - "time": "2018-01-12T06:34:42+00:00" + "time": "2018-02-01T13:46:46+00:00" }, { "name": "sebastian/diff", - "version": "2.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8", + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -972,9 +966,12 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-08-03T08:09:46+00:00" + "time": "2018-02-01T13:45:15+00:00" }, { "name": "sebastian/environment", @@ -1578,7 +1575,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "platform-dev": [] } diff --git a/doc/book/examples.md b/doc/book/examples.md index 48cbc4d8..798e7acc 100644 --- a/doc/book/examples.md +++ b/doc/book/examples.md @@ -80,30 +80,30 @@ Checking permission using `isGranted()` with a class implementing `Zend\Permissions\Rbac\AssertionInterface`: ```php +use App\Model\Article; use Zend\Permissions\Rbac\AssertionInterface; use Zend\Permissions\Rbac\Rbac; -class AssertUserIdMatches implements AssertionInterface +class AssertUserRoleMatches implements AssertionInterface { protected $userId; protected $article; - public function __construct($userId) + public function __construct(string $userId) { $this->userId = $userId; } - public function setArticle($article) + public function setArticle(Article $article) { $this->article = $article; } - public function assert(Rbac $rbac) + public function assert(Rbac $rbac, RoleInterface $role = null, string $permission = null) { if (! $this->article) { return false; } - return ($this->userId === $this->article->getUserId()); } } diff --git a/doc/book/index.html b/doc/book/index.html index 07c8a528..4337842e 100644 --- a/doc/book/index.html +++ b/doc/book/index.html @@ -3,7 +3,7 @@

zend-permissions-rbac

- Provide and query Role-Based Access Controls for your application. + Provide and query Role-Based Access Controls (RBAC) for your application.

$ composer require zendframework/zend-permissions-rbac
diff --git a/doc/book/intro.md b/doc/book/intro.md index ba94f30f..980cc434 100644 --- a/doc/book/intro.md +++ b/doc/book/intro.md @@ -1,8 +1,7 @@ # Introduction -zend-permissions-rbac provides a lightweight role-based access control (RBAC) -implementation based around PHP's `RecursiveIterator` and -`RecursiveIteratorIterator`. RBAC differs from access control lists (ACL) by +`zend-permissions-rbac` provides a lightweight [Role-Based Access Control](https://it.wikipedia.org/wiki/Role-based_access_control) +(RBAC) implementation in PHP. RBAC differs from access control lists (ACL) by putting the emphasis on roles and their permissions rather than objects (resources). @@ -16,7 +15,7 @@ Thus, RBAC has the following model: - many to many relationship between **identities** and **roles**. - many to many relationship between **roles** and **permissions**. -- **roles** can have a parent role. +- **roles** can have parent and child roles (hierarchy of roles). ## Roles diff --git a/doc/book/methods.md b/doc/book/methods.md index c3d086c9..c88113bc 100644 --- a/doc/book/methods.md +++ b/doc/book/methods.md @@ -1,62 +1,39 @@ # Methods -## `Zend\Permissions\Rbac\AbstractIterator` - -The `AbstractIterator` is used as the basis for both the primary `Rbac` class -and the `AbstractRole`. - -Method signature | Description ------------------------------------ | ----------- -`current() : RoleInterface` | Return the current role instance. -`getChildren() : RecursiveIterator` | Returns a recursive iterator of all children of the current role. -`hasChildren() : bool` | Indicates if the current role has children. -`key() : int` | Index of the current role instance. -`next() : void` | Advance to the next role instance. -`rewind() : void` | Seek to the first item in the iterator. -`valid() : bool` | Is the current index valid? - -## `Zend\Permissions\Rbac\AbstractRole` - -The `AbstractRole` provides the base functionality required by the -`RoleInterface`, and is the foundation for the `Role` class. - -Method signature | Description ----------------------------------------------- | ----------- -`addChild(string|RoleInterface $child) : void` | Add a child role to the current instance. -`addPermission(string $name) : void` | Add a permission for the current role. -`getName() : string` | Retrieve the name assigned to this role. -`hasPermission(string $name) : bool` | Does the role have the given permission? -`setParent(RoleInterface $parent) : void` | Assign the provided role as the current role's parent. -`addParent(RoleInterface $parent) : Role` | Add a parent role to the current instance. -`getParent() null|RoleInterface|array` | Retrieve the current role's parent, or array of parents if more that one exists. +## `Zend\Permissions\Rbac\Role` + +The `Role` provides the base functionality required by the `RoleInterface`. + +Method signature | Description +------------------------------------------| ----------- +`__construct(string $name) : void` | Create a new instance with the provided name. +`getName() : string` | Retrieve the name assigned to this role. +`addPermission(string $name) : void` | Add a permission for the current role. +`hasPermission(string $name) : bool` | Does the role have the given permission? +`addChild(RoleInterface $child) : Role` | Add a child role to the current instance. +`getChildren() : RoleInterface[]` | Get all the children roles. +`addParent(RoleInterface $parent) : Role` | Add a parent role to the current instance. +`getParents() : RoleInterface[]` | Get all the parent roles. ## `Zend\Permissions\Rbac\AssertionInterface` Custom assertions can be provided to `Rbac::isGranted()` (see below); such assertions are provided the `Rbac` instance on invocation. -Method signature | Description ---------------------------- | ----------- -`assert(Rbac $rbac) : bool` | Given an RBAC, determine if permission is granted. +Method signature | Description +-------------------------------------------------------------------- | ----------- +`assert(Rbac $rbac, RoleInterface $role, string $permission) : bool` | Given an RBAC, a role, and a permission, determine if permission is granted. ## `Zend\Permissions\Rbac\Rbac` `Rbac` is the object with which you will interact within your application in -order to query for permissions. It extends `AbstractIterator`. +order to query for permissions. Method signature | Description --------------------------------------------------------------------------- | ----------- -`addRole(string|RoleInterface $child, array|RoleInterface $parents = null)` | Add a role to the RBAC. If `$parents` is non-null, the `$child` is also added to any parents provided. -`getRole(string|RoleInterface $role) : RoleInterface` | Recursively queries the RBAC for the given role, returning it if found, and raising an exception otherwise. -`hasRole(string|RoleInterface $role) : bool` | Recursively queries the RBAC for the given role, returning `true` if found, `false` otherwise. +`addRole(string\|RoleInterface $child, array\|RoleInterface $parents = null)` | Add a role to the RBAC. If `$parents` is non-null, the `$child` is also added to any parents provided. +`getRole(string $role) : RoleInterface` | Get the role specified by name, raising an exception if not found. +`hasRole(string\|RoleInterface $role) : bool` | Recursively queries the RBAC for the given role, returning `true` if found, `false` otherwise. `getCreateMissingRoles() : bool` | Retrieve the flag that determines whether or not `$parent` roles are added automatically if not present when calling `addRole()`. `setCreateMissingRoles(bool $flag) : void` | Set the flag that determines whether or not `$parent` roles are added automatically if not present when calling `addRole()`. -`isGranted(string|RoleInterface $role, string $permission, $assert = null)` | Determine if the role has the given permission. If `$assert` is provided and either an `AssertInterface` instance or callable, it will be queried before checking against the given role. - -## `Zend\Permissions\Rbac\Role` - -`Role` inherits from `AbstractRole` and `AbstractIterator`. - -Method signature | Description ----------------------------------- | ----------- -`__construct(string $name) : void` | Create a new instance with the provided name. +`isGranted(string\|RoleInterface $role, string $permission, $assert = null)` | Determine if the role has the given permission. If `$assert` is provided and either an `AssertInterface` instance or callable, it will be queried before checking against the given role. diff --git a/doc/book/migration/to-v3-0.md b/doc/book/migration/to-v3-0.md new file mode 100644 index 00000000..cc30fcae --- /dev/null +++ b/doc/book/migration/to-v3-0.md @@ -0,0 +1,85 @@ +# Upgrading to 3.0 + +If you upgrade from version 2 releases, you will notice a few changes. This +document details the changes + +## Minimum supported PHP version + +Version 3 drops support for PHP versions prior to PHP 7.1. + +## AssertionInterface + +The primary change is the `Zend\Permissions\Rbac\AssertionInterface::assert()` +method definition. + +The new `assert` method has the following signature: + +```php +namespace Zend\Permissions\Rbac; + +public function assert( + Rbac $rbac, + RoleInterface $role, + string $permission +) : bool +``` + +The version 2 releases defined the method such that it only accepted a single +parameter, `Rbac $rbac`. Version 3 adds the `$role` and `$permission` +parameters. This simplifies implementation of dynamic assertions using the role +and the permission information. + +For instance, imagine you want to disable a specific permission `foo` for an +`admin` role; you can implement that as follows: + +```php +public function assert(Rbac $rbac, RoleInterface $role, string $permission) : bool +{ + return ! ($permission === 'foo' && $role->getName() === 'admin'); +} +``` + +If you were previously implementing `AssertionInterface`, you will need to +update the `assert()` signature to match the changes in version 3. + +If you were creating assertions as PHP callables, you may continue to use the +existing signature; however, you may also expand them to accept the new +arguments should they assist you in creating more complex, dynamic assertions. + +## RoleInterface + +`Zend\Permissions\Rbac\RoleInterface` also received a number of changes, +including type hints and method name changes. + +### Type hints + +With the update to [PHP 7.1](#minimum-supported-php-version), we also updated +the `RoleInterface` to provide: + +- scalar type hints where applicable (`addPermission()` and `hasPermission()`). +- add return type hints (including scalar type hints) to all methods. + +You will need to examine the `RoleInterface` definitions to determine what +changes to make to your implementations. + +### setParent becomes addParent + +In version 3, we renamed the method `Role::setParent()` to `Role::addParent()`. +This naming is more consistent with other method names, such as +`Role::addChild()`, and also makes clear that more than one parent may be +provided to any given role. + +### getParent becomes getParents + +In line with the previous change, `getParent()` was also renamed to +`getParents()`, which returns an array of `RoleInterface` instances. + +### Removed support for string arguments in Role::addChild + +Version 3 no longer allows adding a child using a string role name; you may only +provide `RoleInterface` instances. + +### Adds getChildren + +Since roles may have multiple children, the method `getChildren()` was added; it +returns an array of `RoleInterface` instances. diff --git a/mkdocs.yml b/mkdocs.yml index a04995d9..59b3b516 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,8 +6,9 @@ pages: - Reference: - Methods: methods.md - Examples: examples.md + - Migration: + - 'v2.X to v3.0': migration/to-v3-0.md site_name: zend-permissions-rbac site_description: zend-permissions-rbac repo_url: 'https://github.com/zendframework/zend-permissions-rbac' copyright: 'Copyright (c) 2016 Zend Technologies USA Inc.' - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c709b112..09d524cd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ @@ -20,14 +20,4 @@ ./src - - - - - - - diff --git a/src/AbstractIterator.php b/src/AbstractIterator.php deleted file mode 100644 index 2f11bf0b..00000000 --- a/src/AbstractIterator.php +++ /dev/null @@ -1,106 +0,0 @@ - - * 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 int|null 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.getchildren.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 deleted file mode 100644 index 26b05ea4..00000000 --- a/src/AbstractRole.php +++ /dev/null @@ -1,145 +0,0 @@ -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' - ); - } - if (! in_array($child, $this->children, true)) { - $this->children[] = $child; - $child->setParent($this); - } - return $this; - } - - /** - * @deprecated deprecated since version 2.6.0, use addParent() instead - * - * @param RoleInterface $parent - * @return RoleInterface - */ - public function setParent($parent) - { - return $this->addParent($parent); - } - - /** - * @return null|RoleInterface|array - */ - public function getParent() - { - if (null === $this->parents) { - return; - } - if (1 === count($this->parents)) { - return $this->parents[0]; - } - return $this->parents; - } - - /** - * @param RoleInterface $parent - * @return RoleInterface - */ - public function addParent($parent) - { - if (! $parent instanceof RoleInterface) { - throw new Exception\InvalidArgumentException( - 'Parent must implement Zend\Permissions\Rbac\RoleInterface' - ); - } - if (null === $this->parents) { - $this->parents = []; - } - if (! in_array($parent, $this->parents, true)) { - $this->parents[] = $parent; - $parent->addChild($this); - } - return $this; - } -} diff --git a/src/Assertion/CallbackAssertion.php b/src/Assertion/CallbackAssertion.php index b3a64a1e..d92d8a60 100644 --- a/src/Assertion/CallbackAssertion.php +++ b/src/Assertion/CallbackAssertion.php @@ -1,45 +1,43 @@ callback = $callback; + // Cast callable to a closure to enforce type safety. + $this->callback = function ( + Rbac $rbac, + RoleInterface $role = null, + string $permission = null + ) use ($callback) : bool { + return $callback($rbac, $role, $permission); + }; } /** - * Assertion method - must return a boolean. - * - * Returns the result of the composed callback. - * - * @param Rbac $rbac - * @return bool + * {@inheritdoc} */ - public function assert(Rbac $rbac) + public function assert(Rbac $rbac, RoleInterface $role, string $permission) : bool { - return (bool) call_user_func($this->callback, $rbac); + return ($this->callback)($rbac, $role, $permission); } } diff --git a/src/AssertionInterface.php b/src/AssertionInterface.php index cddff258..fa6e329e 100644 --- a/src/AssertionInterface.php +++ b/src/AssertionInterface.php @@ -1,21 +1,18 @@ createMissingRoles = $createMissingRoles; - - return $this; } - /** - * @return bool - */ - public function getCreateMissingRoles() + public function getCreateMissingRoles() : bool { return $this->createMissingRoles; } @@ -43,118 +37,115 @@ public function getCreateMissingRoles() /** * Add a child. * - * @param string|RoleInterface $child - * @param array|RoleInterface|null $parents - * @return self - * @throws Exception\InvalidArgumentException + * @param string|RoleInterface $role + * @param null|array|RoleInterface $parents + * @throws Exception\InvalidArgumentException if $role is not a string or + * RoleInterface. */ - public function addRole($child, $parents = null) + public function addRole($role, $parents = null) : void { - if (is_string($child)) { - $child = new Role($child); + if (is_string($role)) { + $role = new Role($role); } - if (! $child instanceof RoleInterface) { + if (! $role instanceof RoleInterface) { throw new Exception\InvalidArgumentException( - 'Child must be a string or implement Zend\Permissions\Rbac\RoleInterface' + 'Role must be a string or implement Zend\Permissions\Rbac\RoleInterface' ); } if ($parents) { - if (! is_array($parents)) { - $parents = [$parents]; - } + $parents = is_array($parents) ? $parents : [$parents]; foreach ($parents as $parent) { if ($this->createMissingRoles && ! $this->hasRole($parent)) { $this->addRole($parent); } - $this->getRole($parent)->addChild($child); + if (is_string($parent)) { + $parent = $this->getRole($parent); + } + $parent->addChild($role); } } - $this->children[] = $child; - - return $this; + $this->roles[$role->getName()] = $role; } /** - * Is a child with $name registered? + * Is a role registered? * - * @param \Zend\Permissions\Rbac\RoleInterface|string $objectOrName - * @return bool + * @param RoleInterface|string $role */ - public function hasRole($objectOrName) + public function hasRole($role) : bool { - try { - $this->getRole($objectOrName); + if (! is_string($role) && ! $role instanceof RoleInterface) { + throw new Exception\InvalidArgumentException( + 'Role must be a string or implement Zend\Permissions\Rbac\RoleInterface' + ); + } - return true; - } catch (Exception\InvalidArgumentException $e) { - return false; + if (is_string($role)) { + return isset($this->roles[$role]); } + + $roleName = $role->getName(); + return isset($this->roles[$roleName]) + && $this->roles[$roleName] === $role; } /** - * Get a child. + * Get a registered role by name * - * @param \Zend\Permissions\Rbac\RoleInterface|string $objectOrName - * @return RoleInterface - * @throws Exception\InvalidArgumentException + * @throws Exception\InvalidArgumentException if role is not found. */ - public function getRole($objectOrName) + public function getRole(string $roleName) : RoleInterface { - if (! is_string($objectOrName) && ! $objectOrName instanceof RoleInterface) { - throw new Exception\InvalidArgumentException( - 'Expected string or implement \Zend\Permissions\Rbac\RoleInterface' - ); - } - - if (is_object($objectOrName)) { - $requiredRole = $objectOrName->getName(); - } else { - $requiredRole = $objectOrName; - } - - $it = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($it as $leaf) { - /** @var RoleInterface $leaf */ - if ($leaf->getName() == $requiredRole) { - return $leaf; - } + if (! isset($this->roles[$roleName])) { + throw new Exception\InvalidArgumentException(sprintf( + 'No role with name "%s" could be found', + $roleName + )); } - - throw new Exception\InvalidArgumentException(sprintf( - 'No role with name "%s" could be found', - is_object($objectOrName) ? $objectOrName->getName() : $objectOrName - )); + return $this->roles[$roleName]; } /** * 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 - * @throws Exception\InvalidArgumentException - * @return bool + * @param RoleInterface|string $role + * @param null|AssertionInterface|Callable $assertion + * @throws Exception\InvalidArgumentException if the role is not found. + * @throws Exception\InvalidArgumentException if the assertion is an invalid type. */ - public function isGranted($role, $permission, $assert = null) + public function isGranted($role, string $permission, $assertion = null) : bool { - $result = $this->getRole($role)->hasPermission($permission); + if (! $this->hasRole($role)) { + throw new Exception\InvalidArgumentException(sprintf( + 'No role with name "%s" could be found', + is_object($role) ? $role->getName() : $role + )); + } - if ($assert) { - if ($assert instanceof AssertionInterface) { - return $result && $assert->assert($this); - } + if (is_string($role)) { + $role = $this->getRole($role); + } - if (is_callable($assert)) { - return $result && $assert($this); - } + $result = $role->hasPermission($permission); + if (false === $result || null === $assertion) { + return $result; + } + if (! $assertion instanceof AssertionInterface + && ! is_callable($assertion) + ) { throw new Exception\InvalidArgumentException( 'Assertions must be a Callable or an instance of Zend\Permissions\Rbac\AssertionInterface' ); } - return $result; + if ($assertion instanceof AssertionInterface) { + return $result && $assertion->assert($this, $role, $permission); + } + + // Callable assertion provided. + return $result && $assertion($this, $role, $permission); } } diff --git a/src/Role.php b/src/Role.php index 18114a41..fa41aa21 100644 --- a/src/Role.php +++ b/src/Role.php @@ -1,21 +1,170 @@ name = $name; } + + /** + * Get the name of the role. + */ + public function getName() : string + { + return $this->name; + } + + /** + * Add a permission to the role. + */ + public function addPermission(string $name) : void + { + $this->permissions[$name] = true; + } + + /** + * Checks if a permission exists for this role or any child roles. + */ + public function hasPermission(string $name) : bool + { + if (isset($this->permissions[$name])) { + return true; + } + + foreach ($this->children as $child) { + if ($child->hasPermission($name)) { + return true; + } + } + + return false; + } + + /** + * Add a child rold. + * + * @throws Exception\CircularReferenceException + */ + public function addChild(RoleInterface $child) : void + { + $childName = $child->getName(); + if ($this->hasAncestor($child)) { + throw new Exception\CircularReferenceException(sprintf( + 'To prevent circular references, you cannot add role "%s" as child', + $childName + )); + } + + if (! isset($this->children[$childName])) { + $this->children[$childName] = $child; + $child->addParent($this); + } + } + + /** + * Check if a role is an ancestor. + */ + protected function hasAncestor(RoleInterface $role) : bool + { + if (isset($this->parents[$role->getName()])) { + return true; + } + + foreach ($this->parents as $parent) { + if ($parent->hasAncestor($role)) { + return true; + } + } + + return false; + } + + /** + * Get all child roles + * + * @return RoleInterface[] + */ + public function getChildren() : array + { + return array_values($this->children); + } + + /** + * Add a parent role. + * + * @throws Exception\CircularReferenceException + */ + public function addParent(RoleInterface $parent) : void + { + $parentName = $parent->getName(); + if ($this->hasDescendant($parent)) { + throw new Exception\CircularReferenceException(sprintf( + 'To prevent circular references, you cannot add role "%s" as parent', + $parentName + )); + } + + if (! isset($this->parents[$parentName])) { + $this->parents[$parentName] = $parent; + $parent->addChild($this); + } + } + + /** + * Check if a role is a descendant. + */ + protected function hasDescendant(RoleInterface $role) : bool + { + if (isset($this->children[$role->getName()])) { + return true; + } + + foreach ($this->children as $child) { + if ($child->hasDescendant($role)) { + return true; + } + } + + return false; + } + + /** + * Get the parent roles. + * + * @return RoleInterface[] + */ + public function getParents() : array + { + return array_values($this->parents); + } } diff --git a/src/RoleInterface.php b/src/RoleInterface.php index e789c53f..542f2cbd 100644 --- a/src/RoleInterface.php +++ b/src/RoleInterface.php @@ -1,57 +1,52 @@ expectException(Rbac\Exception\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid callback provided; not callable'); - new Rbac\Assertion\CallbackAssertion('I am not callable!'); - } - /** * Ensures callback is set in object */ - public function testCallbackIsSet() + public function testCallbackIsDecoratedAsClosure() { - $callback = function () { + $callback = function () { }; - $assert = new Rbac\Assertion\CallbackAssertion($callback); - $this->assertAttributeSame($callback, 'callback', $assert); + $assert = new Rbac\Assertion\CallbackAssertion($callback); + $this->assertAttributeNotSame($callback, 'callback', $assert); + $this->assertAttributeInstanceOf(Closure::class, 'callback', $assert); } /** @@ -39,13 +34,12 @@ public function testCallbackIsSet() */ public function testAssertMethodPassRbacToCallback() { - $rbac = new Rbac\Rbac(); - $that = $this; - $assert = new Rbac\Assertion\CallbackAssertion(function ($rbacArg) use ($that, $rbac) { - $that->assertSame($rbacArg, $rbac); + $rbac = new Rbac\Rbac(); + $assert = new Rbac\Assertion\CallbackAssertion(function ($rbacArg) use ($rbac) { + Assert::assertSame($rbacArg, $rbac); return false; }); - $foo = new Rbac\Role('foo'); + $foo = new Rbac\Role('foo'); $foo->addPermission('can.foo'); $rbac->addRole($foo); $this->assertFalse($rbac->isGranted($foo, 'can.foo', $assert)); @@ -62,7 +56,7 @@ public function testAssertMethod() $assertRoleMatch = function ($role) { return function ($rbac) use ($role) { - return $role->getName() == 'foo'; + return $role->getName() === 'foo'; }; }; @@ -79,4 +73,30 @@ public function testAssertMethod() $this->assertFalse($rbac->isGranted($bar, 'can.foo', $roleNoMatch)); $this->assertTrue($rbac->isGranted($foo, 'can.foo', $roleMatch)); } + + public function testAssertWithCallable() + { + $rbac = new Rbac\Rbac(); + $foo = new Rbac\Role('foo'); + $foo->addPermission('can.foo'); + $rbac->addRole($foo); + + $callable = function ($rbac, $permission, $role) { + return true; + }; + $this->assertTrue($rbac->isGranted('foo', 'can.foo', $callable)); + $this->assertFalse($rbac->isGranted('foo', 'can.bar', $callable)); + } + + public function testAssertWithInvalidValue() + { + $rbac = new Rbac\Rbac(); + $foo = new Rbac\Role('foo'); + $foo->addPermission('can.foo'); + $rbac->addRole($foo); + + $callable = new stdClass(); + $this->expectException(Rbac\Exception\InvalidArgumentException::class); + $rbac->isGranted('foo', 'can.foo', $callable); + } } diff --git a/test/RbacTest.php b/test/RbacTest.php index 8247ad69..4f46459d 100644 --- a/test/RbacTest.php +++ b/test/RbacTest.php @@ -1,20 +1,18 @@ assertEquals(false, $this->rbac->isGranted('bar', 'can.baz')); } + public function testIsGrantedWithInvalidRole() + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->rbac->isGranted('foo', 'permission'); + } + + public function testGetRole() + { + $foo = new Rbac\Role('foo'); + $this->rbac->addRole($foo); + $this->assertEquals($foo, $this->rbac->getRole('foo')); + } + /** * @covers Zend\Permissions\Rbac\Rbac::hasRole() */ @@ -107,9 +118,23 @@ public function testHasRole() // check that the container do not have the string "baz" $this->assertFalse($this->rbac->hasRole('baz')); - // check that we can compare two different objects with same name + // check that 'snafu' role and $snafu are different $this->assertNotEquals($this->rbac->getRole('snafu'), $snafu); - $this->assertTrue($this->rbac->hasRole($snafu)); + $this->assertTrue($this->rbac->hasRole('snafu')); + $this->assertFalse($this->rbac->hasRole($snafu)); + } + + public function testHasRoleWithInvalidElement() + { + $role = new \stdClass(); + $this->expectException(Exception\InvalidArgumentException::class); + $this->rbac->hasRole($role); + } + + public function testGetUndefinedRole() + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->rbac->getRole('foo'); } public function testAddRoleFromString() @@ -131,6 +156,13 @@ public function testAddRoleFromClass() $this->assertInstanceOf('Zend\Permissions\Rbac\Role', $foo2); } + public function testAddRoleNotValid() + { + $foo = new \stdClass(); + $this->expectException(Exception\InvalidArgumentException::class); + $this->rbac->addRole($foo); + } + public function testAddRoleWithParentsUsingRbac() { $foo = new Rbac\Role('foo'); @@ -139,8 +171,8 @@ public function testAddRoleWithParentsUsingRbac() $this->rbac->addRole($foo); $this->rbac->addRole($bar, $foo); - $this->assertEquals($bar->getParent(), $foo); - $this->assertEquals($bar, $foo->getChildren()); + $this->assertEquals($bar->getParents(), [$foo]); + $this->assertEquals([$bar], $foo->getChildren()); } @@ -150,10 +182,11 @@ public function testAddRoleWithAutomaticParentsUsingRbac() $bar = new Rbac\Role('bar'); $this->rbac->setCreateMissingRoles(true); + $this->assertTrue($this->rbac->getCreateMissingRoles()); $this->rbac->addRole($bar, $foo); - $this->assertEquals($bar->getParent(), $foo); - $this->assertEquals($bar, $foo->getChildren()); + $this->assertEquals($bar->getParents(), [$foo]); + $this->assertEquals([$bar], $foo->getChildren()); } /** @@ -162,7 +195,8 @@ public function testAddRoleWithAutomaticParentsUsingRbac() public function testAddCustomChildRole() { $role = $this->getMockForAbstractClass(Rbac\RoleInterface::class); - $this->rbac->setCreateMissingRoles(true)->addRole($role, ['parent']); + $this->rbac->setCreateMissingRoles(true); + $this->rbac->addRole($role, 'parent'); $role->expects($this->any()) ->method('getName') @@ -194,15 +228,15 @@ public function testAddMultipleParentRole() $viewerRole->addPermission('post.view'); $this->rbac->addRole($viewerRole, ['Editor', 'Manager']); - $this->assertEquals('Viewer', $editorRole->getChildren()->getName()); - $this->assertEquals('Viewer', $managerRole->getChildren()->getName()); + $this->assertEquals('Viewer', $editorRole->getChildren()[0]->getName()); + $this->assertEquals('Viewer', $managerRole->getChildren()[0]->getName()); $this->assertTrue($this->rbac->isGranted('Editor', 'post.view')); $this->assertTrue($this->rbac->isGranted('Manager', 'post.view')); - $this->assertEquals($viewerRole->getParent(), [ $editorRole, $managerRole ]); - $this->assertEquals($managerRole->getParent(), $adminRole); - $this->assertNull($editorRole->getParent()); - $this->assertNull($adminRole->getParent()); + $this->assertEquals($viewerRole->getParents(), [$editorRole, $managerRole]); + $this->assertEquals($managerRole->getParents(), [$adminRole]); + $this->assertEmpty($editorRole->getParents()); + $this->assertEmpty($adminRole->getParents()); } public function testAddParentRole() @@ -226,15 +260,29 @@ public function testAddParentRole() $viewerRole->addParent($managerRole); $this->rbac->addRole($viewerRole); - $this->assertEquals('Viewer', $editorRole->getChildren()->getName()); - $this->assertEquals('Viewer', $managerRole->getChildren()->getName()); + // Check roles hierarchy + $this->assertEquals([$viewerRole], $editorRole->getChildren()); + $this->assertEquals([$viewerRole], $managerRole->getChildren()); + $this->assertEquals($viewerRole->getParents(), [$editorRole, $managerRole]); + $this->assertEquals($managerRole->getParents(), [$adminRole]); + $this->assertEmpty($editorRole->getParents()); + $this->assertEmpty($adminRole->getParents()); + + // Check permissions $this->assertTrue($this->rbac->isGranted('Editor', 'post.view')); + $this->assertTrue($this->rbac->isGranted('Editor', 'post.edit')); + $this->assertTrue($this->rbac->isGranted('Viewer', 'post.view')); $this->assertTrue($this->rbac->isGranted('Manager', 'post.view')); - - $this->assertEquals($viewerRole->getParent(), [ $editorRole, $managerRole ]); - $this->assertEquals($managerRole->getParent(), $adminRole); - $this->assertNull($editorRole->getParent()); - $this->assertNull($adminRole->getParent()); + $this->assertTrue($this->rbac->isGranted('Administrator', 'post.view')); + $this->assertTrue($this->rbac->isGranted('Administrator', 'post.publish')); + $this->assertFalse($this->rbac->isGranted('Administrator', 'post.edit')); + $this->assertFalse($this->rbac->isGranted('Manager', 'post.edit')); + $this->assertFalse($this->rbac->isGranted('Viewer', 'post.edit')); + $this->assertFalse($this->rbac->isGranted('Viewer', 'post.publish')); + $this->assertFalse($this->rbac->isGranted('Viewer', 'user.manage')); + $this->assertFalse($this->rbac->isGranted('Editor', 'user.manage')); + $this->assertFalse($this->rbac->isGranted('Editor', 'post.publish')); + $this->assertFalse($this->rbac->isGranted('Manager', 'user.manage')); } public function testAddTwoChildRole() @@ -246,11 +294,8 @@ public function testAddTwoChildRole() $foo->addChild($bar); $foo->addChild($baz); - $this->assertEquals($foo, $bar->getParent()); - $this->assertEquals($bar, $foo->getChildren()); - $foo->next(); - $this->assertEquals($foo, $baz->getParent()); - $this->assertEquals($baz, $foo->getChildren()); + $this->assertEquals([$foo], $bar->getParents()); + $this->assertEquals([$bar, $baz], $foo->getChildren()); } public function testAddSameParent() @@ -261,6 +306,6 @@ public function testAddSameParent() $foo->addParent($bar); $foo->addParent($bar); - $this->assertEquals($bar, $foo->getParent()); + $this->assertEquals([$bar], $foo->getParents()); } } diff --git a/test/RoleTest.php b/test/RoleTest.php new file mode 100644 index 00000000..9ba46379 --- /dev/null +++ b/test/RoleTest.php @@ -0,0 +1,125 @@ +assertInstanceOf(RoleInterface::class, $foo); + } + + public function testGetName() + { + $foo = new Role('foo'); + $this->assertEquals('foo', $foo->getName()); + } + + public function testAddPermission() + { + $foo = new Role('foo'); + $foo->addPermission('bar'); + $foo->addPermission('baz'); + + $this->assertTrue($foo->hasPermission('bar')); + $this->assertTrue($foo->hasPermission('baz')); + } + + public function testInvalidPermission() + { + $perm = new \stdClass(); + $foo = new Role('foo'); + $this->expectException(\TypeError::class); + $foo->addPermission($perm); + } + + public function testAddChild() + { + $foo = new Role('foo'); + $bar = new Role('bar'); + $baz = new Role('baz'); + + $foo->addChild($bar); + $foo->addChild($baz); + + $this->assertEquals($foo->getChildren(), [$bar, $baz]); + } + + public function testAddParent() + { + $foo = new Role('foo'); + $bar = new Role('bar'); + $baz = new Role('baz'); + + $foo->addParent($bar); + $foo->addParent($baz); + $this->assertEquals($foo->getParents(), [$bar, $baz]); + } + + public function testPermissionHierarchy() + { + $foo = new Role('foo'); + $foo->addPermission('foo.permission'); + + $bar = new Role('bar'); + $bar->addPermission('bar.permission'); + + $baz = new Role('baz'); + $baz->addPermission('baz.permission'); + + // create hierarchy bar -> foo -> baz + $foo->addParent($bar); + $foo->addChild($baz); + + $this->assertTrue($bar->hasPermission('bar.permission')); + $this->assertTrue($bar->hasPermission('foo.permission')); + $this->assertTrue($bar->hasPermission('baz.permission')); + + $this->assertFalse($foo->hasPermission('bar.permission')); + $this->assertTrue($foo->hasPermission('foo.permission')); + $this->assertTrue($foo->hasPermission('baz.permission')); + + $this->assertFalse($baz->hasPermission('foo.permission')); + $this->assertFalse($baz->hasPermission('bar.permission')); + $this->assertTrue($baz->hasPermission('baz.permission')); + } + + public function testCircleReferenceWithChild() + { + $foo = new Role('foo'); + $bar = new Role('bar'); + $baz = new Role('baz'); + $baz->addPermission('baz'); + + $foo->addChild($bar); + $bar->addChild($baz); + $this->expectException(Exception\CircularReferenceException::class); + $baz->addChild($foo); + } + + public function testCircleReferenceWithParent() + { + $foo = new Role('foo'); + $bar = new Role('bar'); + $baz = new Role('baz'); + $baz->addPermission('baz'); + + $foo->addParent($bar); + $bar->addParent($baz); + $this->expectException(Exception\CircularReferenceException::class); + $baz->addParent($foo); + } +} diff --git a/test/TestAsset/RoleMustMatchAssertion.php b/test/TestAsset/RoleMustMatchAssertion.php index 387f4715..2a2a6eec 100644 --- a/test/TestAsset/RoleMustMatchAssertion.php +++ b/test/TestAsset/RoleMustMatchAssertion.php @@ -1,41 +1,23 @@ role = $role; - } - - /** - * Assertion method - must return a boolean. - * - * @param Rbac $rbac - * @return bool - */ - public function assert(Rbac $rbac) + public function assert(Rbac $rbac, RoleInterface $role, string $permission) : bool { - return $this->role->getName() == 'foo'; + return $role->getName() === 'foo'; } } diff --git a/test/TestAsset/RoleTest.php b/test/TestAsset/RoleTest.php index b5cdf75d..c72f8289 100644 --- a/test/TestAsset/RoleTest.php +++ b/test/TestAsset/RoleTest.php @@ -1,23 +1,16 @@ name = $name; - } } diff --git a/test/TestAsset/SimpleFalseAssertion.php b/test/TestAsset/SimpleFalseAssertion.php index 0fb41bf2..b30ab83d 100644 --- a/test/TestAsset/SimpleFalseAssertion.php +++ b/test/TestAsset/SimpleFalseAssertion.php @@ -1,29 +1,21 @@