From 8834dfcc0a41701aa9df7a9bb925c3a04e6b840b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 + .travis.yml | 35 + CONTRIBUTING.md | 229 +++ LICENSE.txt | 27 + README.md | 7 + composer.json | 37 + phpunit.xml.dist | 33 + phpunit.xml.travis | 33 + src/Acl.php | 1069 ++++++++++++++ src/Assertion/AssertionInterface.php | 38 + src/Exception/ExceptionInterface.php | 19 + src/Exception/InvalidArgumentException.php | 20 + src/Exception/RuntimeException.php | 22 + src/Resource/GenericResource.php | 57 + src/Resource/ResourceInterface.php | 26 + src/Role/GenericRole.php | 57 + src/Role/Registry.php | 245 ++++ src/Role/RoleInterface.php | 26 + test/AclTest.php | 1306 +++++++++++++++++ test/TestAsset/AssertionZF7973.php | 26 + test/TestAsset/ExtendedAclZF2234.php | 34 + test/TestAsset/MockAssertion.php | 29 + test/TestAsset/UseCase1/Acl.php | 32 + test/TestAsset/UseCase1/BlogPost.php | 22 + test/TestAsset/UseCase1/User.php | 22 + .../UseCase1/UserIsBlogPostOwnerAssertion.php | 31 + test/bootstrap.php | 34 + 30 files changed, 3582 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Acl.php create mode 100644 src/Assertion/AssertionInterface.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Resource/GenericResource.php create mode 100644 src/Resource/ResourceInterface.php create mode 100644 src/Role/GenericRole.php create mode 100644 src/Role/Registry.php create mode 100644 src/Role/RoleInterface.php create mode 100644 test/AclTest.php create mode 100644 test/TestAsset/AssertionZF7973.php create mode 100644 test/TestAsset/ExtendedAclZF2234.php create mode 100644 test/TestAsset/MockAssertion.php create mode 100644 test/TestAsset/UseCase1/Acl.php create mode 100644 test/TestAsset/UseCase1/BlogPost.php create mode 100644 test/TestAsset/UseCase1/User.php create mode 100644 test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..53bda82 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..85dc9a8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cac0a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..bf4b799 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe909ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7b6fd24 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-permissions-acl/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-permissions-acl.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-permissions-acl) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-permissions-acl.git + $ cd zend-permissions-acl + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-permissions-acl.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-permissions-acl.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6eab5aa --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f1c84d --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# zend-permissions-acl + +Provides a lightweight and flexible access control list (ACL) implementation for +privileges management. + +- File issues at https://github.com/zendframework/zend-permissions-acl/issues +- Documentation is at http://framework.zend.com/docs diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5e6d920 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "zendframework/zend-permissions-acl", + "description": "provides a lightweight and flexible access control list (ACL) implementation for privileges management", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "acl" + ], + "homepage": "https://github.com/zendframework/zend-permissions-acl", + "autoload": { + "psr-4": { + "Zend\\Permissions\\Acl": "src/" + } + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "zendframework/zend-servicemanager": "To support Zend\\Permissions\\Acl\\Assertion\\AssertionManager plugin manager usage" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Permissions\\Acl\\": "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/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0075490 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,33 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 0000000..0075490 --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,33 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + diff --git a/src/Acl.php b/src/Acl.php new file mode 100644 index 0000000..7da33f7 --- /dev/null +++ b/src/Acl.php @@ -0,0 +1,1069 @@ + array( + 'allRoles' => array( + 'allPrivileges' => array( + 'type' => self::TYPE_DENY, + 'assert' => null + ), + 'byPrivilegeId' => array() + ), + 'byRoleId' => array() + ), + 'byResourceId' => array() + ); + + /** + * Adds a Role having an identifier unique to the registry + * + * The $parents parameter may be a reference to, or the string identifier for, + * a Role existing in the registry, or $parents may be passed as an array of + * these - mixing string identifiers and objects is ok - to indicate the Roles + * from which the newly added Role will directly inherit. + * + * In order to resolve potential ambiguities with conflicting rules inherited + * from different parents, the most recently added parent takes precedence over + * parents that were previously added. In other words, the first parent added + * will have the least priority, and the last parent added will have the + * highest priority. + * + * @param Role\RoleInterface $role + * @param Role\RoleInterface|string|array $parents + * @throws Exception\InvalidArgumentException + * @return Acl Provides a fluent interface + */ + public function addRole($role, $parents = null) + { + if (is_string($role)) { + $role = new Role\GenericRole($role); + } elseif (!$role instanceof Role\RoleInterface) { + throw new Exception\InvalidArgumentException( + 'addRole() expects $role to be of type Zend\Permissions\Acl\Role\RoleInterface' + ); + } + + + $this->getRoleRegistry()->add($role, $parents); + + return $this; + } + + /** + * Returns the identified Role + * + * The $role parameter can either be a Role or Role identifier. + * + * @param Role\RoleInterface|string $role + * @return Role\RoleInterface + */ + public function getRole($role) + { + return $this->getRoleRegistry()->get($role); + } + + /** + * Returns true if and only if the Role exists in the registry + * + * The $role parameter can either be a Role or a Role identifier. + * + * @param Role\RoleInterface|string $role + * @return boolean + */ + public function hasRole($role) + { + return $this->getRoleRegistry()->has($role); + } + + /** + * Returns true if and only if $role inherits from $inherit + * + * Both parameters may be either a Role or a Role identifier. If + * $onlyParents is true, then $role must inherit directly from + * $inherit in order to return true. By default, this method looks + * through the entire inheritance DAG to determine whether $role + * inherits from $inherit through its ancestor Roles. + * + * @param Role\RoleInterface|string $role + * @param Role\RoleInterface|string $inherit + * @param boolean $onlyParents + * @return boolean + */ + public function inheritsRole($role, $inherit, $onlyParents = false) + { + return $this->getRoleRegistry()->inherits($role, $inherit, $onlyParents); + } + + /** + * Removes the Role from the registry + * + * The $role parameter can either be a Role or a Role identifier. + * + * @param Role\RoleInterface|string $role + * @return Acl Provides a fluent interface + */ + public function removeRole($role) + { + $this->getRoleRegistry()->remove($role); + + if ($role instanceof Role\RoleInterface) { + $roleId = $role->getRoleId(); + } else { + $roleId = $role; + } + + foreach ($this->rules['allResources']['byRoleId'] as $roleIdCurrent => $rules) { + if ($roleId === $roleIdCurrent) { + unset($this->rules['allResources']['byRoleId'][$roleIdCurrent]); + } + } + foreach ($this->rules['byResourceId'] as $resourceIdCurrent => $visitor) { + if (array_key_exists('byRoleId', $visitor)) { + foreach ($visitor['byRoleId'] as $roleIdCurrent => $rules) { + if ($roleId === $roleIdCurrent) { + unset($this->rules['byResourceId'][$resourceIdCurrent]['byRoleId'][$roleIdCurrent]); + } + } + } + } + + return $this; + } + + /** + * Removes all Roles from the registry + * + * @return Acl Provides a fluent interface + */ + public function removeRoleAll() + { + $this->getRoleRegistry()->removeAll(); + + foreach ($this->rules['allResources']['byRoleId'] as $roleIdCurrent => $rules) { + unset($this->rules['allResources']['byRoleId'][$roleIdCurrent]); + } + foreach ($this->rules['byResourceId'] as $resourceIdCurrent => $visitor) { + foreach ($visitor['byRoleId'] as $roleIdCurrent => $rules) { + unset($this->rules['byResourceId'][$resourceIdCurrent]['byRoleId'][$roleIdCurrent]); + } + } + + return $this; + } + + /** + * Adds a Resource having an identifier unique to the ACL + * + * The $parent parameter may be a reference to, or the string identifier for, + * the existing Resource from which the newly added Resource will inherit. + * + * @param Resource\ResourceInterface|string $resource + * @param Resource\ResourceInterface|string $parent + * @throws Exception\InvalidArgumentException + * @return Acl Provides a fluent interface + */ + public function addResource($resource, $parent = null) + { + if (is_string($resource)) { + $resource = new Resource\GenericResource($resource); + } elseif (!$resource instanceof Resource\ResourceInterface) { + throw new Exception\InvalidArgumentException( + 'addResource() expects $resource to be of type Zend\Permissions\Acl\Resource\ResourceInterface' + ); + } + + $resourceId = $resource->getResourceId(); + + if ($this->hasResource($resourceId)) { + throw new Exception\InvalidArgumentException("Resource id '$resourceId' already exists in the ACL"); + } + + $resourceParent = null; + + if (null !== $parent) { + try { + if ($parent instanceof Resource\ResourceInterface) { + $resourceParentId = $parent->getResourceId(); + } else { + $resourceParentId = $parent; + } + $resourceParent = $this->getResource($resourceParentId); + } catch (\Exception $e) { + throw new Exception\InvalidArgumentException(sprintf( + 'Parent Resource id "%s" does not exist', + $resourceParentId + ), 0, $e); + } + $this->resources[$resourceParentId]['children'][$resourceId] = $resource; + } + + $this->resources[$resourceId] = array( + 'instance' => $resource, + 'parent' => $resourceParent, + 'children' => array() + ); + + return $this; + } + + /** + * Returns the identified Resource + * + * The $resource parameter can either be a Resource or a Resource identifier. + * + * @param Resource\ResourceInterface|string $resource + * @throws Exception\InvalidArgumentException + * @return Resource + */ + public function getResource($resource) + { + if ($resource instanceof Resource\ResourceInterface) { + $resourceId = $resource->getResourceId(); + } else { + $resourceId = (string) $resource; + } + + if (!$this->hasResource($resource)) { + throw new Exception\InvalidArgumentException("Resource '$resourceId' not found"); + } + + return $this->resources[$resourceId]['instance']; + } + + /** + * Returns true if and only if the Resource exists in the ACL + * + * The $resource parameter can either be a Resource or a Resource identifier. + * + * @param Resource\ResourceInterface|string $resource + * @return boolean + */ + public function hasResource($resource) + { + if ($resource instanceof Resource\ResourceInterface) { + $resourceId = $resource->getResourceId(); + } else { + $resourceId = (string) $resource; + } + + return isset($this->resources[$resourceId]); + } + + /** + * Returns true if and only if $resource inherits from $inherit + * + * Both parameters may be either a Resource or a Resource identifier. If + * $onlyParent is true, then $resource must inherit directly from + * $inherit in order to return true. By default, this method looks + * through the entire inheritance tree to determine whether $resource + * inherits from $inherit through its ancestor Resources. + * + * @param Resource\ResourceInterface|string $resource + * @param Resource\ResourceInterface|string inherit + * @param boolean $onlyParent + * @throws Exception\InvalidArgumentException + * @return boolean + */ + public function inheritsResource($resource, $inherit, $onlyParent = false) + { + try { + $resourceId = $this->getResource($resource)->getResourceId(); + $inheritId = $this->getResource($inherit)->getResourceId(); + } catch (Exception\ExceptionInterface $e) { + throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + if (null !== $this->resources[$resourceId]['parent']) { + $parentId = $this->resources[$resourceId]['parent']->getResourceId(); + if ($inheritId === $parentId) { + return true; + } elseif ($onlyParent) { + return false; + } + } else { + return false; + } + + while (null !== $this->resources[$parentId]['parent']) { + $parentId = $this->resources[$parentId]['parent']->getResourceId(); + if ($inheritId === $parentId) { + return true; + } + } + + return false; + } + + /** + * Removes a Resource and all of its children + * + * The $resource parameter can either be a Resource or a Resource identifier. + * + * @param Resource\ResourceInterface|string $resource + * @throws Exception\InvalidArgumentException + * @return Acl Provides a fluent interface + */ + public function removeResource($resource) + { + try { + $resourceId = $this->getResource($resource)->getResourceId(); + } catch (Exception\ExceptionInterface $e) { + throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + $resourcesRemoved = array($resourceId); + if (null !== ($resourceParent = $this->resources[$resourceId]['parent'])) { + unset($this->resources[$resourceParent->getResourceId()]['children'][$resourceId]); + } + foreach ($this->resources[$resourceId]['children'] as $childId => $child) { + $this->removeResource($childId); + $resourcesRemoved[] = $childId; + } + + foreach ($resourcesRemoved as $resourceIdRemoved) { + foreach ($this->rules['byResourceId'] as $resourceIdCurrent => $rules) { + if ($resourceIdRemoved === $resourceIdCurrent) { + unset($this->rules['byResourceId'][$resourceIdCurrent]); + } + } + } + + unset($this->resources[$resourceId]); + + return $this; + } + + /** + * Removes all Resources + * + * @return Acl Provides a fluent interface + */ + public function removeResourceAll() + { + foreach ($this->resources as $resourceId => $resource) { + foreach ($this->rules['byResourceId'] as $resourceIdCurrent => $rules) { + if ($resourceId === $resourceIdCurrent) { + unset($this->rules['byResourceId'][$resourceIdCurrent]); + } + } + } + + $this->resources = array(); + + return $this; + } + + /** + * Adds an "allow" rule to the ACL + * + * @param Role\RoleInterface|string|array $roles + * @param Resource\ResourceInterface|string|array $resources + * @param string|array $privileges + * @param Assertion\AssertionInterface $assert + * @return Acl Provides a fluent interface + */ + public function allow($roles = null, $resources = null, $privileges = null, Assertion\AssertionInterface $assert = null) + { + return $this->setRule(self::OP_ADD, self::TYPE_ALLOW, $roles, $resources, $privileges, $assert); + } + + /** + * Adds a "deny" rule to the ACL + * + * @param Role\RoleInterface|string|array $roles + * @param Resource\ResourceInterface|string|array $resources + * @param string|array $privileges + * @param Assertion\AssertionInterface $assert + * @return Acl Provides a fluent interface + */ + public function deny($roles = null, $resources = null, $privileges = null, Assertion\AssertionInterface $assert = null) + { + return $this->setRule(self::OP_ADD, self::TYPE_DENY, $roles, $resources, $privileges, $assert); + } + + /** + * Removes "allow" permissions from the ACL + * + * @param Role\RoleInterface|string|array $roles + * @param Resource\ResourceInterface|string|array $resources + * @param string|array $privileges + * @return Acl Provides a fluent interface + */ + public function removeAllow($roles = null, $resources = null, $privileges = null) + { + return $this->setRule(self::OP_REMOVE, self::TYPE_ALLOW, $roles, $resources, $privileges); + } + + /** + * Removes "deny" restrictions from the ACL + * + * @param Role\RoleInterface|string|array $roles + * @param Resource\ResourceInterface|string|array $resources + * @param string|array $privileges + * @return Acl Provides a fluent interface + */ + public function removeDeny($roles = null, $resources = null, $privileges = null) + { + return $this->setRule(self::OP_REMOVE, self::TYPE_DENY, $roles, $resources, $privileges); + } + + /** + * Performs operations on ACL rules + * + * The $operation parameter may be either OP_ADD or OP_REMOVE, depending on whether the + * user wants to add or remove a rule, respectively: + * + * OP_ADD specifics: + * + * A rule is added that would allow one or more Roles access to [certain $privileges + * upon] the specified Resource(s). + * + * OP_REMOVE specifics: + * + * The rule is removed only in the context of the given Roles, Resources, and privileges. + * Existing rules to which the remove operation does not apply would remain in the + * ACL. + * + * The $type parameter may be either TYPE_ALLOW or TYPE_DENY, depending on whether the + * rule is intended to allow or deny permission, respectively. + * + * The $roles and $resources parameters may be references to, or the string identifiers for, + * existing Resources/Roles, or they may be passed as arrays of these - mixing string identifiers + * and objects is ok - to indicate the Resources and Roles to which the rule applies. If either + * $roles or $resources is null, then the rule applies to all Roles or all Resources, respectively. + * Both may be null in order to work with the default rule of the ACL. + * + * The $privileges parameter may be used to further specify that the rule applies only + * to certain privileges upon the Resource(s) in question. This may be specified to be a single + * privilege with a string, and multiple privileges may be specified as an array of strings. + * + * If $assert is provided, then its assert() method must return true in order for + * the rule to apply. If $assert is provided with $roles, $resources, and $privileges all + * equal to null, then a rule having a type of: + * + * TYPE_ALLOW will imply a type of TYPE_DENY, and + * + * TYPE_DENY will imply a type of TYPE_ALLOW + * + * when the rule's assertion fails. This is because the ACL needs to provide expected + * behavior when an assertion upon the default ACL rule fails. + * + * @param string $operation + * @param string $type + * @param Role\RoleInterface|string|array $roles + * @param Resource\ResourceInterface|string|array $resources + * @param string|array $privileges + * @param Assertion\AssertionInterface $assert + * @throws Exception\InvalidArgumentException + * @return Acl Provides a fluent interface + */ + public function setRule($operation, $type, $roles = null, $resources = null, + $privileges = null, Assertion\AssertionInterface $assert = null + ) { + // ensure that the rule type is valid; normalize input to uppercase + $type = strtoupper($type); + if (self::TYPE_ALLOW !== $type && self::TYPE_DENY !== $type) { + throw new Exception\InvalidArgumentException(sprintf( + 'Unsupported rule type; must be either "%s" or "%s"', + self::TYPE_ALLOW, + self::TYPE_DENY + )); + } + + // ensure that all specified Roles exist; normalize input to array of Role objects or null + if (!is_array($roles)) { + $roles = array($roles); + } elseif (0 === count($roles)) { + $roles = array(null); + } + $rolesTemp = $roles; + $roles = array(); + foreach ($rolesTemp as $role) { + if (null !== $role) { + $roles[] = $this->getRoleRegistry()->get($role); + } else { + $roles[] = null; + } + } + unset($rolesTemp); + + // ensure that all specified Resources exist; normalize input to array of Resource objects or null + if (!is_array($resources)) { + if (null === $resources && count($this->resources) > 0) { + $resources = array_keys($this->resources); + } else { + $resources = array($resources); + } + } elseif (0 === count($resources)) { + $resources = array(null); + } + $resourcesTemp = $resources; + $resources = array(); + foreach ($resourcesTemp as $resource) { + if (null !== $resource) { + $resources[] = $this->getResource($resource); + } else { + $resources[] = null; + } + } + unset($resourcesTemp); + + // normalize privileges to array + if (null === $privileges) { + $privileges = array(); + } elseif (!is_array($privileges)) { + $privileges = array($privileges); + } + + switch ($operation) { + // add to the rules + case self::OP_ADD: + foreach ($resources as $resource) { + foreach ($roles as $role) { + $rules =& $this->getRules($resource, $role, true); + if (0 === count($privileges)) { + $rules['allPrivileges']['type'] = $type; + $rules['allPrivileges']['assert'] = $assert; + if (!isset($rules['byPrivilegeId'])) { + $rules['byPrivilegeId'] = array(); + } + } else { + foreach ($privileges as $privilege) { + $rules['byPrivilegeId'][$privilege]['type'] = $type; + $rules['byPrivilegeId'][$privilege]['assert'] = $assert; + } + } + } + } + break; + + // remove from the rules + case self::OP_REMOVE: + foreach ($resources as $resource) { + foreach ($roles as $role) { + $rules =& $this->getRules($resource, $role); + if (null === $rules) { + continue; + } + if (0 === count($privileges)) { + if (null === $resource && null === $role) { + if ($type === $rules['allPrivileges']['type']) { + $rules = array( + 'allPrivileges' => array( + 'type' => self::TYPE_DENY, + 'assert' => null + ), + 'byPrivilegeId' => array() + ); + } + continue; + } + + if (isset($rules['allPrivileges']['type']) && + $type === $rules['allPrivileges']['type']) + { + unset($rules['allPrivileges']); + } + } else { + foreach ($privileges as $privilege) { + if (isset($rules['byPrivilegeId'][$privilege]) && + $type === $rules['byPrivilegeId'][$privilege]['type']) + { + unset($rules['byPrivilegeId'][$privilege]); + } + } + } + } + } + break; + + default: + throw new Exception\InvalidArgumentException(sprintf( + 'Unsupported operation; must be either "%s" or "%s"', + self::OP_ADD, + self::OP_REMOVE + )); + } + + return $this; + } + + /** + * Returns true if and only if the Role has access to the Resource + * + * The $role and $resource parameters may be references to, or the string identifiers for, + * an existing Resource and Role combination. + * + * If either $role or $resource is null, then the query applies to all Roles or all Resources, + * respectively. Both may be null to query whether the ACL has a "blacklist" rule + * (allow everything to all). By default, Zend_Acl creates a "whitelist" rule (deny + * everything to all), and this method would return false unless this default has + * been overridden (i.e., by executing $acl->allow()). + * + * If a $privilege is not provided, then this method returns false if and only if the + * Role is denied access to at least one privilege upon the Resource. In other words, this + * method returns true if and only if the Role is allowed all privileges on the Resource. + * + * This method checks Role inheritance using a depth-first traversal of the Role registry. + * The highest priority parent (i.e., the parent most recently added) is checked first, + * and its respective parents are checked similarly before the lower-priority parents of + * the Role are checked. + * + * @param Role\RoleInterface|string $role + * @param Resource\ResourceInterface|string $resource + * @param string $privilege + * @return boolean + */ + public function isAllowed($role = null, $resource = null, $privilege = null) + { + // reset role & resource to null + $this->isAllowedRole = null; + $this->isAllowedResource = null; + $this->isAllowedPrivilege = null; + + if (null !== $role) { + // keep track of originally called role + $this->isAllowedRole = $role; + $role = $this->getRoleRegistry()->get($role); + if (!$this->isAllowedRole instanceof Role\RoleInterface) { + $this->isAllowedRole = $role; + } + } + + if (null !== $resource) { + // keep track of originally called resource + $this->isAllowedResource = $resource; + $resource = $this->getResource($resource); + if (!$this->isAllowedResource instanceof Resource\ResourceInterface) { + $this->isAllowedResource = $resource; + } + } + + if (null === $privilege) { + // query on all privileges + do { + // depth-first search on $role if it is not 'allRoles' pseudo-parent + if (null !== $role && null !== ($result = $this->roleDFSAllPrivileges($role, $resource, $privilege))) { + return $result; + } + + // look for rule on 'allRoles' pseudo-parent + if (null !== ($rules = $this->getRules($resource, null))) { + foreach ($rules['byPrivilegeId'] as $privilege => $rule) { + if (self::TYPE_DENY === ($ruleTypeOnePrivilege = $this->getRuleType($resource, null, $privilege))) { + return false; + } + } + if (null !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, null, null))) { + return self::TYPE_ALLOW === $ruleTypeAllPrivileges; + } + } + + // try next Resource + $resource = $this->resources[$resource->getResourceId()]['parent']; + + } while (true); // loop terminates at 'allResources' pseudo-parent + } else { + $this->isAllowedPrivilege = $privilege; + // query on one privilege + do { + // depth-first search on $role if it is not 'allRoles' pseudo-parent + if (null !== $role && null !== ($result = $this->roleDFSOnePrivilege($role, $resource, $privilege))) { + return $result; + } + + // look for rule on 'allRoles' pseudo-parent + if (null !== ($ruleType = $this->getRuleType($resource, null, $privilege))) { + return self::TYPE_ALLOW === $ruleType; + } elseif (null !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, null, null))) { + return self::TYPE_ALLOW === $ruleTypeAllPrivileges; + } + + // try next Resource + $resource = $this->resources[$resource->getResourceId()]['parent']; + + } while (true); // loop terminates at 'allResources' pseudo-parent + } + } + + /** + * Returns the Role registry for this ACL + * + * If no Role registry has been created yet, a new default Role registry + * is created and returned. + * + * @return Role\Registry + */ + protected function getRoleRegistry() + { + if (null === $this->roleRegistry) { + $this->roleRegistry = new Role\Registry(); + } + return $this->roleRegistry; + } + + /** + * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule + * allowing/denying $role access to all privileges upon $resource + * + * This method returns true if a rule is found and allows access. If a rule exists and denies access, + * then this method returns false. If no applicable rule is found, then this method returns null. + * + * @param Role\RoleInterface $role + * @param Resource\ResourceInterface $resource + * @return boolean|null + */ + protected function roleDFSAllPrivileges(Role\RoleInterface $role, Resource\ResourceInterface $resource = null) + { + $dfs = array( + 'visited' => array(), + 'stack' => array() + ); + + if (null !== ($result = $this->roleDFSVisitAllPrivileges($role, $resource, $dfs))) { + return $result; + } + + // This comment is needed due to a strange php-cs-fixer bug + while (null !== ($role = array_pop($dfs['stack']))) { + if (!isset($dfs['visited'][$role->getRoleId()])) { + if (null !== ($result = $this->roleDFSVisitAllPrivileges($role, $resource, $dfs))) { + return $result; + } + } + } + + return null; + } + + /** + * Visits an $role in order to look for a rule allowing/denying $role access to all privileges upon $resource + * + * This method returns true if a rule is found and allows access. If a rule exists and denies access, + * then this method returns false. If no applicable rule is found, then this method returns null. + * + * This method is used by the internal depth-first search algorithm and may modify the DFS data structure. + * + * @param Role\RoleInterface $role + * @param Resource\ResourceInterface $resource + * @param array $dfs + * @return boolean|null + * @throws Exception\RuntimeException + */ + protected function roleDFSVisitAllPrivileges(Role\RoleInterface $role, Resource\ResourceInterface $resource = null, &$dfs = null) + { + if (null === $dfs) { + throw new Exception\RuntimeException('$dfs parameter may not be null'); + } + + if (null !== ($rules = $this->getRules($resource, $role))) { + foreach ($rules['byPrivilegeId'] as $privilege => $rule) { + if (self::TYPE_DENY === ($ruleTypeOnePrivilege = $this->getRuleType($resource, $role, $privilege))) { + return false; + } + } + if (null !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, $role, null))) { + return self::TYPE_ALLOW === $ruleTypeAllPrivileges; + } + } + + $dfs['visited'][$role->getRoleId()] = true; + foreach ($this->getRoleRegistry()->getParents($role) as $roleParentId => $roleParent) { + $dfs['stack'][] = $roleParent; + } + + return null; + } + + /** + * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule + * allowing/denying $role access to a $privilege upon $resource + * + * This method returns true if a rule is found and allows access. If a rule exists and denies access, + * then this method returns false. If no applicable rule is found, then this method returns null. + * + * @param Role\RoleInterface $role + * @param Resource\ResourceInterface $resource + * @param string $privilege + * @return boolean|null + * @throws Exception\RuntimeException + */ + protected function roleDFSOnePrivilege(Role\RoleInterface $role, Resource\ResourceInterface $resource = null, $privilege = null) + { + if (null === $privilege) { + throw new Exception\RuntimeException('$privilege parameter may not be null'); + } + + $dfs = array( + 'visited' => array(), + 'stack' => array() + ); + + if (null !== ($result = $this->roleDFSVisitOnePrivilege($role, $resource, $privilege, $dfs))) { + return $result; + } + + // This comment is needed due to a strange php-cs-fixer bug + while (null !== ($role = array_pop($dfs['stack']))) { + if (!isset($dfs['visited'][$role->getRoleId()])) { + if (null !== ($result = $this->roleDFSVisitOnePrivilege($role, $resource, $privilege, $dfs))) { + return $result; + } + } + } + + return null; + } + + /** + * Visits an $role in order to look for a rule allowing/denying $role access to a $privilege upon $resource + * + * This method returns true if a rule is found and allows access. If a rule exists and denies access, + * then this method returns false. If no applicable rule is found, then this method returns null. + * + * This method is used by the internal depth-first search algorithm and may modify the DFS data structure. + * + * @param Role\RoleInterface $role + * @param Resource\ResourceInterface $resource + * @param string $privilege + * @param array $dfs + * @return boolean|null + * @throws Exception\RuntimeException + */ + protected function roleDFSVisitOnePrivilege(Role\RoleInterface $role, Resource\ResourceInterface $resource = null, + $privilege = null, &$dfs = null + ) { + if (null === $privilege) { + /** + * @see Zend_Acl_Exception + */ + throw new Exception\RuntimeException('$privilege parameter may not be null'); + } + + if (null === $dfs) { + /** + * @see Zend_Acl_Exception + */ + throw new Exception\RuntimeException('$dfs parameter may not be null'); + } + + if (null !== ($ruleTypeOnePrivilege = $this->getRuleType($resource, $role, $privilege))) { + return self::TYPE_ALLOW === $ruleTypeOnePrivilege; + } elseif (null !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, $role, null))) { + return self::TYPE_ALLOW === $ruleTypeAllPrivileges; + } + + $dfs['visited'][$role->getRoleId()] = true; + foreach ($this->getRoleRegistry()->getParents($role) as $roleParentId => $roleParent) { + $dfs['stack'][] = $roleParent; + } + + return null; + } + + /** + * Returns the rule type associated with the specified Resource, Role, and privilege + * combination. + * + * If a rule does not exist or its attached assertion fails, which means that + * the rule is not applicable, then this method returns null. Otherwise, the + * rule type applies and is returned as either TYPE_ALLOW or TYPE_DENY. + * + * If $resource or $role is null, then this means that the rule must apply to + * all Resources or Roles, respectively. + * + * If $privilege is null, then the rule must apply to all privileges. + * + * If all three parameters are null, then the default ACL rule type is returned, + * based on whether its assertion method passes. + * + * @param null|Resource\ResourceInterface $resource + * @param null|Role\RoleInterface $role + * @param null|string $privilege + * @return string|null + */ + protected function getRuleType(Resource\ResourceInterface $resource = null, Role\RoleInterface $role = null, $privilege = null) + { + // get the rules for the $resource and $role + if (null === ($rules = $this->getRules($resource, $role))) { + return null; + } + + // follow $privilege + if (null === $privilege) { + if (isset($rules['allPrivileges'])) { + $rule = $rules['allPrivileges']; + } else { + return null; + } + } elseif (!isset($rules['byPrivilegeId'][$privilege])) { + return null; + } else { + $rule = $rules['byPrivilegeId'][$privilege]; + } + + // check assertion first + if ($rule['assert']) { + $assertion = $rule['assert']; + $assertionValue = $assertion->assert( + $this, + ($this->isAllowedRole instanceof Role\RoleInterface) ? $this->isAllowedRole : $role, + ($this->isAllowedResource instanceof Resource\ResourceInterface) ? $this->isAllowedResource : $resource, + $this->isAllowedPrivilege + ); + } + + if (null === $rule['assert'] || $assertionValue) { + return $rule['type']; + } elseif (null !== $resource || null !== $role || null !== $privilege) { + return null; + } elseif (self::TYPE_ALLOW === $rule['type']) { + return self::TYPE_DENY; + } else { + return self::TYPE_ALLOW; + } + } + + /** + * Returns the rules associated with a Resource and a Role, or null if no such rules exist + * + * If either $resource or $role is null, this means that the rules returned are for all Resources or all Roles, + * respectively. Both can be null to return the default rule set for all Resources and all Roles. + * + * If the $create parameter is true, then a rule set is first created and then returned to the caller. + * + * @param Resource\ResourceInterface $resource + * @param Role\RoleInterface $role + * @param boolean $create + * @return array|null + */ + protected function &getRules(Resource\ResourceInterface $resource = null, Role\RoleInterface $role = null, $create = false) + { + // create a reference to null + $null = null; + $nullRef =& $null; + + // follow $resource + do { + if (null === $resource) { + $visitor =& $this->rules['allResources']; + break; + } + $resourceId = $resource->getResourceId(); + if (!isset($this->rules['byResourceId'][$resourceId])) { + if (!$create) { + return $nullRef; + } + $this->rules['byResourceId'][$resourceId] = array(); + } + $visitor =& $this->rules['byResourceId'][$resourceId]; + } while (false); + + + // follow $role + if (null === $role) { + if (!isset($visitor['allRoles'])) { + if (!$create) { + return $nullRef; + } + $visitor['allRoles']['byPrivilegeId'] = array(); + } + return $visitor['allRoles']; + } + $roleId = $role->getRoleId(); + if (!isset($visitor['byRoleId'][$roleId])) { + if (!$create) { + return $nullRef; + } + $visitor['byRoleId'][$roleId]['byPrivilegeId'] = array(); + } + return $visitor['byRoleId'][$roleId]; + } + + /** + * @return array of registered roles + */ + public function getRoles() + { + return array_keys($this->getRoleRegistry()->getRoles()); + } + + /** + * @return array of registered resources + */ + public function getResources() + { + return array_keys($this->resources); + } +} diff --git a/src/Assertion/AssertionInterface.php b/src/Assertion/AssertionInterface.php new file mode 100644 index 0000000..ce5441d --- /dev/null +++ b/src/Assertion/AssertionInterface.php @@ -0,0 +1,38 @@ +resourceId = (string) $resourceId; + } + + /** + * Defined by ResourceInterface; returns the Resource identifier + * + * @return string + */ + public function getResourceId() + { + return $this->resourceId; + } + + /** + * Defined by ResourceInterface; returns the Resource identifier + * Proxies to getResourceId() + * + * @return string + */ + public function __toString() + { + return $this->getResourceId(); + } +} diff --git a/src/Resource/ResourceInterface.php b/src/Resource/ResourceInterface.php new file mode 100644 index 0000000..56a988f --- /dev/null +++ b/src/Resource/ResourceInterface.php @@ -0,0 +1,26 @@ +roleId = (string) $roleId; + } + + /** + * Defined by RoleInterface; returns the Role identifier + * + * @return string + */ + public function getRoleId() + { + return $this->roleId; + } + + /** + * Defined by RoleInterface; returns the Role identifier + * Proxies to getRoleId() + * + * @return string + */ + public function __toString() + { + return $this->getRoleId(); + } +} diff --git a/src/Role/Registry.php b/src/Role/Registry.php new file mode 100644 index 0000000..f32c31f --- /dev/null +++ b/src/Role/Registry.php @@ -0,0 +1,245 @@ +getRoleId(); + + if ($this->has($roleId)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Role id "%s" already exists in the registry', + $roleId + )); + } + + $roleParents = array(); + + if (null !== $parents) { + if (!is_array($parents)) { + $parents = array($parents); + } + foreach ($parents as $parent) { + try { + if ($parent instanceof RoleInterface) { + $roleParentId = $parent->getRoleId(); + } else { + $roleParentId = $parent; + } + $roleParent = $this->get($roleParentId); + } catch (\Exception $e) { + throw new Exception\InvalidArgumentException(sprintf( + 'Parent Role id "%s" does not exist', + $roleParentId + ), 0, $e); + } + $roleParents[$roleParentId] = $roleParent; + $this->roles[$roleParentId]['children'][$roleId] = $role; + } + } + + $this->roles[$roleId] = array( + 'instance' => $role, + 'parents' => $roleParents, + 'children' => array(), + ); + + return $this; + } + + /** + * Returns the identified Role + * + * The $role parameter can either be a Role or a Role identifier. + * + * @param RoleInterface|string $role + * @throws Exception\InvalidArgumentException + * @return RoleInterface + */ + public function get($role) + { + if ($role instanceof RoleInterface) { + $roleId = $role->getRoleId(); + } else { + $roleId = (string) $role; + } + + if (!$this->has($role)) { + throw new Exception\InvalidArgumentException("Role '$roleId' not found"); + } + + return $this->roles[$roleId]['instance']; + } + + /** + * Returns true if and only if the Role exists in the registry + * + * The $role parameter can either be a Role or a Role identifier. + * + * @param RoleInterface|string $role + * @return boolean + */ + public function has($role) + { + if ($role instanceof RoleInterface) { + $roleId = $role->getRoleId(); + } else { + $roleId = (string) $role; + } + + return isset($this->roles[$roleId]); + } + + /** + * Returns an array of an existing Role's parents + * + * The array keys are the identifiers of the parent Roles, and the values are + * the parent Role instances. The parent Roles are ordered in this array by + * ascending priority. The highest priority parent Role, last in the array, + * corresponds with the parent Role most recently added. + * + * If the Role does not have any parents, then an empty array is returned. + * + * @param RoleInterface|string $role + * @return array + */ + public function getParents($role) + { + $roleId = $this->get($role)->getRoleId(); + + return $this->roles[$roleId]['parents']; + } + + /** + * Returns true if and only if $role inherits from $inherit + * + * Both parameters may be either a Role or a Role identifier. If + * $onlyParents is true, then $role must inherit directly from + * $inherit in order to return true. By default, this method looks + * through the entire inheritance DAG to determine whether $role + * inherits from $inherit through its ancestor Roles. + * + * @param RoleInterface|string $role + * @param RoleInterface|string $inherit + * @param boolean $onlyParents + * @throws Exception\InvalidArgumentException + * @return boolean + */ + public function inherits($role, $inherit, $onlyParents = false) + { + try { + $roleId = $this->get($role)->getRoleId(); + $inheritId = $this->get($inherit)->getRoleId(); + } catch (Exception\ExceptionInterface $e) { + throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + $inherits = isset($this->roles[$roleId]['parents'][$inheritId]); + + if ($inherits || $onlyParents) { + return $inherits; + } + + foreach ($this->roles[$roleId]['parents'] as $parentId => $parent) { + if ($this->inherits($parentId, $inheritId)) { + return true; + } + } + + return false; + } + + /** + * Removes the Role from the registry + * + * The $role parameter can either be a Role or a Role identifier. + * + * @param RoleInterface|string $role + * @throws Exception\InvalidArgumentException + * @return Registry Provides a fluent interface + */ + public function remove($role) + { + try { + $roleId = $this->get($role)->getRoleId(); + } catch (Exception\ExceptionInterface $e) { + throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + foreach ($this->roles[$roleId]['children'] as $childId => $child) { + unset($this->roles[$childId]['parents'][$roleId]); + } + foreach ($this->roles[$roleId]['parents'] as $parentId => $parent) { + unset($this->roles[$parentId]['children'][$roleId]); + } + + unset($this->roles[$roleId]); + + return $this; + } + + /** + * Removes all Roles from the registry + * + * @return Registry Provides a fluent interface + */ + public function removeAll() + { + $this->roles = array(); + + return $this; + } + + /** + * Get all roles in the registry + * + * @return array + */ + public function getRoles() + { + return $this->roles; + } +} diff --git a/src/Role/RoleInterface.php b/src/Role/RoleInterface.php new file mode 100644 index 0000000..862a718 --- /dev/null +++ b/src/Role/RoleInterface.php @@ -0,0 +1,26 @@ +_acl = new Acl\Acl(); + } + + /** + * Ensures that basic addition and retrieval of a single Role works + * + * @return void + */ + public function testRoleRegistryAddAndGetOne() + { + $roleGuest = new Role\GenericRole('guest'); + + $role = $this->_acl->addRole($roleGuest) + ->getRole($roleGuest->getRoleId()); + $this->assertTrue($roleGuest === $role); + $role = $this->_acl->getRole($roleGuest); + $this->assertTrue($roleGuest === $role); + } + + /** + * Ensures that basic addition and retrieval of a single Resource works + */ + public function testRoleAddAndGetOneByString() + { + $role = $this->_acl->addRole('area') + ->getRole('area'); + $this->assertInstanceOf('Zend\Permissions\Acl\Role\RoleInterface', $role); + $this->assertEquals('area', $role->getRoleId()); + } + + /** + * Ensures that basic removal of a single Role works + * + * @return void + */ + public function testRoleRegistryRemoveOne() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->removeRole($roleGuest); + $this->assertFalse($this->_acl->hasRole($roleGuest)); + } + + /** + * Ensures that an exception is thrown when a non-existent Role is specified for removal + * + * @return void + */ + public function testRoleRegistryRemoveOneNonExistent() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', 'not found'); + $this->_acl->removeRole('nonexistent'); + } + + /** + * Ensures that removal of all Roles works + * + * @return void + */ + public function testRoleRegistryRemoveAll() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->removeRoleAll(); + $this->assertFalse($this->_acl->hasRole($roleGuest)); + } + + /** + * Ensures that an exception is thrown when a non-existent Role is specified as a parent upon Role addition + * + * @return void + */ + public function testRoleRegistryAddInheritsNonExistent() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException'); + $this->_acl->addRole(new Role\GenericRole('guest'), 'nonexistent'); + } + + /** + * Ensures that an exception is thrown when a not Role is passed + * + * @return void + */ + public function testRoleRegistryAddNotRole() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', + 'addRole() expects $role to be of type Zend\Permissions\Acl\Role'); + $this->_acl->addRole(new \stdClass, 'guest'); + } + + /** + * Ensures that an exception is thrown when a non-existent Role is specified to each parameter of inherits() + * + * @return void + */ + public function testRoleRegistryInheritsNonExistent() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest); + try { + $this->_acl->inheritsRole('nonexistent', $roleGuest); + $this->fail('Expected Zend\Permissions\Acl\Role\Exception not thrown upon specifying a non-existent child Role'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + try { + $this->_acl->inheritsRole($roleGuest, 'nonexistent'); + $this->fail('Expected Zend\Permissions\Acl\Role\Exception not thrown upon specifying a non-existent child Role'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + } + + /** + * Tests basic Role inheritance + * + * @return void + */ + public function testRoleRegistryInherits() + { + $roleGuest = new Role\GenericRole('guest'); + $roleMember = new Role\GenericRole('member'); + $roleEditor = new Role\GenericRole('editor'); + $roleRegistry = new Role\Registry(); + $roleRegistry->add($roleGuest) + ->add($roleMember, $roleGuest->getRoleId()) + ->add($roleEditor, $roleMember); + $this->assertTrue(0 === count($roleRegistry->getParents($roleGuest))); + $roleMemberParents = $roleRegistry->getParents($roleMember); + $this->assertTrue(1 === count($roleMemberParents)); + $this->assertTrue(isset($roleMemberParents['guest'])); + $roleEditorParents = $roleRegistry->getParents($roleEditor); + $this->assertTrue(1 === count($roleEditorParents)); + $this->assertTrue(isset($roleEditorParents['member'])); + $this->assertTrue($roleRegistry->inherits($roleMember, $roleGuest, true)); + $this->assertTrue($roleRegistry->inherits($roleEditor, $roleMember, true)); + $this->assertTrue($roleRegistry->inherits($roleEditor, $roleGuest)); + $this->assertFalse($roleRegistry->inherits($roleGuest, $roleMember)); + $this->assertFalse($roleRegistry->inherits($roleMember, $roleEditor)); + $this->assertFalse($roleRegistry->inherits($roleGuest, $roleEditor)); + $roleRegistry->remove($roleMember); + $this->assertTrue(0 === count($roleRegistry->getParents($roleEditor))); + $this->assertFalse($roleRegistry->inherits($roleEditor, $roleGuest)); + } + + /** + * Tests basic Role multiple inheritance + * + * @return void + */ + public function testRoleRegistryInheritsMultiple() + { + $roleParent1 = new Role\GenericRole('parent1'); + $roleParent2 = new Role\GenericRole('parent2'); + $roleChild = new Role\GenericRole('child'); + $roleRegistry = new Role\Registry(); + $roleRegistry->add($roleParent1) + ->add($roleParent2) + ->add($roleChild, array($roleParent1, $roleParent2)); + $roleChildParents = $roleRegistry->getParents($roleChild); + $this->assertTrue(2 === count($roleChildParents)); + $i = 1; + foreach ($roleChildParents as $roleParentId => $roleParent) { + $this->assertTrue("parent$i" === $roleParentId); + $i++; + } + $this->assertTrue($roleRegistry->inherits($roleChild, $roleParent1)); + $this->assertTrue($roleRegistry->inherits($roleChild, $roleParent2)); + $roleRegistry->remove($roleParent1); + $roleChildParents = $roleRegistry->getParents($roleChild); + $this->assertTrue(1 === count($roleChildParents)); + $this->assertTrue(isset($roleChildParents['parent2'])); + $this->assertTrue($roleRegistry->inherits($roleChild, $roleParent2)); + } + + /** + * Ensures that the same Role cannot be registered more than once to the registry + * + * @return void + */ + public function testRoleRegistryDuplicate() + { + $roleGuest = new Role\GenericRole('guest'); + $roleRegistry = new Role\Registry(); + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', 'already exists'); + $roleRegistry->add($roleGuest) + ->add($roleGuest); + } + + /** + * Ensures that two Roles having the same ID cannot be registered + * + * @return void + */ + public function testRoleRegistryDuplicateId() + { + $roleGuest1 = new Role\GenericRole('guest'); + $roleGuest2 = new Role\GenericRole('guest'); + $roleRegistry = new Role\Registry(); + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', 'already exists'); + $roleRegistry->add($roleGuest1) + ->add($roleGuest2); + } + + /** + * Ensures that basic addition and retrieval of a single Resource works + * + * @return void + */ + public function testResourceAddAndGetOne() + { + $resourceArea = new Resource\GenericResource('area'); + $resource = $this->_acl->addResource($resourceArea) + ->getResource($resourceArea->getResourceId()); + $this->assertTrue($resourceArea === $resource); + $resource = $this->_acl->getResource($resourceArea); + $this->assertTrue($resourceArea === $resource); + } + + /** + * Ensures that basic addition and retrieval of a single Resource works + */ + public function testResourceAddAndGetOneByString() + { + $resource = $this->_acl->addResource('area') + ->getResource('area'); + $this->assertInstanceOf('Zend\Permissions\Acl\Resource\ResourceInterface', $resource); + $this->assertEquals('area', $resource->getResourceId()); + } + + /** + * Ensures that basic addition and retrieval of a single Resource works + * + * @group ZF-1167 + */ + public function testResourceAddAndGetOneWithAddResourceMethod() + { + $resourceArea = new Resource\GenericResource('area'); + $resource = $this->_acl->addResource($resourceArea) + ->getResource($resourceArea->getResourceId()); + $this->assertTrue($resourceArea === $resource); + $resource = $this->_acl->getResource($resourceArea); + $this->assertTrue($resourceArea === $resource); + } + + /** + * Ensures that basic removal of a single Resource works + * + * @return void + */ + public function testResourceRemoveOne() + { + $resourceArea = new Resource\GenericResource('area'); + $this->_acl->addResource($resourceArea) + ->removeResource($resourceArea); + $this->assertFalse($this->_acl->hasResource($resourceArea)); + } + + /** + * Ensures that an exception is thrown when a non-existent Resource is specified for removal + * + * @return void + */ + public function testResourceRemoveOneNonExistent() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\ExceptionInterface', 'not found'); + $this->_acl->removeResource('nonexistent'); + } + + /** + * Ensures that removal of all Resources works + * + * @return void + */ + public function testResourceRemoveAll() + { + $resourceArea = new Resource\GenericResource('area'); + $this->_acl->addResource($resourceArea) + ->removeResourceAll(); + $this->assertFalse($this->_acl->hasResource($resourceArea)); + } + + /** + * Ensures that an exception is thrown when a non-existent Resource is specified as a parent upon Resource addition + * + * @return void + */ + public function testResourceAddInheritsNonExistent() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', 'does not exist'); + $this->_acl->addResource(new Resource\GenericResource('area'), 'nonexistent'); + } + + /** + * Ensures that an exception is thrown when a not Resource is passed + * + * @return void + */ + public function testResourceRegistryAddNotResource() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\InvalidArgumentException', + 'addResource() expects $resource to be of type Zend\Permissions\Acl\Resource'); + $this->_acl->addResource(new \stdClass); + } + + /** + * Ensures that an exception is thrown when a non-existent Resource is specified to each parameter of inherits() + * + * @return void + */ + public function testResourceInheritsNonExistent() + { + $resourceArea = new Resource\GenericResource('area'); + $this->_acl->addResource($resourceArea); + try { + $this->_acl->inheritsResource('nonexistent', $resourceArea); + $this->fail('Expected Zend\Permissions\Acl\Exception\ExceptionInterface not thrown upon specifying a non-existent child Resource'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertContains('not found', $e->getMessage()); + } + try { + $this->_acl->inheritsResource($resourceArea, 'nonexistent'); + $this->fail('Expected Zend\Permissions\Acl\Exception\ExceptionInterface not thrown upon specifying a non-existent parent Resource'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertContains('not found', $e->getMessage()); + } + } + + /** + * Tests basic Resource inheritance + * + * @return void + */ + public function testResourceInherits() + { + $resourceCity = new Resource\GenericResource('city'); + $resourceBuilding = new Resource\GenericResource('building'); + $resourceRoom = new Resource\GenericResource('room'); + $this->_acl->addResource($resourceCity) + ->addResource($resourceBuilding, $resourceCity->getResourceId()) + ->addResource($resourceRoom, $resourceBuilding); + $this->assertTrue($this->_acl->inheritsResource($resourceBuilding, $resourceCity, true)); + $this->assertTrue($this->_acl->inheritsResource($resourceRoom, $resourceBuilding, true)); + $this->assertTrue($this->_acl->inheritsResource($resourceRoom, $resourceCity)); + $this->assertFalse($this->_acl->inheritsResource($resourceCity, $resourceBuilding)); + $this->assertFalse($this->_acl->inheritsResource($resourceBuilding, $resourceRoom)); + $this->assertFalse($this->_acl->inheritsResource($resourceCity, $resourceRoom)); + $this->_acl->removeResource($resourceBuilding); + $this->assertFalse($this->_acl->hasResource($resourceRoom)); + } + + /** + * Ensures that the same Resource cannot be added more than once + * + * @return void + */ + public function testResourceDuplicate() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\ExceptionInterface', 'already exists'); + $resourceArea = new Resource\GenericResource('area'); + $this->_acl->addResource($resourceArea) + ->addResource($resourceArea); + } + + /** + * Ensures that two Resources having the same ID cannot be added + * + * @return void + */ + public function testResourceDuplicateId() + { + $this->setExpectedException('Zend\Permissions\Acl\Exception\ExceptionInterface', 'already exists'); + $resourceArea1 = new Resource\GenericResource('area'); + $resourceArea2 = new Resource\GenericResource('area'); + $this->_acl->addResource($resourceArea1) + ->addResource($resourceArea2); + } + + /** + * Ensures that an exception is thrown when a non-existent Role and Resource parameters are specified to isAllowed() + * + * @return void + */ + public function testIsAllowedNonExistent() + { + try { + $this->_acl->isAllowed('nonexistent'); + $this->fail('Expected Zend\Permissions\Acl\Role\Exception\ExceptionInterface not thrown upon non-existent Role'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + try { + $this->_acl->isAllowed(null, 'nonexistent'); + $this->fail('Expected Zend\Permissions\Acl\Exception\ExceptionInterface not thrown upon non-existent Resource'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + } + + /** + * Ensures that by default, Zend_Acl denies access to everything by all + * + * @return void + */ + public function testDefaultDeny() + { + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that the default rule obeys its assertion + * + * @return void + */ + public function testDefaultAssert() + { + $this->_acl->deny(null, null, null, new TestAsset\MockAssertion(false)); + $this->assertTrue($this->_acl->isAllowed()); + $this->assertTrue($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that ACL-wide rules (all Roles, Resources, and privileges) work properly + * + * @return void + */ + public function testDefaultRuleSet() + { + $this->_acl->allow(); + $this->assertTrue($this->_acl->isAllowed()); + $this->_acl->deny(); + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that by default, Zend_Acl denies access to a privilege on anything by all + * + * @return void + */ + public function testDefaultPrivilegeDeny() + { + $this->assertFalse($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that ACL-wide rules apply to privileges + * + * @return void + */ + public function testDefaultRuleSetPrivilege() + { + $this->_acl->allow(); + $this->assertTrue($this->_acl->isAllowed(null, null, 'somePrivilege')); + $this->_acl->deny(); + $this->assertFalse($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that a privilege allowed for all Roles upon all Resources works properly + * + * @return void + */ + public function testPrivilegeAllow() + { + $this->_acl->allow(null, null, 'somePrivilege'); + $this->assertTrue($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that a privilege denied for all Roles upon all Resources works properly + * + * @return void + */ + public function testPrivilegeDeny() + { + $this->_acl->allow(); + $this->_acl->deny(null, null, 'somePrivilege'); + $this->assertFalse($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that multiple privileges work properly + * + * @return void + */ + public function testPrivileges() + { + $this->_acl->allow(null, null, array('p1', 'p2', 'p3')); + $this->assertTrue($this->_acl->isAllowed(null, null, 'p1')); + $this->assertTrue($this->_acl->isAllowed(null, null, 'p2')); + $this->assertTrue($this->_acl->isAllowed(null, null, 'p3')); + $this->assertFalse($this->_acl->isAllowed(null, null, 'p4')); + $this->_acl->deny(null, null, 'p1'); + $this->assertFalse($this->_acl->isAllowed(null, null, 'p1')); + $this->_acl->deny(null, null, array('p2', 'p3')); + $this->assertFalse($this->_acl->isAllowed(null, null, 'p2')); + $this->assertFalse($this->_acl->isAllowed(null, null, 'p3')); + } + + /** + * Ensures that assertions on privileges work properly + * + * @return void + */ + public function testPrivilegeAssert() + { + $this->_acl->allow(null, null, 'somePrivilege', new TestAsset\MockAssertion(true)); + $this->assertTrue($this->_acl->isAllowed(null, null, 'somePrivilege')); + $this->_acl->allow(null, null, 'somePrivilege', new TestAsset\MockAssertion(false)); + $this->assertFalse($this->_acl->isAllowed(null, null, 'somePrivilege')); + } + + /** + * Ensures that by default, Zend_Acl denies access to everything for a particular Role + * + * @return void + */ + public function testRoleDefaultDeny() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest); + $this->assertFalse($this->_acl->isAllowed($roleGuest)); + } + + /** + * Ensures that ACL-wide rules (all Resources and privileges) work properly for a particular Role + * + * @return void + */ + public function testRoleDefaultRuleSet() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest); + $this->assertTrue($this->_acl->isAllowed($roleGuest)); + $this->_acl->deny($roleGuest); + $this->assertFalse($this->_acl->isAllowed($roleGuest)); + } + + /** + * Ensures that by default, Zend_Acl denies access to a privilege on anything for a particular Role + * + * @return void + */ + public function testRoleDefaultPrivilegeDeny() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + } + + /** + * Ensures that ACL-wide rules apply to privileges for a particular Role + * + * @return void + */ + public function testRoleDefaultRuleSetPrivilege() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + $this->_acl->deny($roleGuest); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + } + + /** + * Ensures that a privilege allowed for a particular Role upon all Resources works properly + * + * @return void + */ + public function testRolePrivilegeAllow() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest, null, 'somePrivilege'); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + } + + /** + * Ensures that a privilege denied for a particular Role upon all Resources works properly + * + * @return void + */ + public function testRolePrivilegeDeny() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest) + ->deny($roleGuest, null, 'somePrivilege'); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + } + + /** + * Ensures that multiple privileges work properly for a particular Role + * + * @return void + */ + public function testRolePrivileges() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest, null, array('p1', 'p2', 'p3')); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'p1')); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'p2')); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'p3')); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'p4')); + $this->_acl->deny($roleGuest, null, 'p1'); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'p1')); + $this->_acl->deny($roleGuest, null, array('p2', 'p3')); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'p2')); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'p3')); + } + + /** + * Ensures that assertions on privileges work properly for a particular Role + * + * @return void + */ + public function testRolePrivilegeAssert() + { + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest) + ->allow($roleGuest, null, 'somePrivilege', new TestAsset\MockAssertion(true)); + $this->assertTrue($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + $this->_acl->allow($roleGuest, null, 'somePrivilege', new TestAsset\MockAssertion(false)); + $this->assertFalse($this->_acl->isAllowed($roleGuest, null, 'somePrivilege')); + } + + /** + * Ensures that removing the default deny rule results in default deny rule + * + * @return void + */ + public function testRemoveDefaultDeny() + { + $this->assertFalse($this->_acl->isAllowed()); + $this->_acl->removeDeny(); + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that removing the default deny rule results in assertion method being removed + * + * @return void + */ + public function testRemoveDefaultDenyAssert() + { + $this->_acl->deny(null, null, null, new TestAsset\MockAssertion(false)); + $this->assertTrue($this->_acl->isAllowed()); + $this->_acl->removeDeny(); + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that removing the default allow rule results in default deny rule being assigned + * + * @return void + */ + public function testRemoveDefaultAllow() + { + $this->_acl->allow(); + $this->assertTrue($this->_acl->isAllowed()); + $this->_acl->removeAllow(); + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that removing non-existent default allow rule does nothing + * + * @return void + */ + public function testRemoveDefaultAllowNonExistent() + { + $this->_acl->removeAllow(); + $this->assertFalse($this->_acl->isAllowed()); + } + + /** + * Ensures that removing non-existent default deny rule does nothing + * + * @return void + */ + public function testRemoveDefaultDenyNonExistent() + { + $this->_acl->allow() + ->removeDeny(); + $this->assertTrue($this->_acl->isAllowed()); + } + + /** + * Ensures that for a particular Role, a deny rule on a specific Resource is honored before an allow rule + * on the entire ACL + * + * @return void + */ + public function testRoleDefaultAllowRuleWithResourceDenyRule() + { + $this->_acl->addRole(new Role\GenericRole('guest')) + ->addRole(new Role\GenericRole('staff'), 'guest') + ->addResource(new Resource\GenericResource('area1')) + ->addResource(new Resource\GenericResource('area2')) + ->deny() + ->allow('staff') + ->deny('staff', array('area1', 'area2')); + $this->assertFalse($this->_acl->isAllowed('staff', 'area1')); + } + + /** + * Ensures that for a particular Role, a deny rule on a specific privilege is honored before an allow + * rule on the entire ACL + * + * @return void + */ + public function testRoleDefaultAllowRuleWithPrivilegeDenyRule() + { + $this->_acl->addRole(new Role\GenericRole('guest')) + ->addRole(new Role\GenericRole('staff'), 'guest') + ->deny() + ->allow('staff') + ->deny('staff', null, array('privilege1', 'privilege2')); + $this->assertFalse($this->_acl->isAllowed('staff', null, 'privilege1')); + } + + /** + * Ensure that basic rule removal works + * + * @return void + */ + public function testRulesRemove() + { + $this->_acl->allow(null, null, array('privilege1', 'privilege2')); + $this->assertFalse($this->_acl->isAllowed()); + $this->assertTrue($this->_acl->isAllowed(null, null, 'privilege1')); + $this->assertTrue($this->_acl->isAllowed(null, null, 'privilege2')); + $this->_acl->removeAllow(null, null, 'privilege1'); + $this->assertFalse($this->_acl->isAllowed(null, null, 'privilege1')); + $this->assertTrue($this->_acl->isAllowed(null, null, 'privilege2')); + } + + /** + * Ensures that removal of a Role results in its rules being removed + * + * @return void + */ + public function testRuleRoleRemove() + { + $this->_acl->addRole(new Role\GenericRole('guest')) + ->allow('guest'); + $this->assertTrue($this->_acl->isAllowed('guest')); + $this->_acl->removeRole('guest'); + try { + $this->_acl->isAllowed('guest'); + $this->fail('Expected Zend\Permissions\Acl\Role\Exception not thrown upon isAllowed() on non-existent Role'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + $this->_acl->addRole(new Role\GenericRole('guest')); + $this->assertFalse($this->_acl->isAllowed('guest')); + } + + /** + * Ensures that removal of all Roles results in Role-specific rules being removed + * + * @return void + */ + public function testRuleRoleRemoveAll() + { + $this->_acl->addRole(new Role\GenericRole('guest')) + ->allow('guest'); + $this->assertTrue($this->_acl->isAllowed('guest')); + $this->_acl->removeRoleAll(); + try { + $this->_acl->isAllowed('guest'); + $this->fail('Expected Zend\Permissions\Acl\Role\Exception not thrown upon isAllowed() on non-existent Role'); + } catch (Acl\Exception\InvalidArgumentException $e) { + $this->assertContains('not found', $e->getMessage()); + } + $this->_acl->addRole(new Role\GenericRole('guest')); + $this->assertFalse($this->_acl->isAllowed('guest')); + } + + /** + * Ensures that removal of a Resource results in its rules being removed + * + * @return void + */ + public function testRulesResourceRemove() + { + $this->_acl->addResource(new Resource\GenericResource('area')) + ->allow(null, 'area'); + $this->assertTrue($this->_acl->isAllowed(null, 'area')); + $this->_acl->removeResource('area'); + try { + $this->_acl->isAllowed(null, 'area'); + $this->fail('Expected Zend\Permissions\Acl\Exception not thrown upon isAllowed() on non-existent Resource'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertContains('not found', $e->getMessage()); + } + $this->_acl->addResource(new Resource\GenericResource('area')); + $this->assertFalse($this->_acl->isAllowed(null, 'area')); + } + + /** + * Ensures that removal of all Resources results in Resource-specific rules being removed + * + * @return void + */ + public function testRulesResourceRemoveAll() + { + $this->_acl->addResource(new Resource\GenericResource('area')) + ->allow(null, 'area'); + $this->assertTrue($this->_acl->isAllowed(null, 'area')); + $this->_acl->removeResourceAll(); + try { + $this->_acl->isAllowed(null, 'area'); + $this->fail('Expected Zend\Permissions\Acl\Exception\ExceptionInterface not thrown upon isAllowed() on non-existent Resource'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertContains('not found', $e->getMessage()); + } + $this->_acl->addResource(new Resource\GenericResource('area')); + $this->assertFalse($this->_acl->isAllowed(null, 'area')); + } + + /** + * Ensures that an example for a content management system is operable + * + * @return void + */ + public function testCMSExample() + { + // Add some roles to the Role registry + $this->_acl->addRole(new Role\GenericRole('guest')) + ->addRole(new Role\GenericRole('staff'), 'guest') // staff inherits permissions from guest + ->addRole(new Role\GenericRole('editor'), 'staff') // editor inherits permissions from staff + ->addRole(new Role\GenericRole('administrator')); + + // Guest may only view content + $this->_acl->allow('guest', null, 'view'); + + // Staff inherits view privilege from guest, but also needs additional privileges + $this->_acl->allow('staff', null, array('edit', 'submit', 'revise')); + + // Editor inherits view, edit, submit, and revise privileges, but also needs additional privileges + $this->_acl->allow('editor', null, array('publish', 'archive', 'delete')); + + // Administrator inherits nothing but is allowed all privileges + $this->_acl->allow('administrator'); + + // Access control checks based on above permission sets + + $this->assertTrue($this->_acl->isAllowed('guest', null, 'view')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'edit')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'submit')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'revise')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'publish')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'archive')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'delete')); + $this->assertFalse($this->_acl->isAllowed('guest', null, 'unknown')); + $this->assertFalse($this->_acl->isAllowed('guest')); + + $this->assertTrue($this->_acl->isAllowed('staff', null, 'view')); + $this->assertTrue($this->_acl->isAllowed('staff', null, 'edit')); + $this->assertTrue($this->_acl->isAllowed('staff', null, 'submit')); + $this->assertTrue($this->_acl->isAllowed('staff', null, 'revise')); + $this->assertFalse($this->_acl->isAllowed('staff', null, 'publish')); + $this->assertFalse($this->_acl->isAllowed('staff', null, 'archive')); + $this->assertFalse($this->_acl->isAllowed('staff', null, 'delete')); + $this->assertFalse($this->_acl->isAllowed('staff', null, 'unknown')); + $this->assertFalse($this->_acl->isAllowed('staff')); + + $this->assertTrue($this->_acl->isAllowed('editor', null, 'view')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'edit')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'submit')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'revise')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'publish')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'archive')); + $this->assertTrue($this->_acl->isAllowed('editor', null, 'delete')); + $this->assertFalse($this->_acl->isAllowed('editor', null, 'unknown')); + $this->assertFalse($this->_acl->isAllowed('editor')); + + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'view')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'edit')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'submit')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'revise')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'publish')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'archive')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'delete')); + $this->assertTrue($this->_acl->isAllowed('administrator', null, 'unknown')); + $this->assertTrue($this->_acl->isAllowed('administrator')); + + // Some checks on specific areas, which inherit access controls from the root ACL node + $this->_acl->addResource(new Resource\GenericResource('newsletter')) + ->addResource(new Resource\GenericResource('pending'), 'newsletter') + ->addResource(new Resource\GenericResource('gallery')) + ->addResource(new Resource\GenericResource('profiles', 'gallery')) + ->addResource(new Resource\GenericResource('config')) + ->addResource(new Resource\GenericResource('hosts'), 'config'); + $this->assertTrue($this->_acl->isAllowed('guest', 'pending', 'view')); + $this->assertTrue($this->_acl->isAllowed('staff', 'profiles', 'revise')); + $this->assertTrue($this->_acl->isAllowed('staff', 'pending', 'view')); + $this->assertTrue($this->_acl->isAllowed('staff', 'pending', 'edit')); + $this->assertFalse($this->_acl->isAllowed('staff', 'pending', 'publish')); + $this->assertFalse($this->_acl->isAllowed('staff', 'pending')); + $this->assertFalse($this->_acl->isAllowed('editor', 'hosts', 'unknown')); + $this->assertTrue($this->_acl->isAllowed('administrator', 'pending')); + + // Add a new group, marketing, which bases its permissions on staff + $this->_acl->addRole(new Role\GenericRole('marketing'), 'staff'); + + // Refine the privilege sets for more specific needs + + // Allow marketing to publish and archive newsletters + $this->_acl->allow('marketing', 'newsletter', array('publish', 'archive')); + + // Allow marketing to publish and archive latest news + $this->_acl->addResource(new Resource\GenericResource('news')) + ->addResource(new Resource\GenericResource('latest'), 'news'); + $this->_acl->allow('marketing', 'latest', array('publish', 'archive')); + + // Deny staff (and marketing, by inheritance) rights to revise latest news + $this->_acl->deny('staff', 'latest', 'revise'); + + // Deny everyone access to archive news announcements + $this->_acl->addResource(new Resource\GenericResource('announcement'), 'news'); + $this->_acl->deny(null, 'announcement', 'archive'); + + // Access control checks for the above refined permission sets + + $this->assertTrue($this->_acl->isAllowed('marketing', null, 'view')); + $this->assertTrue($this->_acl->isAllowed('marketing', null, 'edit')); + $this->assertTrue($this->_acl->isAllowed('marketing', null, 'submit')); + $this->assertTrue($this->_acl->isAllowed('marketing', null, 'revise')); + $this->assertFalse($this->_acl->isAllowed('marketing', null, 'publish')); + $this->assertFalse($this->_acl->isAllowed('marketing', null, 'archive')); + $this->assertFalse($this->_acl->isAllowed('marketing', null, 'delete')); + $this->assertFalse($this->_acl->isAllowed('marketing', null, 'unknown')); + $this->assertFalse($this->_acl->isAllowed('marketing')); + + $this->assertTrue($this->_acl->isAllowed('marketing', 'newsletter', 'publish')); + $this->assertFalse($this->_acl->isAllowed('staff', 'pending', 'publish')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'pending', 'publish')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'newsletter', 'archive')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'newsletter', 'delete')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'newsletter')); + + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'publish')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'archive')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'latest', 'delete')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'latest', 'revise')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'latest')); + + $this->assertFalse($this->_acl->isAllowed('marketing', 'announcement', 'archive')); + $this->assertFalse($this->_acl->isAllowed('staff', 'announcement', 'archive')); + $this->assertFalse($this->_acl->isAllowed('administrator', 'announcement', 'archive')); + + $this->assertFalse($this->_acl->isAllowed('staff', 'latest', 'publish')); + $this->assertFalse($this->_acl->isAllowed('editor', 'announcement', 'archive')); + + // Remove some previous permission specifications + + // Marketing can no longer publish and archive newsletters + $this->_acl->removeAllow('marketing', 'newsletter', array('publish', 'archive')); + + // Marketing can no longer archive the latest news + $this->_acl->removeAllow('marketing', 'latest', 'archive'); + + // Now staff (and marketing, by inheritance) may revise latest news + $this->_acl->removeDeny('staff', 'latest', 'revise'); + + // Access control checks for the above refinements + + $this->assertFalse($this->_acl->isAllowed('marketing', 'newsletter', 'publish')); + $this->assertFalse($this->_acl->isAllowed('marketing', 'newsletter', 'archive')); + + $this->assertFalse($this->_acl->isAllowed('marketing', 'latest', 'archive')); + + $this->assertTrue($this->_acl->isAllowed('staff', 'latest', 'revise')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'revise')); + + // Grant marketing all permissions on the latest news + $this->_acl->allow('marketing', 'latest'); + + // Access control checks for the above refinement + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'archive')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'publish')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest', 'edit')); + $this->assertTrue($this->_acl->isAllowed('marketing', 'latest')); + + } + + /** + * Ensures that the $onlyParents argument to inheritsRole() works + * + * @return void + * @group ZF-2502 + */ + public function testRoleInheritanceSupportsCheckingOnlyParents() + { + $this->_acl->addRole(new Role\GenericRole('grandparent')) + ->addRole(new Role\GenericRole('parent'), 'grandparent') + ->addRole(new Role\GenericRole('child'), 'parent'); + $this->assertFalse($this->_acl->inheritsRole('child', 'grandparent', true)); + } + + /** + * Ensures that the solution for ZF-2234 works as expected + * + * @return void + * @group ZF-2234 + */ + public function testAclInternalDFSMethodsBehaveProperly() + { + $acl = new TestAsset\ExtendedAclZF2234(); + + $someResource = new Resource\GenericResource('someResource'); + $someRole = new Role\GenericRole('someRole'); + + $acl->addResource($someResource) + ->addRole($someRole); + + $nullValue = null; + $nullReference =& $nullValue; + + try { + $acl->exroleDFSVisitAllPrivileges($someRole, $someResource, $nullReference); + $this->fail('Expected Zend\Permissions\Acl\Exception not thrown'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertEquals('$dfs parameter may not be null', $e->getMessage()); + } + + try { + $acl->exroleDFSOnePrivilege($someRole, $someResource, null); + $this->fail('Expected Zend\Permissions\Acl\Exception not thrown'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertEquals('$privilege parameter may not be null', $e->getMessage()); + } + + try { + $acl->exroleDFSVisitOnePrivilege($someRole, $someResource, null); + $this->fail('Expected Zend\Permissions\Acl\Exception not thrown'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertEquals('$privilege parameter may not be null', $e->getMessage()); + } + + try { + $acl->exroleDFSVisitOnePrivilege($someRole, $someResource, 'somePrivilege', $nullReference); + $this->fail('Expected Zend\Permissions\Acl\Exception not thrown'); + } catch (Acl\Exception\ExceptionInterface $e) { + $this->assertEquals('$dfs parameter may not be null', $e->getMessage()); + } + } + + + /** + * @group ZF-1721 + */ + public function testAclAssertionsGetProperRoleWhenInheritenceIsUsed() + { + $acl = $this->_loadUseCase1(); + + $user = new Role\GenericRole('publisher'); + $blogPost = new Resource\GenericResource('blogPost'); + + /** + * @var ZendTest\Permissions\Acl\UseCase1\UserIsBlogPostOwnerAssertion + */ + $assertion = $acl->customAssertion; + + $this->assertTrue($acl->isAllowed($user, $blogPost, 'modify')); + + $this->assertEquals('publisher', $assertion->lastAssertRole->getRoleId()); + + } + + /** + * + * @group ZF-1722 + */ + public function testAclAssertionsGetOriginalIsAllowedObjects() + { + $acl = $this->_loadUseCase1(); + + $user = new TestAsset\UseCase1\User(); + $blogPost = new TestAsset\UseCase1\BlogPost(); + + $this->assertTrue($acl->isAllowed($user, $blogPost, 'view')); + + /** + * @var ZendTest\Permissions\Acl\UseCase1\UserIsBlogPostOwnerAssertion + */ + $assertion = $acl->customAssertion; + + $assertion->assertReturnValue = true; + $user->role = 'contributor'; + $this->assertTrue($acl->isAllowed($user, $blogPost, 'modify'), 'Assertion should return true'); + $assertion->assertReturnValue = false; + $this->assertFalse($acl->isAllowed($user, $blogPost, 'modify'), 'Assertion should return false'); + + // check to see if the last assertion has the proper objets + $this->assertInstanceOf('ZendTest\Permissions\Acl\TestAsset\UseCase1\User', $assertion->lastAssertRole, 'Assertion did not receive proper role object'); + $this->assertInstanceOf('ZendTest\Permissions\Acl\TestAsset\UseCase1\BlogPost', $assertion->lastAssertResource, 'Assertion did not receive proper resource object'); + + } + + /** + * + * @return Zend_Acl_UseCase1_Acl + */ + protected function _loadUseCase1() + { + return new TestAsset\UseCase1\Acl(); + } + + /** + * Confirm that deleting a role after allowing access to all roles + * raise undefined index error + * + * @group ZF-5700 + */ + public function testRemovingRoleAfterItWasAllowedAccessToAllResourcesGivesError() + { + $acl = new Acl\Acl(); + $acl->addRole(new Role\GenericRole('test0')); + $acl->addRole(new Role\GenericRole('test1')); + $acl->addRole(new Role\GenericRole('test2')); + $acl->addResource(new Resource\GenericResource('Test')); + + $acl->allow(null,'Test','xxx'); + + // error test + $acl->removeRole('test0'); + + // Check after fix + $this->assertFalse($acl->hasRole('test0')); + } + + /** + * @group ZF-8039 + * + * Meant to test for the (in)existance of this notice: + * "Notice: Undefined index: allPrivileges in lib/Zend/Acl.php on line 682" + */ + public function testMethodRemoveAllowDoesNotThrowNotice() + { + $acl = new Acl\Acl(); + $acl->addRole('admin'); + $acl->addResource('blog'); + $acl->allow('admin', 'blog', 'read'); + $acl->removeAllow(array('admin'), array('blog'), null); + } + + public function testRoleObjectImplementsToString() + { + $role = new Role\GenericRole('_fooBar_'); + $this->assertEquals('_fooBar_',(string)$role); + } + + public function testResourceObjectImplementsToString() + { + $resource = new Resource\GenericResource('_fooBar_'); + $this->assertEquals('_fooBar_',(string)$resource); + } + + /** + * @group ZF-7973 + */ + public function testAclPassesPrivilegeToAssertClass() + { + $assertion = new TestAsset\AssertionZF7973(); + + $acl = new Acl\Acl(); + $acl->addRole('role'); + $acl->addResource('resource'); + $acl->allow('role',null,null,$assertion); + $allowed = $acl->isAllowed('role','resource','privilege',$assertion); + + $this->assertTrue($allowed); + } + + /** + * @group ZF-8468 + */ + public function testgetRoles() + { + $this->assertEquals(array(),$this->_acl->getRoles()); + + $roleGuest = new Role\GenericRole('guest'); + $this->_acl->addRole($roleGuest); + $this->_acl->addRole(new Role\GenericRole('staff'), $roleGuest); + $this->_acl->addRole(new Role\GenericRole('editor'), 'staff'); + $this->_acl->addRole(new Role\GenericRole('administrator')); + + $expected = array('guest', 'staff','editor','administrator'); + $this->assertEquals($expected, $this->_acl->getRoles()); + } + + /** + * @group ZF-8468 + */ + public function testgetResources() + { + $this->assertEquals(array(),$this->_acl->getResources()); + + $this->_acl->addResource(new Resource\GenericResource('someResource')); + $this->_acl->addResource(new Resource\GenericResource('someOtherResource')); + + $expected = array('someResource', 'someOtherResource'); + $this->assertEquals($expected, $this->_acl->getResources()); + } + + /** + * @group ZF-9643 + */ + public function testRemoveAllowWithNullResourceAppliesToAllResources() + { + $this->_acl->addRole('guest'); + $this->_acl->addResource('blogpost'); + $this->_acl->addResource('newsletter'); + $this->_acl->allow('guest', 'blogpost', 'read'); + $this->_acl->allow('guest', 'newsletter', 'read'); + $this->assertTrue($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertTrue($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + $this->_acl->removeAllow('guest', 'newsletter', 'read'); + $this->assertTrue($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertFalse($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + $this->_acl->removeAllow('guest', null, 'read'); + $this->assertFalse($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertFalse($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + // ensure allow null/all resoures works + $this->_acl->allow('guest', null, 'read'); + $this->assertTrue($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertTrue($this->_acl->isAllowed('guest', 'newsletter', 'read')); + } + + /** + * @group ZF-9643 + */ + public function testRemoveDenyWithNullResourceAppliesToAllResources() + { + $this->_acl->addRole('guest'); + $this->_acl->addResource('blogpost'); + $this->_acl->addResource('newsletter'); + + $this->_acl->allow(); + $this->_acl->deny('guest', 'blogpost', 'read'); + $this->_acl->deny('guest', 'newsletter', 'read'); + $this->assertFalse($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertFalse($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + $this->_acl->removeDeny('guest', 'newsletter', 'read'); + $this->assertFalse($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertTrue($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + $this->_acl->removeDeny('guest', null, 'read'); + $this->assertTrue($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertTrue($this->_acl->isAllowed('guest', 'newsletter', 'read')); + + // ensure deny null/all resources works + $this->_acl->deny('guest', null, 'read'); + $this->assertFalse($this->_acl->isAllowed('guest', 'blogpost', 'read')); + $this->assertFalse($this->_acl->isAllowed('guest', 'newsletter', 'read')); + } + +} diff --git a/test/TestAsset/AssertionZF7973.php b/test/TestAsset/AssertionZF7973.php new file mode 100644 index 0000000..23ac2ef --- /dev/null +++ b/test/TestAsset/AssertionZF7973.php @@ -0,0 +1,26 @@ +roleDFSVisitAllPrivileges($role, $resource, $dfs); + } + + public function exroleDFSOnePrivilege(Acl\Role\RoleInterface $role, Acl\Resource\ResourceInterface $resource = null, + $privilege = null) + { + return $this->roleDFSOnePrivilege($role, $resource, $privilege); + } + + public function exroleDFSVisitOnePrivilege(Acl\Role\RoleInterface $role, Acl\Resource\ResourceInterface $resource = null, + $privilege = null, &$dfs = null) + { + return $this->roleDFSVisitOnePrivilege($role, $resource, $privilege, $dfs); + } +} diff --git a/test/TestAsset/MockAssertion.php b/test/TestAsset/MockAssertion.php new file mode 100644 index 0000000..da879ff --- /dev/null +++ b/test/TestAsset/MockAssertion.php @@ -0,0 +1,29 @@ +_returnValue = (bool) $returnValue; + } + + public function assert(Acl\Acl $acl, Acl\Role\RoleInterface $role = null, Acl\Resource\ResourceInterface $resource = null, + $privilege = null) + { + return $this->_returnValue; + } +} diff --git a/test/TestAsset/UseCase1/Acl.php b/test/TestAsset/UseCase1/Acl.php new file mode 100644 index 0000000..1f4a685 --- /dev/null +++ b/test/TestAsset/UseCase1/Acl.php @@ -0,0 +1,32 @@ +customAssertion = new UserIsBlogPostOwnerAssertion(); + + $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('guest')); // $acl->addRole('guest'); + $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('contributor'), 'guest'); // $acl->addRole('contributor', 'guest'); + $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('publisher'), 'contributor'); // $acl->addRole('publisher', 'contributor'); + $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('admin')); // $acl->addRole('admin'); + $this->addResource(new \Zend\Permissions\Acl\Resource\GenericResource('blogPost')); // $acl->addResource('blogPost'); + $this->allow('guest', 'blogPost', 'view'); + $this->allow('contributor', 'blogPost', 'contribute'); + $this->allow('contributor', 'blogPost', 'modify', $this->customAssertion); + $this->allow('publisher', 'blogPost', 'publish'); + } +} diff --git a/test/TestAsset/UseCase1/BlogPost.php b/test/TestAsset/UseCase1/BlogPost.php new file mode 100644 index 0000000..4f62c49 --- /dev/null +++ b/test/TestAsset/UseCase1/BlogPost.php @@ -0,0 +1,22 @@ +role; + } +} diff --git a/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php b/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php new file mode 100644 index 0000000..3d733a1 --- /dev/null +++ b/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php @@ -0,0 +1,31 @@ +lastAssertRole = $user; + $this->lastAssertResource = $blogPost; + $this->lastAssertPrivilege = $privilege; + return $this->assertReturnValue; + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..7431f62 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +