From c41e81b6a7a14fcfc318aad8b730592872487aff Mon Sep 17 00:00:00 2001 From: Nikola Posa Date: Sun, 28 Jun 2015 13:45:58 +0200 Subject: [PATCH 01/13] Created Proprietary interface applicable to Resources and Roles. Created Ownership assertion implementation. --- src/Assertion/OwnershipAssertion.php | 38 ++++++++++++++ src/ProprietaryInterface.php | 24 +++++++++ test/Assertion/OwnershipAssertionTest.php | 64 +++++++++++++++++++++++ test/TestAsset/UseCase2/Acl.php | 32 ++++++++++++ test/TestAsset/UseCase2/Author1.php | 17 ++++++ test/TestAsset/UseCase2/Author2.php | 17 ++++++ test/TestAsset/UseCase2/BlogPost.php | 32 ++++++++++++ test/TestAsset/UseCase2/Comment.php | 23 ++++++++ test/TestAsset/UseCase2/User.php | 30 +++++++++++ 9 files changed, 277 insertions(+) create mode 100644 src/Assertion/OwnershipAssertion.php create mode 100644 src/ProprietaryInterface.php create mode 100644 test/Assertion/OwnershipAssertionTest.php create mode 100644 test/TestAsset/UseCase2/Acl.php create mode 100644 test/TestAsset/UseCase2/Author1.php create mode 100644 test/TestAsset/UseCase2/Author2.php create mode 100644 test/TestAsset/UseCase2/BlogPost.php create mode 100644 test/TestAsset/UseCase2/Comment.php create mode 100644 test/TestAsset/UseCase2/User.php diff --git a/src/Assertion/OwnershipAssertion.php b/src/Assertion/OwnershipAssertion.php new file mode 100644 index 0000000..53a6a7e --- /dev/null +++ b/src/Assertion/OwnershipAssertion.php @@ -0,0 +1,38 @@ + + */ +class OwnershipAssertion implements AssertionInterface +{ + public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) + { + //Assert passes if role or resource is not proprietary + if (!$role instanceof ProprietaryInterface || !$resource instanceof ProprietaryInterface) { + return true; + } + + //Assert passes if resources does not have an owner + if ($resource->getOwnerId() === null) { + return true; + } + + return ($resource->getOwnerId() === $role->getOwnerId()); + } +} diff --git a/src/ProprietaryInterface.php b/src/ProprietaryInterface.php new file mode 100644 index 0000000..8da8f3b --- /dev/null +++ b/src/ProprietaryInterface.php @@ -0,0 +1,24 @@ + + */ +interface ProprietaryInterface +{ + /** + * @return mixed + */ + public function getOwnerId(); +} diff --git a/test/Assertion/OwnershipAssertionTest.php b/test/Assertion/OwnershipAssertionTest.php new file mode 100644 index 0000000..a672d81 --- /dev/null +++ b/test/Assertion/OwnershipAssertionTest.php @@ -0,0 +1,64 @@ +assertTrue($acl->isAllowed('guest', 'blogPost', 'view')); + $this->assertFalse($acl->isAllowed('guest', 'blogPost', 'delete')); + } + + public function testAssertPassesIfResourceIsNotProprietary() + { + $acl = new UseCase2\Acl(); + + $author = new UseCase2\Author1(); + + $this->assertTrue($acl->isAllowed($author, 'comment', 'view')); + $this->assertFalse($acl->isAllowed($author, 'comment', 'delete')); + } + + public function testAssertPassesIfResourceDoesNotHaveOwner() + { + $acl = new UseCase2\Acl(); + + $author = new UseCase2\Author1(); + + $blogPost = new UseCase2\BlogPost(); + $blogPost->author = null; + + $this->assertTrue($acl->isAllowed($author, 'blogPost', 'write')); + $this->assertTrue($acl->isAllowed($author, $blogPost, 'edit')); + } + + public function testAssertFailsIfResourceHasOwnerOtherThanRoleOwner() + { + $acl = new UseCase2\Acl(); + + $author1 = new UseCase2\Author1(); + $author2 = new UseCase2\Author2(); + + $blogPost = new UseCase2\BlogPost(); + $blogPost->author = $author1; + + $this->assertTrue($acl->isAllowed($author2, 'blogPost', 'write')); + $this->assertFalse($acl->isAllowed($author2, $blogPost, 'edit')); + } +} diff --git a/test/TestAsset/UseCase2/Acl.php b/test/TestAsset/UseCase2/Acl.php new file mode 100644 index 0000000..9074ab5 --- /dev/null +++ b/test/TestAsset/UseCase2/Acl.php @@ -0,0 +1,32 @@ +addRole('guest'); + $this->addRole('member', 'guest'); + $this->addRole('author', 'member'); + $this->addRole('admin'); + + $this->addResource(new BlogPost()); + $this->addResource(new Comment()); + + $this->allow('guest', 'blogPost', 'view'); + $this->allow('guest', 'comment', array('view', 'submit')); + $this->allow('author', 'blogPost', 'write'); + $this->allow('author', 'blogPost', 'edit', new OwnershipAssertion()); + $this->allow('admin'); + } +} diff --git a/test/TestAsset/UseCase2/Author1.php b/test/TestAsset/UseCase2/Author1.php new file mode 100644 index 0000000..d3fea05 --- /dev/null +++ b/test/TestAsset/UseCase2/Author1.php @@ -0,0 +1,17 @@ +author === null) { + return null; + } + + return $this->author->getOwnerId(); + } +} diff --git a/test/TestAsset/UseCase2/Comment.php b/test/TestAsset/UseCase2/Comment.php new file mode 100644 index 0000000..7efeda6 --- /dev/null +++ b/test/TestAsset/UseCase2/Comment.php @@ -0,0 +1,23 @@ + + */ +class Comment implements Resource\ResourceInterface +{ + public function getResourceId() + { + return 'comment'; + } +} diff --git a/test/TestAsset/UseCase2/User.php b/test/TestAsset/UseCase2/User.php new file mode 100644 index 0000000..0ee9b0d --- /dev/null +++ b/test/TestAsset/UseCase2/User.php @@ -0,0 +1,30 @@ +role; + } + + public function getOwnerId() + { + return $this->id; + } +} From fbab559d4a2cde001299292e090ab93b4fe7e832 Mon Sep 17 00:00:00 2001 From: Nikola Posa Date: Sun, 28 Jun 2015 14:52:59 +0200 Subject: [PATCH 02/13] Coding style fixes. --- test/Assertion/OwnershipAssertionTest.php | 128 +++++++++++----------- test/TestAsset/UseCase2/Acl.php | 64 +++++------ test/TestAsset/UseCase2/Author1.php | 34 +++--- test/TestAsset/UseCase2/Author2.php | 34 +++--- test/TestAsset/UseCase2/BlogPost.php | 64 +++++------ test/TestAsset/UseCase2/User.php | 60 +++++----- 6 files changed, 192 insertions(+), 192 deletions(-) diff --git a/test/Assertion/OwnershipAssertionTest.php b/test/Assertion/OwnershipAssertionTest.php index a672d81..e523cec 100644 --- a/test/Assertion/OwnershipAssertionTest.php +++ b/test/Assertion/OwnershipAssertionTest.php @@ -1,64 +1,64 @@ -assertTrue($acl->isAllowed('guest', 'blogPost', 'view')); - $this->assertFalse($acl->isAllowed('guest', 'blogPost', 'delete')); - } - - public function testAssertPassesIfResourceIsNotProprietary() - { - $acl = new UseCase2\Acl(); - - $author = new UseCase2\Author1(); - - $this->assertTrue($acl->isAllowed($author, 'comment', 'view')); - $this->assertFalse($acl->isAllowed($author, 'comment', 'delete')); - } - - public function testAssertPassesIfResourceDoesNotHaveOwner() - { - $acl = new UseCase2\Acl(); - - $author = new UseCase2\Author1(); - - $blogPost = new UseCase2\BlogPost(); - $blogPost->author = null; - - $this->assertTrue($acl->isAllowed($author, 'blogPost', 'write')); - $this->assertTrue($acl->isAllowed($author, $blogPost, 'edit')); - } - - public function testAssertFailsIfResourceHasOwnerOtherThanRoleOwner() - { - $acl = new UseCase2\Acl(); - - $author1 = new UseCase2\Author1(); - $author2 = new UseCase2\Author2(); - - $blogPost = new UseCase2\BlogPost(); - $blogPost->author = $author1; - - $this->assertTrue($acl->isAllowed($author2, 'blogPost', 'write')); - $this->assertFalse($acl->isAllowed($author2, $blogPost, 'edit')); - } -} +assertTrue($acl->isAllowed('guest', 'blogPost', 'view')); + $this->assertFalse($acl->isAllowed('guest', 'blogPost', 'delete')); + } + + public function testAssertPassesIfResourceIsNotProprietary() + { + $acl = new UseCase2\Acl(); + + $author = new UseCase2\Author1(); + + $this->assertTrue($acl->isAllowed($author, 'comment', 'view')); + $this->assertFalse($acl->isAllowed($author, 'comment', 'delete')); + } + + public function testAssertPassesIfResourceDoesNotHaveOwner() + { + $acl = new UseCase2\Acl(); + + $author = new UseCase2\Author1(); + + $blogPost = new UseCase2\BlogPost(); + $blogPost->author = null; + + $this->assertTrue($acl->isAllowed($author, 'blogPost', 'write')); + $this->assertTrue($acl->isAllowed($author, $blogPost, 'edit')); + } + + public function testAssertFailsIfResourceHasOwnerOtherThanRoleOwner() + { + $acl = new UseCase2\Acl(); + + $author1 = new UseCase2\Author1(); + $author2 = new UseCase2\Author2(); + + $blogPost = new UseCase2\BlogPost(); + $blogPost->author = $author1; + + $this->assertTrue($acl->isAllowed($author2, 'blogPost', 'write')); + $this->assertFalse($acl->isAllowed($author2, $blogPost, 'edit')); + } +} diff --git a/test/TestAsset/UseCase2/Acl.php b/test/TestAsset/UseCase2/Acl.php index 9074ab5..66139fb 100644 --- a/test/TestAsset/UseCase2/Acl.php +++ b/test/TestAsset/UseCase2/Acl.php @@ -1,32 +1,32 @@ -addRole('guest'); - $this->addRole('member', 'guest'); - $this->addRole('author', 'member'); - $this->addRole('admin'); - - $this->addResource(new BlogPost()); - $this->addResource(new Comment()); - - $this->allow('guest', 'blogPost', 'view'); - $this->allow('guest', 'comment', array('view', 'submit')); - $this->allow('author', 'blogPost', 'write'); - $this->allow('author', 'blogPost', 'edit', new OwnershipAssertion()); - $this->allow('admin'); - } -} +addRole('guest'); + $this->addRole('member', 'guest'); + $this->addRole('author', 'member'); + $this->addRole('admin'); + + $this->addResource(new BlogPost()); + $this->addResource(new Comment()); + + $this->allow('guest', 'blogPost', 'view'); + $this->allow('guest', 'comment', array('view', 'submit')); + $this->allow('author', 'blogPost', 'write'); + $this->allow('author', 'blogPost', 'edit', new OwnershipAssertion()); + $this->allow('admin'); + } +} diff --git a/test/TestAsset/UseCase2/Author1.php b/test/TestAsset/UseCase2/Author1.php index d3fea05..fe9e7f9 100644 --- a/test/TestAsset/UseCase2/Author1.php +++ b/test/TestAsset/UseCase2/Author1.php @@ -1,17 +1,17 @@ -author === null) { - return null; - } - - return $this->author->getOwnerId(); - } -} +author === null) { + return null; + } + + return $this->author->getOwnerId(); + } +} diff --git a/test/TestAsset/UseCase2/User.php b/test/TestAsset/UseCase2/User.php index 0ee9b0d..b0311e6 100644 --- a/test/TestAsset/UseCase2/User.php +++ b/test/TestAsset/UseCase2/User.php @@ -1,30 +1,30 @@ -role; - } - - public function getOwnerId() - { - return $this->id; - } -} +role; + } + + public function getOwnerId() + { + return $this->id; + } +} From 2d31dc5b708109f24e651cae0956c04de10b2705 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 11:54:01 -0500 Subject: [PATCH 03/13] CS/consistency fixes - Ran phpcbf to auto-fix CS issues. - Updated docblocks to match new preferred style and only reference current year. - Removed `@author` annotations; use `git log`/`git blame`/et al for this. - Import most specific class/interface when possible (not namespace). --- src/Assertion/OwnershipAssertion.php | 12 ++++-------- src/ProprietaryInterface.php | 14 ++++++-------- test/Assertion/OwnershipAssertionTest.php | 15 +++++---------- test/TestAsset/UseCase2/Acl.php | 13 ++++++------- test/TestAsset/UseCase2/Author1.php | 8 +++----- test/TestAsset/UseCase2/Author2.php | 8 +++----- test/TestAsset/UseCase2/BlogPost.php | 12 +++++------- test/TestAsset/UseCase2/Comment.php | 15 +++++---------- test/TestAsset/UseCase2/User.php | 12 +++++------- 9 files changed, 42 insertions(+), 67 deletions(-) diff --git a/src/Assertion/OwnershipAssertion.php b/src/Assertion/OwnershipAssertion.php index 53a6a7e..dcd1100 100644 --- a/src/Assertion/OwnershipAssertion.php +++ b/src/Assertion/OwnershipAssertion.php @@ -1,10 +1,8 @@ */ class OwnershipAssertion implements AssertionInterface { public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) { //Assert passes if role or resource is not proprietary - if (!$role instanceof ProprietaryInterface || !$resource instanceof ProprietaryInterface) { + if (! $role instanceof ProprietaryInterface || ! $resource instanceof ProprietaryInterface) { return true; } diff --git a/src/ProprietaryInterface.php b/src/ProprietaryInterface.php index 8da8f3b..00a388a 100644 --- a/src/ProprietaryInterface.php +++ b/src/ProprietaryInterface.php @@ -1,19 +1,17 @@ + * Provides information about the owner of some object. Used in conjunction + * with the Ownership assertion. */ interface ProprietaryInterface { diff --git a/test/Assertion/OwnershipAssertionTest.php b/test/Assertion/OwnershipAssertionTest.php index e523cec..04b2452 100644 --- a/test/Assertion/OwnershipAssertionTest.php +++ b/test/Assertion/OwnershipAssertionTest.php @@ -1,21 +1,16 @@ addResource(new Comment()); $this->allow('guest', 'blogPost', 'view'); - $this->allow('guest', 'comment', array('view', 'submit')); + $this->allow('guest', 'comment', ['view', 'submit']); $this->allow('author', 'blogPost', 'write'); $this->allow('author', 'blogPost', 'edit', new OwnershipAssertion()); $this->allow('admin'); diff --git a/test/TestAsset/UseCase2/Author1.php b/test/TestAsset/UseCase2/Author1.php index fe9e7f9..8488948 100644 --- a/test/TestAsset/UseCase2/Author1.php +++ b/test/TestAsset/UseCase2/Author1.php @@ -1,10 +1,8 @@ - */ -class Comment implements Resource\ResourceInterface +class Comment implements ResourceInterface { public function getResourceId() { diff --git a/test/TestAsset/UseCase2/User.php b/test/TestAsset/UseCase2/User.php index b0311e6..b5c8707 100644 --- a/test/TestAsset/UseCase2/User.php +++ b/test/TestAsset/UseCase2/User.php @@ -1,18 +1,16 @@ Date: Tue, 1 May 2018 12:08:07 -0500 Subject: [PATCH 04/13] Adds documentation for #3 (ownership assertions) Based on the wonderful issue description! --- docs/book/ownership.md | 126 +++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 6 +- 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 docs/book/ownership.md diff --git a/docs/book/ownership.md b/docs/book/ownership.md new file mode 100644 index 0000000..a675e2b --- /dev/null +++ b/docs/book/ownership.md @@ -0,0 +1,126 @@ +# Ownership Assertions + +- Since 2.7.0 + +When setting up permissions for an application, site owners common will want to +allow roles to manipulate resources owned by the user with that role. For +example, a blog author should have permission to _write_ new posts, and also to +_modify_ his or her **own** posts, but **not** posts of other authors. + +To accomodate this use case, we provide two interfaces: + +- **`Zend\Acl\ProprietaryInterface`** is applicable to _resources_ and _roles_. + It provides information about the _owner_ of an object. Objects implementing + this interface are used in conjunction with the `OwnershipAssertion`. + +- **`Zend\Acl\Assertion\OwnershipAssertion`** ensures that a resource is owned + by a specific role by comparing it to owners provided by + `ProprietaryInterface` implementations. + +### Example + +Consider the following entities: + +```php +namespace MyApp\Entity; + +use Zend\Permissions\Acl\ProprietaryInterface; +use Zend\Permissions\Acl\Resource\ResourceInterface; +use Zend\Permissions\Acl\Role\RoleInterface; + +class User implements RoleInterface, ProprietaryInterface +{ + protected $id; + + protected $role = 'guest'; + + public function __construct($id, $role) + { + $this->id = $id; + $this->role = $role; + } + + public function getRoleId() + { + return $this->role; + } + + public function getOwnerId() + { + return $this->id; + } +} + +class BlogPost implements ResourceInterface, ProprietaryInterface +{ + public $author = null; + + public function getResourceId() + { + return 'blogPost'; + } + + public function getOwnerId() + { + if ($this->author === null) { + return null; + } + + return $this->author->getOwnerId(); + } +} +``` + +The `User` marks itself as an _owner_ by implementing `ProprietaryInterface`; +its `getOwnerId()` method will return the user identifier provided during +instantiation. + +A `BlogPost` marks itself as a resource and an _owner_ by also implementing +`ProprietaryInterface`; in its case, it returns the author identifier, if +present, but `null` otherwise. + +Now let's wire these up into an ACL: + +```php +namespace MyApp; + +use MyApp\Entity; +use Zend\Permissions\Acl\Acl; +use Zend\Permissions\Acl\Assertion\OwnershipAssertion; + +$acl = new Acl(); +$acl->addRole('guest'); +$acl->addRole('member', 'guest'); +$acl->addRole('author', 'member'); +$acl->addRole('admin'); + +$acl->addResource('blogPost'); +$acl->addResource('comment'); + +$acl->allow('guest', 'blogPost', 'view'); +$acl->allow('guest', 'comment', array('view', 'submit')); +$acl->allow('author', 'blogPost', 'write'); +$acl->allow('author', 'blogPost', 'edit', new OwnershipAssertion()); +$acl->allow('admin'); + +$author1 = new User(1, 'author'); +$author2 = new User(2, 'author'); + +$blogPost = new BlogPost(); +$blogPost->author = $author1; +``` + +The takeaways from the above should be: + +- An `author` can _write_ blog posts, and _edit_ posts it owns. +- `$author1` and `$author2` are both authors. +- `$author1` is the author of `$blogPost`. + +Knowing these facts, we can expect the following assertion results: + +```php +$acl->isAllowed($author1, 'blogPost', 'write'); // true +$acl->isAllowed($author1, $blogPost, 'edit'); // true +$acl->isAllowed($author2, 'blogPost', 'write'); // true +$acl->isAllowed($author2, $blogPost, 'edit'); // false +``` diff --git a/mkdocs.yml b/mkdocs.yml index b3fd8ff..6d29540 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,8 +4,10 @@ pages: - index.md - 'Theory and Usage': usage.md - 'Refining ACLs': refining.md - - 'Advanced Usage': advanced.md + - Reference: + - 'Ownership Assertions': ownership.md + - 'Advanced Usage': advanced.md site_name: zend-permissions-acl -site_description: Zend\Permissions.acl +site_description: 'Provides a lightweight and flexible access control list (ACL) implementation for privileges management' repo_url: 'https://github.com/zendframework/zend-permissions-acl' copyright: 'Copyright (c) 2005-2018 Zend Technologies USA Inc.' From dc5ae749a55a43f8ab3b383adbf5ee01f1868994 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 12:12:12 -0500 Subject: [PATCH 05/13] Adds CHANGELOG entry for #3 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26cd08a..63ae56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#3](https://github.com/zendframework/zend-permissions-acl/pull/3) adds two new interfaces designed to allow creation of ownership-based assertions easier: + - `Zend\Permissions\Acl\ProprietaryInterface` is applicable to both roles and resources, and provides the method `getOwnerId()` for retrieving the owner role of an object. + - `Zend\Permissions\Acl\Assertion\OwnershipAssertion` ensures that the owner of a proprietary resource matches that of the role. ### Changed From db9bd643231b5c3c634c921f374e05684d20ad52 Mon Sep 17 00:00:00 2001 From: Nikola Posa Date: Sat, 16 Jul 2016 13:42:19 +0200 Subject: [PATCH 06/13] Expression assertion. CS fixes. Testing ExpressionAssertion. 'regex' operator instead of 'contains'. Exception in case that context object property cannot be resolved. Exception in case that context object property cannot be resolved. Exception in case that context object property cannot be resolved. --- src/Assertion/ExpressionAssertion.php | 263 +++++++++++++++ test/Assertion/ExpressionAssertionTest.php | 362 +++++++++++++++++++++ test/TestAsset/UseCase3/BlogPost.php | 43 +++ test/TestAsset/UseCase3/User.php | 36 ++ 4 files changed, 704 insertions(+) create mode 100644 src/Assertion/ExpressionAssertion.php create mode 100644 test/Assertion/ExpressionAssertionTest.php create mode 100644 test/TestAsset/UseCase3/BlogPost.php create mode 100644 test/TestAsset/UseCase3/User.php diff --git a/src/Assertion/ExpressionAssertion.php b/src/Assertion/ExpressionAssertion.php new file mode 100644 index 0000000..94743d0 --- /dev/null +++ b/src/Assertion/ExpressionAssertion.php @@ -0,0 +1,263 @@ +'; + const OPERATOR_GTE = '>='; + const OPERATOR_IN = 'in'; + const OPERATOR_NIN = 'nin'; + const OPERATOR_REGEX = 'regex'; + + /** + * @var mixed + */ + private $left; + + /** + * @var string + */ + private $operator; + + /** + * @var mixed + */ + private $right; + + /** + * @var array + */ + private $assertContext = []; + + /** + * @var array + */ + private static $validOperators = [ + self::OPERATOR_EQ, + self::OPERATOR_NEQ, + self::OPERATOR_LT, + self::OPERATOR_LTE, + self::OPERATOR_GT, + self::OPERATOR_GTE, + self::OPERATOR_IN, + self::OPERATOR_NIN, + self::OPERATOR_REGEX, + ]; + + private function __construct($left, $operator, $right) + { + $this->left = $left; + $this->operator = $operator; + $this->right = $right; + } + + /** + * @param mixed $left + * @param string $operator + * @param mixed $right + * @return self + */ + public static function fromProperties($left, $operator, $right) + { + $operator = strtolower($operator); + + self::validateOperand($left); + self::validateOperator($operator); + self::validateOperand($right); + + return new self($left, $operator, $right); + } + + /** + * @param array $expression + * @throws InvalidAssertionException + * @return self + */ + public static function fromArray(array $expression) + { + $required = ['left', 'operator', 'right']; + + if (count(array_intersect_key($expression, array_flip($required))) < count($required)) { + throw new InvalidAssertionException( + "Expression assertion requires 'left', 'operator' and 'right' to be supplied" + ); + } + + return self::fromProperties( + $expression['left'], + $expression['operator'], + $expression['right'] + ); + } + + private static function validateOperand($operand) + { + if (is_array($operand) && isset($operand[self::OPERAND_CONTEXT_PROPERTY])) { + if (! is_string($operand[self::OPERAND_CONTEXT_PROPERTY])) { + throw new InvalidAssertionException('Expression assertion context operand must be string'); + } + } + } + + private static function validateOperator($operator) + { + if (! in_array($operator, self::$validOperators)) { + throw new InvalidAssertionException('Provided expression assertion operator is not supported'); + } + } + + /** + * {@inheritDoc} + */ + public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) + { + $this->assertContext = [ + 'acl' => $acl, + 'role' => $role, + 'resource' => $resource, + 'privilege' => $privilege, + ]; + + return $this->evaluate(); + } + + private function evaluate() + { + $left = $this->getLeftValue(); + $right = $this->getRightValue(); + + return static::evaluateExpression($left, $this->operator, $right); + } + + private function getLeftValue() + { + return $this->resolveOperandValue($this->left); + } + + private function getRightValue() + { + return $this->resolveOperandValue($this->right); + } + + private function resolveOperandValue($operand) + { + if (is_array($operand) && isset($operand[self::OPERAND_CONTEXT_PROPERTY])) { + $contextProperty = $operand[self::OPERAND_CONTEXT_PROPERTY]; + + if (strpos($contextProperty, '.') !== false) { //property path? + list($objectName, $objectField) = explode('.', $contextProperty, 2); + + if (! isset($this->assertContext[$objectName])) { + throw new RuntimeException(sprintf( + "'%s' is not available in the assertion context", + $objectName + )); + } + + try { + return $this->getObjectFieldValue($this->assertContext[$objectName], $objectField); + } catch (\RuntimeException $ex) { + throw new RuntimeException(sprintf( + "'%s' property cannot be resolved on the '%s' object", + $objectField, + $objectName + )); + } + } + + if (! isset($this->assertContext[$contextProperty])) { + throw new RuntimeException(sprintf( + "'%s' is not available in the assertion context", + $contextProperty + )); + } + + return $this->assertContext[$contextProperty]; + } + + return $operand; + } + + private function getObjectFieldValue($object, $field) + { + $accessors = ['get', 'is']; + + $fieldAccessor = $field; + + if (false !== strpos($field, '_')) { + $fieldAccessor = str_replace(' ', '', ucwords(str_replace('_', ' ', $field))); + } + + foreach ($accessors as $accessor) { + $accessor .= $fieldAccessor; + + if (! method_exists($object, $accessor)) { + continue; + } + + return $object->$accessor(); + } + + if (! property_exists($object, $field)) { + throw new \RuntimeException('Object property cannot be resolved'); + } + + return $object->$field; + } + + private static function evaluateExpression($left, $operator, $right) + { + switch ($operator) { + case self::OPERATOR_EQ: + return $left == $right; + case self::OPERATOR_NEQ: + return $left != $right; + case self::OPERATOR_LT: + return $left < $right; + case self::OPERATOR_LTE: + return $left <= $right; + case self::OPERATOR_GT: + return $left > $right; + case self::OPERATOR_GTE: + return $left >= $right; + case self::OPERATOR_IN: + return in_array($left, $right); + case self::OPERATOR_NIN: + return ! in_array($left, $right); + case self::OPERATOR_REGEX: + return (bool) preg_match($right, $left); + default: + throw new RuntimeException(sprintf( + 'Unsupported expression assertion operator: %s', + $operator + )); + } + } + + public function __sleep() + { + return [ + 'left', + 'operator', + 'right', + ]; + } +} diff --git a/test/Assertion/ExpressionAssertionTest.php b/test/Assertion/ExpressionAssertionTest.php new file mode 100644 index 0000000..b18df45 --- /dev/null +++ b/test/Assertion/ExpressionAssertionTest.php @@ -0,0 +1,362 @@ +assertInstanceOf(ExpressionAssertion::class, $assertion); + } + + public function testFromArrayCreation() + { + $assertion = ExpressionAssertion::fromArray([ + 'left' => 'foo', + 'operator' => '=', + 'right' => 'bar' + ]); + + $this->assertInstanceOf(ExpressionAssertion::class, $assertion); + } + + public function testExceptionIsRaisedInCaseOfInvalidExpressionArray() + { + $this->expectException(InvalidAssertionException::class); + $this->expectExceptionMessage("Expression assertion requires 'left', 'operator' and 'right' to be supplied"); + + ExpressionAssertion::fromArray(['left' => 'test', 'foo' => 'bar']); + } + + public function testExceptionIsRaisedInCaseOfInvalidExpressionContextOperandType() + { + $this->expectException(InvalidAssertionException::class); + $this->expectExceptionMessage('Expression assertion context operand must be string'); + + ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 123], + 'in', + 'test' + ); + } + + public function testExceptionIsRaisedInCaseOfInvalidExpressionOperator() + { + $this->expectException(InvalidAssertionException::class); + $this->expectExceptionMessage('Provided expression assertion operator is not supported'); + + ExpressionAssertion::fromProperties( + 'test', + 'invalid', + 'test' + ); + } + + /** + * @dataProvider getExpressions + */ + public function testExpressionsEvaluation(array $expression, $role, $resource, $privilege, $expectedAssert) + { + $assertion = ExpressionAssertion::fromArray($expression); + + $this->assertThat( + $assertion->assert(new Acl(), $role, $resource, $privilege), + $expectedAssert ? $this->isTrue() : $this->isFalse() + ); + } + + public function getExpressions() + { + $author3 = new User([ + 'username' => 'author3', + ]); + $post3 = new BlogPost([ + 'author' => $author3, + ]); + + return [ + 'equality' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => '=', + 'right' => 'test', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'inequality' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => '!=', + 'right' => 'test', + ], + 'role' => new User([ + 'username' => 'foobar', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'boolean-equality' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => '=', + 'right' => true, + ], + 'role' => $author3, + 'resource' => $post3, + 'privilege' => 'read', + 'assert' => true, + ], + 'greater-than' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], + 'operator' => '>', + 'right' => 20, + ], + 'role' => new User([ + 'username' => 'foobar', + 'age' => 15, + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => false, + ], + 'greater-than-or-equal' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], + 'operator' => '>=', + 'right' => 20, + ], + 'role' => new User([ + 'username' => 'foobar', + 'age' => 20, + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'less-than' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], + 'operator' => '<', + 'right' => 30, + ], + 'role' => new User([ + 'username' => 'foobar', + 'age' => 20, + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'less-than-or-equal' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], + 'operator' => '<=', + 'right' => 30, + ], + 'role' => new User([ + 'username' => 'foobar', + 'age' => 30, + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'in' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => 'in', + 'right' => ['foo', 'bar'], + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => false, + ], + 'not-in' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => 'nin', + 'right' => ['foo', 'bar'], + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'regex' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => 'regex', + 'right' => '/foobar/', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => false, + ], + 'REGEX' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'resource.shortDescription'], + 'operator' => 'REGEX', + 'right' => '/ipsum/', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost([ + 'title' => 'Test', + 'content' => 'lorem ipsum dolor sit amet', + 'shortDescription' => 'lorem ipsum' + ]), + 'privilege' => 'read', + 'assert' => true, + ], + 'equality-calculated-property' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.adult'], + 'operator' => '=', + 'right' => true, + ], + 'role' => new User([ + 'username' => 'test', + 'age' => 30, + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'privilege' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'privilege'], + 'operator' => '=', + 'right' => 'read', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'update', + 'assert' => false, + ], + ]; + } + + public function testExceptionIsRaisedInCaseOfUnknownContextOperand() + { + $assertion = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'foobar'], + '=', + 'test' + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("'foobar' is not available in the assertion context"); + + $assertion->assert(new Acl(), new User(), new BlogPost(), 'read'); + } + + public function testExceptionIsRaisedInCaseOfUnknownContextOperandContainingPropertyPath() + { + $assertion = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'foo.bar'], + '=', + 'test' + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("'foo' is not available in the assertion context"); + + $assertion->assert(new Acl(), new User(), new BlogPost(), 'read'); + } + + public function testExceptionIsRaisedIfContextObjectPropertyCannotBeResolved() + { + $assertion = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age123'], + '=', + 30 + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("'age123' property cannot be resolved on the 'role' object"); + + $assertion->assert(new Acl(), new User(), new BlogPost(), 'read'); + } + + public function testExceptionIsRaisedInCaseThatAssertHasBeenInvokedWithoutPassingContext() + { + $assertion = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + '=', + 'test' + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("'role' is not available in the assertion context"); + + $assertion->assert(new Acl()); + } + + public function testSerialization() + { + $assertion = ExpressionAssertion::fromProperties( + 'foo', + '=', + 'bar' + ); + + $serializedAssertion = serialize($assertion); + + $this->assertContains('left', $serializedAssertion); + $this->assertContains('foo', $serializedAssertion); + $this->assertContains('operator', $serializedAssertion); + $this->assertContains('=', $serializedAssertion); + $this->assertContains('right', $serializedAssertion); + $this->assertContains('bar', $serializedAssertion); + } + + public function testSerializationShouldNotSerializeAssertContext() + { + $assertion = ExpressionAssertion::fromProperties( + 'foo', + '=', + 'bar' + ); + + $serializedAssertion = serialize($assertion); + + $this->assertNotContains('assertContext', $serializedAssertion); + } +} diff --git a/test/TestAsset/UseCase3/BlogPost.php b/test/TestAsset/UseCase3/BlogPost.php new file mode 100644 index 0000000..5865eb0 --- /dev/null +++ b/test/TestAsset/UseCase3/BlogPost.php @@ -0,0 +1,43 @@ + $value) { + $this->$property = $value; + } + } + + public function getResourceId() + { + return 'blogPost'; + } + + public function getShortDescription() + { + return $this->shortDescription; + } + + public function getAuthorName() + { + return $this->author ? $this->author->username : ''; + } +} diff --git a/test/TestAsset/UseCase3/User.php b/test/TestAsset/UseCase3/User.php new file mode 100644 index 0000000..5998e73 --- /dev/null +++ b/test/TestAsset/UseCase3/User.php @@ -0,0 +1,36 @@ + $value) { + $this->$property = $value; + } + } + + public function getRoleId() + { + return $this->role; + } + + public function isAdult() + { + return $this->age >= 18; + } +} From 0d0f2dfc3828ec2f5004f90c2e24f918f709e8b7 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 12:35:28 -0500 Subject: [PATCH 07/13] Change value of OPERATOR_NIN to "!in" This is closer semantically to `!=` and makes it clear it's a negation. Additionally, updates the tests to refer to the operator constants whenever possible. --- src/Assertion/ExpressionAssertion.php | 2 +- test/Assertion/ExpressionAssertionTest.php | 40 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Assertion/ExpressionAssertion.php b/src/Assertion/ExpressionAssertion.php index 94743d0..8d586f7 100644 --- a/src/Assertion/ExpressionAssertion.php +++ b/src/Assertion/ExpressionAssertion.php @@ -24,7 +24,7 @@ final class ExpressionAssertion implements AssertionInterface const OPERATOR_GT = '>'; const OPERATOR_GTE = '>='; const OPERATOR_IN = 'in'; - const OPERATOR_NIN = 'nin'; + const OPERATOR_NIN = '!in'; const OPERATOR_REGEX = 'regex'; /** diff --git a/test/Assertion/ExpressionAssertionTest.php b/test/Assertion/ExpressionAssertionTest.php index b18df45..8c26dc6 100644 --- a/test/Assertion/ExpressionAssertionTest.php +++ b/test/Assertion/ExpressionAssertionTest.php @@ -32,7 +32,7 @@ public function testFromArrayCreation() { $assertion = ExpressionAssertion::fromArray([ 'left' => 'foo', - 'operator' => '=', + 'operator' => ExpressionAssertion::OPERATOR_EQ, 'right' => 'bar' ]); @@ -54,7 +54,7 @@ public function testExceptionIsRaisedInCaseOfInvalidExpressionContextOperandType ExpressionAssertion::fromProperties( [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 123], - 'in', + ExpressionAssertion::OPERATOR_IN, 'test' ); } @@ -97,7 +97,7 @@ public function getExpressions() 'equality' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => '=', + 'operator' => ExpressionAssertion::OPERATOR_EQ, 'right' => 'test', ], 'role' => new User([ @@ -110,7 +110,7 @@ public function getExpressions() 'inequality' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => '!=', + 'operator' => ExpressionAssertion::OPERATOR_NEQ, 'right' => 'test', ], 'role' => new User([ @@ -123,7 +123,7 @@ public function getExpressions() 'boolean-equality' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => '=', + 'operator' => ExpressionAssertion::OPERATOR_EQ, 'right' => true, ], 'role' => $author3, @@ -134,7 +134,7 @@ public function getExpressions() 'greater-than' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], - 'operator' => '>', + 'operator' => ExpressionAssertion::OPERATOR_GT, 'right' => 20, ], 'role' => new User([ @@ -148,7 +148,7 @@ public function getExpressions() 'greater-than-or-equal' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], - 'operator' => '>=', + 'operator' => ExpressionAssertion::OPERATOR_GTE, 'right' => 20, ], 'role' => new User([ @@ -162,7 +162,7 @@ public function getExpressions() 'less-than' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], - 'operator' => '<', + 'operator' => ExpressionAssertion::OPERATOR_LT, 'right' => 30, ], 'role' => new User([ @@ -176,7 +176,7 @@ public function getExpressions() 'less-than-or-equal' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age'], - 'operator' => '<=', + 'operator' => ExpressionAssertion::OPERATOR_LTE, 'right' => 30, ], 'role' => new User([ @@ -190,7 +190,7 @@ public function getExpressions() 'in' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => 'in', + 'operator' => ExpressionAssertion::OPERATOR_IN, 'right' => ['foo', 'bar'], ], 'role' => new User([ @@ -203,7 +203,7 @@ public function getExpressions() 'not-in' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => 'nin', + 'operator' => ExpressionAssertion::OPERATOR_NIN, 'right' => ['foo', 'bar'], ], 'role' => new User([ @@ -216,7 +216,7 @@ public function getExpressions() 'regex' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - 'operator' => 'regex', + 'operator' => ExpressionAssertion::OPERATOR_REGEX, 'right' => '/foobar/', ], 'role' => new User([ @@ -246,7 +246,7 @@ public function getExpressions() 'equality-calculated-property' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.adult'], - 'operator' => '=', + 'operator' => ExpressionAssertion::OPERATOR_EQ, 'right' => true, ], 'role' => new User([ @@ -260,7 +260,7 @@ public function getExpressions() 'privilege' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'privilege'], - 'operator' => '=', + 'operator' => ExpressionAssertion::OPERATOR_EQ, 'right' => 'read', ], 'role' => new User([ @@ -277,7 +277,7 @@ public function testExceptionIsRaisedInCaseOfUnknownContextOperand() { $assertion = ExpressionAssertion::fromProperties( [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'foobar'], - '=', + ExpressionAssertion::OPERATOR_EQ, 'test' ); @@ -291,7 +291,7 @@ public function testExceptionIsRaisedInCaseOfUnknownContextOperandContainingProp { $assertion = ExpressionAssertion::fromProperties( [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'foo.bar'], - '=', + ExpressionAssertion::OPERATOR_EQ, 'test' ); @@ -305,7 +305,7 @@ public function testExceptionIsRaisedIfContextObjectPropertyCannotBeResolved() { $assertion = ExpressionAssertion::fromProperties( [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.age123'], - '=', + ExpressionAssertion::OPERATOR_EQ, 30 ); @@ -319,7 +319,7 @@ public function testExceptionIsRaisedInCaseThatAssertHasBeenInvokedWithoutPassin { $assertion = ExpressionAssertion::fromProperties( [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], - '=', + ExpressionAssertion::OPERATOR_EQ, 'test' ); @@ -333,7 +333,7 @@ public function testSerialization() { $assertion = ExpressionAssertion::fromProperties( 'foo', - '=', + ExpressionAssertion::OPERATOR_EQ, 'bar' ); @@ -351,7 +351,7 @@ public function testSerializationShouldNotSerializeAssertContext() { $assertion = ExpressionAssertion::fromProperties( 'foo', - '=', + ExpressionAssertion::OPERATOR_EQ, 'bar' ); From 46cabc9d0b43a1fad4558b5d7d0603a301016e3c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 15:42:42 -0500 Subject: [PATCH 08/13] Refactor ExpressionAssertion and incorporate feedback - Removes `$assertContext` property, in favor of passing context to methods and using the locally scoped variable directly. This fixes potential concurrency issues. - Removes `__sleep` implementation, as it is no longer necessary with `$assertContext` removed. - Extracts test for "property exists" to its own method. `property_exists()` will return `true` even for non-public properties, so it requires using `ReflectionProperty` to test if it is public. - Moves test for named object in context into `getObjectFieldValue()`, as it is directly related to that functionality. - Modifies `getObjectFieldValue()` to throw a package `RuntimeException` instead of a global `RuntimeException`. It can do this now because the `$context` is passed to it, along with the object name, allowing us to construct the expected exception. - Modifies `resolveOperandValue()` to simplify property unrolling, by having it call `getObjectFieldValue` with the context, object name, and property to retrieve. It no longer needs to test for the object in the context, nor try/catch around retrievng the property. - Modifies the `OPERATOR_NIN` value to be `!in`; `!` is contextually `not`, while `n` is ambiguous. - Adds the operator `OPERATOR_NREGEX` (`!regex`) to allow negated regular expressions. - Adds the operator `OPERATOR_SAME` (`===`) to allow strict equality expressions. - Adds the operator `OPERATOR_NSAME` (`!==`) to allow strict inequality expressions. --- src/Assertion/ExpressionAssertion.php | 292 +++++++++++++-------- test/Assertion/ExpressionAssertionTest.php | 39 +++ 2 files changed, 228 insertions(+), 103 deletions(-) diff --git a/src/Assertion/ExpressionAssertion.php b/src/Assertion/ExpressionAssertion.php index 8d586f7..df18997 100644 --- a/src/Assertion/ExpressionAssertion.php +++ b/src/Assertion/ExpressionAssertion.php @@ -7,25 +7,70 @@ namespace Zend\Permissions\Acl\Assertion; +use ReflectionProperty; use Zend\Permissions\Acl\Acl; use Zend\Permissions\Acl\Role\RoleInterface; use Zend\Permissions\Acl\Resource\ResourceInterface; use Zend\Permissions\Acl\Assertion\Exception\InvalidAssertionException; use Zend\Permissions\Acl\Exception\RuntimeException; +/** + * Create an assertion based on expression rules. + * + * Each of the constructor, fromProperties, and fromArray methods allow you to + * define expression rules, and these include the left hand side, operator, and + * right hand side of the expression. + * + * The left and right hand sides of the expression are the values to compare. + * These values can be either an exact value to match, or an array with the key + * OPERAND_CONTEXT_PROPERTY pointing to one of two value types. + * + * First, it can be a string value matching one of "acl", "privilege", "role", + * or "resource", with the latter two values being the most common. In those + * cases, the matching value passed to `assert()` will be used in the + * comparison. + * + * Second, it can be a dot-separated property path string of the format + * "object.property", representing the associated object (role, resource, acl, + * or privilege) and its property to test against. The property may refer to a + * public property, or a public `get` or `is` method (following + * canonicalization of the property by replacing underscore separated values + * with camelCase values). + */ final class ExpressionAssertion implements AssertionInterface { const OPERAND_CONTEXT_PROPERTY = '__context'; - const OPERATOR_EQ = '='; - const OPERATOR_NEQ = '!='; - const OPERATOR_LT = '<'; - const OPERATOR_LTE = '<='; - const OPERATOR_GT = '>'; - const OPERATOR_GTE = '>='; - const OPERATOR_IN = 'in'; - const OPERATOR_NIN = '!in'; - const OPERATOR_REGEX = 'regex'; + const OPERATOR_EQ = '='; + const OPERATOR_NEQ = '!='; + const OPERATOR_LT = '<'; + const OPERATOR_LTE = '<='; + const OPERATOR_GT = '>'; + const OPERATOR_GTE = '>='; + const OPERATOR_IN = 'in'; + const OPERATOR_NIN = '!in'; + const OPERATOR_REGEX = 'regex'; + const OPERATOR_NREGEX = '!regex'; + const OPERATOR_SAME = '==='; + const OPERATOR_NSAME = '!=='; + + /** + * @var array + */ + private static $validOperators = [ + self::OPERATOR_EQ, + self::OPERATOR_NEQ, + self::OPERATOR_LT, + self::OPERATOR_LTE, + self::OPERATOR_GT, + self::OPERATOR_GTE, + self::OPERATOR_IN, + self::OPERATOR_NIN, + self::OPERATOR_REGEX, + self::OPERATOR_NREGEX, + self::OPERATOR_SAME, + self::OPERATOR_NSAME, + ]; /** * @var mixed @@ -43,25 +88,15 @@ final class ExpressionAssertion implements AssertionInterface private $right; /** - * @var array - */ - private $assertContext = []; - - /** - * @var array + * Constructor + * + * Note that the constructor is marked private; use `fromProperties()` or + * `fromArray()` to create an instance. + * + * @param mixed|array $left See the class description for valid values. + * @param string $operator One of the OPERATOR constants (or their values) + * @param mixed|array $right See the class description for valid values. */ - private static $validOperators = [ - self::OPERATOR_EQ, - self::OPERATOR_NEQ, - self::OPERATOR_LT, - self::OPERATOR_LTE, - self::OPERATOR_GT, - self::OPERATOR_GTE, - self::OPERATOR_IN, - self::OPERATOR_NIN, - self::OPERATOR_REGEX, - ]; - private function __construct($left, $operator, $right) { $this->left = $left; @@ -70,10 +105,12 @@ private function __construct($left, $operator, $right) } /** - * @param mixed $left - * @param string $operator - * @param mixed $right + * @param mixed|array $left See the class description for valid values. + * @param string $operator One of the OPERATOR constants (or their values) + * @param mixed|array $right See the class description for valid values. * @return self + * @throws InvalidAssertionException if either operand is invalid. + * @throws InvalidAssertionException if the operator is not supported. */ public static function fromProperties($left, $operator, $right) { @@ -87,9 +124,16 @@ public static function fromProperties($left, $operator, $right) } /** - * @param array $expression - * @throws InvalidAssertionException + * @param array $expression Must contain the following keys: + * - left: the left-hand side of the expression + * - operator: the operator to use for the comparison + * - right: the right-hand side of the expression + * See the class description for valid values for the left and right + * hand side values. * @return self + * @throws InvalidAssertionException if missing one of the required keys. + * @throws InvalidAssertionException if either operand is invalid. + * @throws InvalidAssertionException if the operator is not supported. */ public static function fromArray(array $expression) { @@ -108,6 +152,10 @@ public static function fromArray(array $expression) ); } + /** + * @param mixed|array $operand + * @throws InvalidAssertionException if the operand is invalid. + */ private static function validateOperand($operand) { if (is_array($operand) && isset($operand[self::OPERAND_CONTEXT_PROPERTY])) { @@ -117,9 +165,13 @@ private static function validateOperand($operand) } } + /** + * @param string $operand + * @throws InvalidAssertionException if the operator is not supported. + */ private static function validateOperator($operator) { - if (! in_array($operator, self::$validOperators)) { + if (! in_array($operator, self::$validOperators, true)) { throw new InvalidAssertionException('Provided expression assertion operator is not supported'); } } @@ -129,100 +181,127 @@ private static function validateOperator($operator) */ public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) { - $this->assertContext = [ - 'acl' => $acl, - 'role' => $role, - 'resource' => $resource, + return $this->evaluate([ + 'acl' => $acl, + 'role' => $role, + 'resource' => $resource, 'privilege' => $privilege, - ]; - - return $this->evaluate(); + ]); } - private function evaluate() + /** + * @param array $context Contains the acl, privilege, role, and resource + * being tested currently. + * @return bool + */ + private function evaluate(array $context) { - $left = $this->getLeftValue(); - $right = $this->getRightValue(); + $left = $this->getLeftValue($context); + $right = $this->getRightValue($context); return static::evaluateExpression($left, $this->operator, $right); } - private function getLeftValue() + /** + * @param array $context Contains the acl, privilege, role, and resource + * being tested currently. + * @return mixed + */ + private function getLeftValue(array $context) { - return $this->resolveOperandValue($this->left); + return $this->resolveOperandValue($this->left, $context); } - private function getRightValue() + /** + * @param array $context Contains the acl, privilege, role, and resource + * being tested currently. + * @return mixed + */ + private function getRightValue(array $context) { - return $this->resolveOperandValue($this->right); + return $this->resolveOperandValue($this->right, $context); } - private function resolveOperandValue($operand) + /** + * @param mixed|array + * @param array $context Contains the acl, privilege, role, and resource + * being tested currently. + * @return mixed + * @throws RuntimeException if object cannot be resolved in context. + * @throws RuntimeException if property cannot be resolved. + */ + private function resolveOperandValue($operand, array $context) { - if (is_array($operand) && isset($operand[self::OPERAND_CONTEXT_PROPERTY])) { - $contextProperty = $operand[self::OPERAND_CONTEXT_PROPERTY]; - - if (strpos($contextProperty, '.') !== false) { //property path? - list($objectName, $objectField) = explode('.', $contextProperty, 2); - - if (! isset($this->assertContext[$objectName])) { - throw new RuntimeException(sprintf( - "'%s' is not available in the assertion context", - $objectName - )); - } - - try { - return $this->getObjectFieldValue($this->assertContext[$objectName], $objectField); - } catch (\RuntimeException $ex) { - throw new RuntimeException(sprintf( - "'%s' property cannot be resolved on the '%s' object", - $objectField, - $objectName - )); - } - } + if (! is_array($operand) || ! isset($operand[self::OPERAND_CONTEXT_PROPERTY])) { + return $operand; + } - if (! isset($this->assertContext[$contextProperty])) { - throw new RuntimeException(sprintf( - "'%s' is not available in the assertion context", - $contextProperty - )); - } + $contextProperty = $operand[self::OPERAND_CONTEXT_PROPERTY]; - return $this->assertContext[$contextProperty]; + if (strpos($contextProperty, '.') !== false) { // property path? + list($objectName, $objectField) = explode('.', $contextProperty, 2); + return $this->getObjectFieldValue($context, $objectName, $objectField); } - return $operand; + if (! isset($context[$contextProperty])) { + throw new RuntimeException(sprintf( + "'%s' is not available in the assertion context", + $contextProperty + )); + } + + return $context[$contextProperty]; } - private function getObjectFieldValue($object, $field) + /** + * @param array $context Contains the acl, privilege, role, and resource + * being tested currently. + * @param string $objectName Name of object in context to use. + * @param string $field + * @return mixed + * @throws RuntimeException if object cannot be resolved in context. + * @throws RuntimeException if property cannot be resolved. + */ + private function getObjectFieldValue(array $context, $objectName, $field) { - $accessors = ['get', 'is']; - - $fieldAccessor = $field; - - if (false !== strpos($field, '_')) { - $fieldAccessor = str_replace(' ', '', ucwords(str_replace('_', ' ', $field))); + if (! isset($context[$objectName])) { + throw new RuntimeException(sprintf( + "'%s' is not available in the assertion context", + $objectName + )); } + $object = $context[$objectName]; + $accessors = ['get', 'is']; + $fieldAccessor = false === strpos($field, '_') + ? $field + : str_replace(' ', '', ucwords(str_replace('_', ' ', $field))); + foreach ($accessors as $accessor) { $accessor .= $fieldAccessor; - if (! method_exists($object, $accessor)) { - continue; + if (method_exists($object, $accessor)) { + return $object->$accessor(); } - - return $object->$accessor(); } - if (! property_exists($object, $field)) { - throw new \RuntimeException('Object property cannot be resolved'); + if (! $this->propertyExists($object, $field)) { + throw new RuntimeException(sprintf( + "'%s' property cannot be resolved on the '%s' object", + $field, + $objectName + )); } return $object->$field; } + /** + * @param mixed $left + * @param string $right + * @param mixed $right + * @throws RuntimeException if operand is not supported. + */ private static function evaluateExpression($left, $operator, $right) { switch ($operator) { @@ -244,20 +323,27 @@ private static function evaluateExpression($left, $operator, $right) return ! in_array($left, $right); case self::OPERATOR_REGEX: return (bool) preg_match($right, $left); - default: - throw new RuntimeException(sprintf( - 'Unsupported expression assertion operator: %s', - $operator - )); + case self::OPERATOR_NREGEX: + return ! (bool) preg_match($right, $left); + case self::OPERATOR_SAME: + return $left === $right; + case self::OPERATOR_NSAME: + return $left !== $right; } } - public function __sleep() + /** + * @param object $object + * @param string $field + * @return mixed + */ + private function propertyExists($object, $property) { - return [ - 'left', - 'operator', - 'right', - ]; + if (! property_exists($object, $property)) { + return false; + } + + $r = new ReflectionProperty($object, $property); + return $r->isPublic(); } } diff --git a/test/Assertion/ExpressionAssertionTest.php b/test/Assertion/ExpressionAssertionTest.php index 8c26dc6..0f4db95 100644 --- a/test/Assertion/ExpressionAssertionTest.php +++ b/test/Assertion/ExpressionAssertionTest.php @@ -243,6 +243,45 @@ public function getExpressions() 'privilege' => 'read', 'assert' => true, ], + 'nregex' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => ExpressionAssertion::OPERATOR_NREGEX, + 'right' => '/barbaz/', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'same' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => ExpressionAssertion::OPERATOR_SAME, + 'right' => 'test', + ], + 'role' => new User([ + 'username' => 'test', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], + 'not-same' => [ + 'expression' => [ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + 'operator' => ExpressionAssertion::OPERATOR_NSAME, + 'right' => 'test', + ], + 'role' => new User([ + 'username' => 'foobar', + ]), + 'resource' => new BlogPost(), + 'privilege' => 'read', + 'assert' => true, + ], 'equality-calculated-property' => [ 'expression' => [ 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.adult'], From 9e42ed5599750a7b69271623211db0017356bc84 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 16:09:47 -0500 Subject: [PATCH 09/13] Provides documentation for ExpressionAssertion --- docs/book/expression.md | 195 ++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 196 insertions(+) create mode 100644 docs/book/expression.md diff --git a/docs/book/expression.md b/docs/book/expression.md new file mode 100644 index 0000000..c4a92e3 --- /dev/null +++ b/docs/book/expression.md @@ -0,0 +1,195 @@ +# Expression Assertions + +- Since 2.7.0 + +Many custom assertions are doing basic comparisons: + +- Equality of a role property to a value or property of the resource. +- Other comparisons (`>`, `<`, `in_array`, etc.) of a role property to a value + or values (potentially a property of the resource). +- Regular expressions. + +While these can be easily accommodated by the `CallbackAssertion`, such +assertions have one notable problem: they cannot be easily serialized. + +To facilitate such assertions, we now provide +`Zend\Permissions\Acl\Assertion\ExpressionAssertion`. This class provides two +static factory methods for creating an instance, each expecting the following: + +- The left operand +- An operator +- The right operand + +When the assertion is executed, it uses the operator to determine how to compare +the two operands, and thus answer the assertion. + +## Operands + +The operands can be any PHP value. + +Additionally, they can be an associative array containing the key +`ExpressionAssertion::OPERAND_CONTEXT_PROPERTY` (`__context`), with a string +value. + +That value can be one of the following: + +- A string matching the values "acl", "privilege", "role", or "resource", with + the latter two being most common. When one of these is provided, the + corresponding argument to the `assert()` method will be used. + +- A dot-separated string with the first segment being one of the above values, + and the second being a property or field of that object. The + `ExpressionAssertion` will test for: + + - a method matching `get()` + - a method matching `is()` + - a public property named `` + + in that specific order. In the first two cases, `` will be normalized + to WordCase when creating the method name to test. + +## Operators + +`ExpressionAssertion` supports the following operators: + +```php + const OPERATOR_EQ = '='; + const OPERATOR_NEQ = '!='; + const OPERATOR_LT = '<'; + const OPERATOR_LTE = '<='; + const OPERATOR_GT = '>'; + const OPERATOR_GTE = '>='; + const OPERATOR_IN = 'in'; + const OPERATOR_NIN = '!in'; + const OPERATOR_REGEX = 'regex'; + const OPERATOR_NREGEX = '!regex'; + const OPERATOR_SAME = '==='; + const OPERATOR_NSAME = '!=='; +``` + +In most cases, these will operate using the operators as listed above, with the +following exceptions: + +- `OPERATOR_EQ` will use `==` as the comparison operator; `OPERATOR_NEQ` will + likewise use `!=`. +- `OPERATOR_IN` and `OPERATOR_NIN` use `in_array()` (with the latter negating + the result), both doing strict comparisons. The right hand operand is expected + to be the array in which to look for results, and the left hand operand is + expected to be the needle to look for. +- `OPERATOR_REGEX` and `OPERATOR_NREGEX` will perform a `preg_match()` + operation, using the right hand operand as the regular expression, and the + left hand operand as the value to compare. + +## Constructors + +The constructor of `ExpressionAssertion` is private. Instead, you will use one +of two static methods in order to create instances: + +- `fromProperties($left, $operator, $right)` +- `fromArray(array $expression)` (expects keys for "left", "operator", and "right") + +When creating expressions manually, the first is generally the best choice. When +storing expressions in configuration or a database, the latter is useful, as you +can pass a row of data at a time to the method to get expression instances. + +## Examples + +First, we'll define both a role and a resource: + +```php +namespace Blog\Entity; + +use Zend\Permissions\Acl\Resource\ResourceInterface; +use Zend\Permissions\Acl\Role\RoleInterface; + +class BlogPost implements ResourceInterface +{ + public $title; + + public $shortDescription; + + public $content; + + public $author; + + public function __construct(array $data = []) + { + foreach ($data as $property => $value) { + $this->$property = $value; + } + } + + public function getResourceId() + { + return 'blogPost'; + } + + public function getShortDescription() + { + return $this->shortDescription; + } + + public function getAuthorName() + { + return $this->author ? $this->author->username : ''; + } +} + +class User implements RoleInterface +{ + public $username; + + public $role = 'guest'; + + public $age; + + public function __construct(array $data = []) + { + foreach ($data as $property => $value) { + $this->$property = $value; + } + } + + public function getRoleId() + { + return $this->role; + } + + public function isAdult() + { + return $this->age >= 18; + } +} +``` + +Next, let's define some assertions. + +```php +use Zend\Permissions\Acl\Assertion\ExpressionAssertion; + +// Username of role must be "test": +// Will access $username property on the role instance. +$isTestUser = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'], + '===', + 'test' +); + + +// Role must be at least 18 years old: +// Will execute `isAdult()` on the role instance. +$isOfLegalAge = ExpressionAssertion::fromProperties( + [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.adult'], + '===', + true +); + +// Must have edited text: +// Will do a regex comparison on the shortDescription of the blog post +// to ensure we do not have filler text. +$isEditedDescription = ExpressionAssertion::fromArray([ + 'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'resource.shortDescription'], + 'operator' => '!regex', + 'right' => '/lorem ipsum/i', +]); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 6d29540..9019ef5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ pages: - 'Refining ACLs': refining.md - Reference: - 'Ownership Assertions': ownership.md + - 'Expression Assertions': expression.md - 'Advanced Usage': advanced.md site_name: zend-permissions-acl site_description: 'Provides a lightweight and flexible access control list (ACL) implementation for privileges management' From 5ad8b0fcf4940cfbf9d1ef9489dc609bf83c9f3a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 16:40:26 -0500 Subject: [PATCH 10/13] Renames use case directories and cleans up tests - `UseCase1` => `StandardUseCase` - `UseCase2` => `OwnershipUseCase` - `UseCase3` => `ExpressionUseCase` When updating `AclTest` to fix mentions of `UseCase1`, I also found a number of invalid `expectException()` statements (passing the message as well as the class), usage of strings for FQCN, missing imports, and inconsistent indentation, so fixed all of those as well. --- test/AclTest.php | 271 ++++++++++-------- test/Assertion/ExpressionAssertionTest.php | 4 +- test/Assertion/OwnershipAssertionTest.php | 22 +- .../BlogPost.php | 2 +- .../{UseCase3 => ExpressionUseCase}/User.php | 2 +- .../{UseCase2 => OwnershipUseCase}/Acl.php | 6 +- .../Author1.php | 2 +- .../Author2.php | 2 +- .../BlogPost.php | 2 +- .../Comment.php | 2 +- .../{UseCase2 => OwnershipUseCase}/User.php | 2 +- test/TestAsset/StandardUseCase/Acl.php | 34 +++ test/TestAsset/StandardUseCase/BlogPost.php | 20 ++ test/TestAsset/StandardUseCase/User.php | 20 ++ .../UserIsBlogPostOwnerAssertion.php | 33 +++ test/TestAsset/UseCase1/Acl.php | 31 -- test/TestAsset/UseCase1/BlogPost.php | 21 -- test/TestAsset/UseCase1/User.php | 21 -- .../UseCase1/UserIsBlogPostOwnerAssertion.php | 34 --- 19 files changed, 288 insertions(+), 243 deletions(-) rename test/TestAsset/{UseCase3 => ExpressionUseCase}/BlogPost.php (93%) rename test/TestAsset/{UseCase3 => ExpressionUseCase}/User.php (92%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/Acl.php (87%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/Author1.php (86%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/Author2.php (86%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/BlogPost.php (92%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/Comment.php (88%) rename test/TestAsset/{UseCase2 => OwnershipUseCase}/User.php (91%) create mode 100644 test/TestAsset/StandardUseCase/Acl.php create mode 100644 test/TestAsset/StandardUseCase/BlogPost.php create mode 100644 test/TestAsset/StandardUseCase/User.php create mode 100644 test/TestAsset/StandardUseCase/UserIsBlogPostOwnerAssertion.php delete mode 100644 test/TestAsset/UseCase1/Acl.php delete mode 100644 test/TestAsset/UseCase1/BlogPost.php delete mode 100644 test/TestAsset/UseCase1/User.php delete mode 100644 test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php diff --git a/test/AclTest.php b/test/AclTest.php index 2b3a8d6..49a230b 100644 --- a/test/AclTest.php +++ b/test/AclTest.php @@ -10,15 +10,13 @@ namespace ZendTest\Permissions\Acl; use PHPUnit\Framework\TestCase; +use stdClass; use Zend\Permissions\Acl; use Zend\Permissions\Acl\Exception\ExceptionInterface; use Zend\Permissions\Acl\Exception\InvalidArgumentException; use Zend\Permissions\Acl\Resource; use Zend\Permissions\Acl\Role; -/** - * @group Zend_Acl - */ class AclTest extends TestCase { /** @@ -59,9 +57,10 @@ public function testRoleRegistryAddAndGetOne() */ public function testRoleAddAndGetOneByString() { - $role = $this->acl->addRole('area') - ->getRole('area'); - $this->assertInstanceOf('Zend\Permissions\Acl\Role\RoleInterface', $role); + $role = $this->acl + ->addRole('area') + ->getRole('area'); + $this->assertInstanceOf(Role\RoleInterface::class, $role); $this->assertEquals('area', $role->getRoleId()); } @@ -73,8 +72,9 @@ public function testRoleAddAndGetOneByString() public function testRoleRegistryRemoveOne() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->removeRole($roleGuest); + $this->acl + ->addRole($roleGuest) + ->removeRole($roleGuest); $this->assertFalse($this->acl->hasRole($roleGuest)); } @@ -97,8 +97,9 @@ public function testRoleRegistryRemoveOneNonExistent() public function testRoleRegistryRemoveAll() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->removeRoleAll(); + $this->acl + ->addRole($roleGuest) + ->removeRoleAll(); $this->assertFalse($this->acl->hasRole($roleGuest)); } @@ -120,10 +121,8 @@ public function testRoleRegistryAddInheritsNonExistent() */ public function testRoleRegistryAddNotRole() { - $this->expectException( - InvalidArgumentException::class, - 'addRole() expects $role to be of type Zend\Permissions\Acl\Role' - ); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('addRole() expects $role to be of type Zend\Permissions\Acl\Role'); $this->acl->addRole(new \stdClass, 'guest'); } @@ -165,9 +164,10 @@ public function testRoleRegistryInherits() $roleMember = new Role\GenericRole('member'); $roleEditor = new Role\GenericRole('editor'); $roleRegistry = new Role\Registry(); - $roleRegistry->add($roleGuest) - ->add($roleMember, $roleGuest->getRoleId()) - ->add($roleEditor, $roleMember); + $roleRegistry + ->add($roleGuest) + ->add($roleMember, $roleGuest->getRoleId()) + ->add($roleEditor, $roleMember); $this->assertEmpty($roleRegistry->getParents($roleGuest)); $roleMemberParents = $roleRegistry->getParents($roleMember); $this->assertCount(1, $roleMemberParents); @@ -197,9 +197,10 @@ public function testRoleRegistryInheritsMultipleArray() $roleParent2 = new Role\GenericRole('parent2'); $roleChild = new Role\GenericRole('child'); $roleRegistry = new Role\Registry(); - $roleRegistry->add($roleParent1) - ->add($roleParent2) - ->add($roleChild, [$roleParent1, $roleParent2]); + $roleRegistry + ->add($roleParent1) + ->add($roleParent2) + ->add($roleChild, [$roleParent1, $roleParent2]); $roleChildParents = $roleRegistry->getParents($roleChild); $this->assertCount(2, $roleChildParents); $i = 1; @@ -227,7 +228,8 @@ public function testRoleRegistryInheritsMultipleTraversable() $roleParent2 = new Role\GenericRole('parent2'); $roleChild = new Role\GenericRole('child'); $roleRegistry = new Role\Registry(); - $roleRegistry->add($roleParent1) + $roleRegistry + ->add($roleParent1) ->add($roleParent2) ->add( $roleChild, @@ -259,8 +261,9 @@ public function testRoleRegistryDuplicate() $roleGuest = new Role\GenericRole('guest'); $roleRegistry = new Role\Registry(); $this->expectException(InvalidArgumentException::class, 'already exists'); - $roleRegistry->add($roleGuest) - ->add($roleGuest); + $roleRegistry + ->add($roleGuest) + ->add($roleGuest); } /** @@ -274,8 +277,9 @@ public function testRoleRegistryDuplicateId() $roleGuest2 = new Role\GenericRole('guest'); $roleRegistry = new Role\Registry(); $this->expectException(InvalidArgumentException::class, 'already exists'); - $roleRegistry->add($roleGuest1) - ->add($roleGuest2); + $roleRegistry + ->add($roleGuest1) + ->add($roleGuest2); } /** @@ -286,8 +290,9 @@ public function testRoleRegistryDuplicateId() public function testResourceAddAndGetOne() { $resourceArea = new Resource\GenericResource('area'); - $resource = $this->acl->addResource($resourceArea) - ->getResource($resourceArea->getResourceId()); + $resource = $this->acl + ->addResource($resourceArea) + ->getResource($resourceArea->getResourceId()); $this->assertEquals($resourceArea, $resource); $resource = $this->acl->getResource($resourceArea); $this->assertEquals($resourceArea, $resource); @@ -298,9 +303,10 @@ public function testResourceAddAndGetOne() */ public function testResourceAddAndGetOneByString() { - $resource = $this->acl->addResource('area') - ->getResource('area'); - $this->assertInstanceOf('Zend\Permissions\Acl\Resource\ResourceInterface', $resource); + $resource = $this->acl + ->addResource('area') + ->getResource('area'); + $this->assertInstanceOf(Resource\ResourceInterface::class, $resource); $this->assertEquals('area', $resource->getResourceId()); } @@ -312,8 +318,9 @@ public function testResourceAddAndGetOneByString() public function testResourceAddAndGetOneWithAddResourceMethod() { $resourceArea = new Resource\GenericResource('area'); - $resource = $this->acl->addResource($resourceArea) - ->getResource($resourceArea->getResourceId()); + $resource = $this->acl + ->addResource($resourceArea) + ->getResource($resourceArea->getResourceId()); $this->assertEquals($resourceArea, $resource); $resource = $this->acl->getResource($resourceArea); $this->assertEquals($resourceArea, $resource); @@ -327,8 +334,9 @@ public function testResourceAddAndGetOneWithAddResourceMethod() public function testResourceRemoveOne() { $resourceArea = new Resource\GenericResource('area'); - $this->acl->addResource($resourceArea) - ->removeResource($resourceArea); + $this->acl + ->addResource($resourceArea) + ->removeResource($resourceArea); $this->assertFalse($this->acl->hasResource($resourceArea)); } @@ -339,7 +347,8 @@ public function testResourceRemoveOne() */ public function testResourceRemoveOneNonExistent() { - $this->expectException(ExceptionInterface::class, 'not found'); + $this->expectException(ExceptionInterface::class); + $this->expectExceptionMessage('not found'); $this->acl->removeResource('nonexistent'); } @@ -351,8 +360,9 @@ public function testResourceRemoveOneNonExistent() public function testResourceRemoveAll() { $resourceArea = new Resource\GenericResource('area'); - $this->acl->addResource($resourceArea) - ->removeResourceAll(); + $this->acl + ->addResource($resourceArea) + ->removeResourceAll(); $this->assertFalse($this->acl->hasResource($resourceArea)); } @@ -363,7 +373,8 @@ public function testResourceRemoveAll() */ public function testResourceAddInheritsNonExistent() { - $this->expectException(InvalidArgumentException::class, 'does not exist'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('does not exist'); $this->acl->addResource(new Resource\GenericResource('area'), 'nonexistent'); } @@ -374,11 +385,9 @@ public function testResourceAddInheritsNonExistent() */ public function testResourceRegistryAddNotResource() { - $this->expectException( - InvalidArgumentException::class, - 'addResource() expects $resource to be of type Zend\Permissions\Acl\Resource' - ); - $this->acl->addResource(new \stdClass); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('addResource() expects $resource to be of type Zend\Permissions\Acl\Resource'); + $this->acl->addResource(new stdClass); } /** @@ -393,8 +402,8 @@ public function testResourceInheritsNonExistent() try { $this->acl->inheritsResource('nonexistent', $resourceArea); $this->fail( - 'Expected Zend\Permissions\Acl\Exception\ExceptionInterface not ' - . 'thrown upon specifying a non-existent child Resource' + '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()); @@ -420,9 +429,10 @@ 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->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)); @@ -440,10 +450,14 @@ public function testResourceInherits() */ public function testResourceDuplicate() { - $this->expectException(ExceptionInterface::class, 'already exists'); $resourceArea = new Resource\GenericResource('area'); - $this->acl->addResource($resourceArea) - ->addResource($resourceArea); + + $this->expectException(ExceptionInterface::class); + $this->expectExceptionMessage('already exists'); + + $this->acl + ->addResource($resourceArea) + ->addResource($resourceArea); } /** @@ -453,11 +467,15 @@ public function testResourceDuplicate() */ public function testResourceDuplicateId() { - $this->expectException(ExceptionInterface::class, 'already exists'); $resourceArea1 = new Resource\GenericResource('area'); $resourceArea2 = new Resource\GenericResource('area'); - $this->acl->addResource($resourceArea1) - ->addResource($resourceArea2); + + $this->expectException(ExceptionInterface::class); + $this->expectExceptionMessage('already exists'); + + $this->acl + ->addResource($resourceArea1) + ->addResource($resourceArea2); } /** @@ -578,8 +596,10 @@ public function testPrivileges() $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, ['p2', 'p3']); $this->assertFalse($this->acl->isAllowed(null, null, 'p2')); $this->assertFalse($this->acl->isAllowed(null, null, 'p3')); @@ -594,6 +614,7 @@ 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')); } @@ -618,9 +639,11 @@ public function testRoleDefaultDeny() public function testRoleDefaultRuleSet() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest); + $this->acl + ->addRole($roleGuest) + ->allow($roleGuest); $this->assertTrue($this->acl->isAllowed($roleGuest)); + $this->acl->deny($roleGuest); $this->assertFalse($this->acl->isAllowed($roleGuest)); } @@ -645,9 +668,12 @@ public function testRoleDefaultPrivilegeDeny() public function testRoleDefaultRuleSetPrivilege() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest); + + $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')); } @@ -660,8 +686,9 @@ public function testRoleDefaultRuleSetPrivilege() public function testRolePrivilegeAllow() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest, null, 'somePrivilege'); + $this->acl + ->addRole($roleGuest) + ->allow($roleGuest, null, 'somePrivilege'); $this->assertTrue($this->acl->isAllowed($roleGuest, null, 'somePrivilege')); } @@ -673,9 +700,10 @@ public function testRolePrivilegeAllow() public function testRolePrivilegeDeny() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest) - ->deny($roleGuest, null, 'somePrivilege'); + $this->acl + ->addRole($roleGuest) + ->allow($roleGuest) + ->deny($roleGuest, null, 'somePrivilege'); $this->assertFalse($this->acl->isAllowed($roleGuest, null, 'somePrivilege')); } @@ -687,14 +715,17 @@ public function testRolePrivilegeDeny() public function testRolePrivileges() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest, null, ['p1', 'p2', 'p3']); + $this->acl + ->addRole($roleGuest) + ->allow($roleGuest, null, ['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, ['p2', 'p3']); $this->assertFalse($this->acl->isAllowed($roleGuest, null, 'p2')); $this->assertFalse($this->acl->isAllowed($roleGuest, null, 'p3')); @@ -708,9 +739,11 @@ public function testRolePrivileges() public function testRolePrivilegeAssert() { $roleGuest = new Role\GenericRole('guest'); - $this->acl->addRole($roleGuest) - ->allow($roleGuest, null, 'somePrivilege', new TestAsset\MockAssertion(true)); + $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')); } @@ -771,8 +804,9 @@ public function testRemoveDefaultAllowNonExistent() */ public function testRemoveDefaultDenyNonExistent() { - $this->acl->allow() - ->removeDeny(); + $this->acl + ->allow() + ->removeDeny(); $this->assertTrue($this->acl->isAllowed()); } @@ -784,13 +818,14 @@ public function testRemoveDefaultDenyNonExistent() */ 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', ['area1', 'area2']); + $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', ['area1', 'area2']); $this->assertFalse($this->acl->isAllowed('staff', 'area1')); } @@ -802,11 +837,12 @@ public function testRoleDefaultAllowRuleWithResourceDenyRule() */ public function testRoleDefaultAllowRuleWithPrivilegeDenyRule() { - $this->acl->addRole(new Role\GenericRole('guest')) - ->addRole(new Role\GenericRole('staff'), 'guest') - ->deny() - ->allow('staff') - ->deny('staff', null, ['privilege1', 'privilege2']); + $this->acl + ->addRole(new Role\GenericRole('guest')) + ->addRole(new Role\GenericRole('staff'), 'guest') + ->deny() + ->allow('staff') + ->deny('staff', null, ['privilege1', 'privilege2']); $this->assertFalse($this->acl->isAllowed('staff', null, 'privilege1')); } @@ -821,6 +857,7 @@ public function testRulesRemove() $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')); @@ -833,8 +870,9 @@ public function testRulesRemove() */ public function testRuleRoleRemove() { - $this->acl->addRole(new Role\GenericRole('guest')) - ->allow('guest'); + $this->acl + ->addRole(new Role\GenericRole('guest')) + ->allow('guest'); $this->assertTrue($this->acl->isAllowed('guest')); $this->acl->removeRole('guest'); try { @@ -856,9 +894,11 @@ public function testRuleRoleRemove() */ public function testRuleRoleRemoveAll() { - $this->acl->addRole(new Role\GenericRole('guest')) - ->allow('guest'); + $this->acl + ->addRole(new Role\GenericRole('guest')) + ->allow('guest'); $this->assertTrue($this->acl->isAllowed('guest')); + $this->acl->removeRoleAll(); try { $this->acl->isAllowed('guest'); @@ -879,8 +919,9 @@ public function testRuleRoleRemoveAll() */ public function testRulesResourceRemove() { - $this->acl->addResource(new Resource\GenericResource('area')) - ->allow(null, 'area'); + $this->acl + ->addResource(new Resource\GenericResource('area')) + ->allow(null, 'area'); $this->assertTrue($this->acl->isAllowed(null, 'area')); $this->acl->removeResource('area'); try { @@ -902,8 +943,9 @@ public function testRulesResourceRemove() */ public function testRulesResourceRemoveAll() { - $this->acl->addResource(new Resource\GenericResource('area')) - ->allow(null, 'area'); + $this->acl + ->addResource(new Resource\GenericResource('area')) + ->allow(null, 'area'); $this->assertTrue($this->acl->isAllowed(null, 'area')); $this->acl->removeResourceAll(); try { @@ -927,10 +969,11 @@ public function testRulesResourceRemoveAll() 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')); + $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'); @@ -987,12 +1030,13 @@ public function testCMSExample() $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->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')); @@ -1093,9 +1137,10 @@ public function testCMSExample() */ public function testRoleInheritanceSupportsCheckingOnlyParents() { - $this->acl->addRole(new Role\GenericRole('grandparent')) - ->addRole(new Role\GenericRole('parent'), 'grandparent') - ->addRole(new Role\GenericRole('child'), 'parent'); + $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)); } @@ -1153,13 +1198,13 @@ public function testAclInternalDFSMethodsBehaveProperly() */ public function testAclAssertionsGetProperRoleWhenInheritenceIsUsed() { - $acl = $this->loadUseCase1(); + $acl = $this->loadStandardUseCase(); $user = new Role\GenericRole('publisher'); $blogPost = new Resource\GenericResource('blogPost'); /** - * @var ZendTest\Permissions\Acl\UseCase1\UserIsBlogPostOwnerAssertion + * @var ZendTest\Permissions\Acl\StandardUseCase\UserIsBlogPostOwnerAssertion */ $assertion = $acl->customAssertion; @@ -1174,15 +1219,15 @@ public function testAclAssertionsGetProperRoleWhenInheritenceIsUsed() */ public function testAclAssertionsGetOriginalIsAllowedObjects() { - $acl = $this->loadUseCase1(); + $acl = $this->loadStandardUseCase(); - $user = new TestAsset\UseCase1\User(); - $blogPost = new TestAsset\UseCase1\BlogPost(); + $user = new TestAsset\StandardUseCase\User(); + $blogPost = new TestAsset\StandardUseCase\BlogPost(); $this->assertTrue($acl->isAllowed($user, $blogPost, 'view')); /** - * @var ZendTest\Permissions\Acl\UseCase1\UserIsBlogPostOwnerAssertion + * @var ZendTest\Permissions\Acl\StandardUseCase\UserIsBlogPostOwnerAssertion */ $assertion = $acl->customAssertion; @@ -1194,12 +1239,12 @@ public function testAclAssertionsGetOriginalIsAllowedObjects() // check to see if the last assertion has the proper objects $this->assertInstanceOf( - 'ZendTest\Permissions\Acl\TestAsset\UseCase1\User', + TestAsset\StandardUseCase\User::class, $assertion->lastAssertRole, 'Assertion did not receive proper role object' ); $this->assertInstanceOf( - 'ZendTest\Permissions\Acl\TestAsset\UseCase1\BlogPost', + TestAsset\StandardUseCase\BlogPost::class, $assertion->lastAssertResource, 'Assertion did not receive proper resource object' ); @@ -1207,11 +1252,11 @@ public function testAclAssertionsGetOriginalIsAllowedObjects() /** * - * @return Zend_Acl_UseCase1_Acl + * @return TestAsset\StandardUseCase\Acl */ - protected function loadUseCase1() + protected function loadStandardUseCase() { - return new TestAsset\UseCase1\Acl(); + return new TestAsset\StandardUseCase\Acl(); } /** diff --git a/test/Assertion/ExpressionAssertionTest.php b/test/Assertion/ExpressionAssertionTest.php index 0f4db95..abfde97 100644 --- a/test/Assertion/ExpressionAssertionTest.php +++ b/test/Assertion/ExpressionAssertionTest.php @@ -12,8 +12,8 @@ use Zend\Permissions\Acl\Assertion\Exception\InvalidAssertionException; use Zend\Permissions\Acl\Assertion\ExpressionAssertion; use Zend\Permissions\Acl\Exception\RuntimeException; -use ZendTest\Permissions\Acl\TestAsset\UseCase3\User; -use ZendTest\Permissions\Acl\TestAsset\UseCase3\BlogPost; +use ZendTest\Permissions\Acl\TestAsset\ExpressionUseCase\User; +use ZendTest\Permissions\Acl\TestAsset\ExpressionUseCase\BlogPost; class ExpressionAssertionTest extends TestCase { diff --git a/test/Assertion/OwnershipAssertionTest.php b/test/Assertion/OwnershipAssertionTest.php index 04b2452..8a35948 100644 --- a/test/Assertion/OwnershipAssertionTest.php +++ b/test/Assertion/OwnershipAssertionTest.php @@ -8,13 +8,13 @@ namespace ZendTest\Permissions\Acl\Assertion; use PHPUnit\Framework\TestCase; -use ZendTest\Permissions\Acl\TestAsset\UseCase2; +use ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; class OwnershipAssertionTest extends TestCase { public function testAssertPassesIfRoleIsNotProprietary() { - $acl = new UseCase2\Acl(); + $acl = new OwnershipUseCase\Acl(); $this->assertTrue($acl->isAllowed('guest', 'blogPost', 'view')); $this->assertFalse($acl->isAllowed('guest', 'blogPost', 'delete')); @@ -22,9 +22,9 @@ public function testAssertPassesIfRoleIsNotProprietary() public function testAssertPassesIfResourceIsNotProprietary() { - $acl = new UseCase2\Acl(); + $acl = new OwnershipUseCase\Acl(); - $author = new UseCase2\Author1(); + $author = new OwnershipUseCase\Author1(); $this->assertTrue($acl->isAllowed($author, 'comment', 'view')); $this->assertFalse($acl->isAllowed($author, 'comment', 'delete')); @@ -32,11 +32,11 @@ public function testAssertPassesIfResourceIsNotProprietary() public function testAssertPassesIfResourceDoesNotHaveOwner() { - $acl = new UseCase2\Acl(); + $acl = new OwnershipUseCase\Acl(); - $author = new UseCase2\Author1(); + $author = new OwnershipUseCase\Author1(); - $blogPost = new UseCase2\BlogPost(); + $blogPost = new OwnershipUseCase\BlogPost(); $blogPost->author = null; $this->assertTrue($acl->isAllowed($author, 'blogPost', 'write')); @@ -45,12 +45,12 @@ public function testAssertPassesIfResourceDoesNotHaveOwner() public function testAssertFailsIfResourceHasOwnerOtherThanRoleOwner() { - $acl = new UseCase2\Acl(); + $acl = new OwnershipUseCase\Acl(); - $author1 = new UseCase2\Author1(); - $author2 = new UseCase2\Author2(); + $author1 = new OwnershipUseCase\Author1(); + $author2 = new OwnershipUseCase\Author2(); - $blogPost = new UseCase2\BlogPost(); + $blogPost = new OwnershipUseCase\BlogPost(); $blogPost->author = $author1; $this->assertTrue($acl->isAllowed($author2, 'blogPost', 'write')); diff --git a/test/TestAsset/UseCase3/BlogPost.php b/test/TestAsset/ExpressionUseCase/BlogPost.php similarity index 93% rename from test/TestAsset/UseCase3/BlogPost.php rename to test/TestAsset/ExpressionUseCase/BlogPost.php index 5865eb0..70d31d5 100644 --- a/test/TestAsset/UseCase3/BlogPost.php +++ b/test/TestAsset/ExpressionUseCase/BlogPost.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase3; +namespace ZendTest\Permissions\Acl\TestAsset\ExpressionUseCase; use Zend\Permissions\Acl\Resource\ResourceInterface; diff --git a/test/TestAsset/UseCase3/User.php b/test/TestAsset/ExpressionUseCase/User.php similarity index 92% rename from test/TestAsset/UseCase3/User.php rename to test/TestAsset/ExpressionUseCase/User.php index 5998e73..afbaa19 100644 --- a/test/TestAsset/UseCase3/User.php +++ b/test/TestAsset/ExpressionUseCase/User.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase3; +namespace ZendTest\Permissions\Acl\TestAsset\ExpressionUseCase; use Zend\Permissions\Acl\Role\RoleInterface; diff --git a/test/TestAsset/UseCase2/Acl.php b/test/TestAsset/OwnershipUseCase/Acl.php similarity index 87% rename from test/TestAsset/UseCase2/Acl.php rename to test/TestAsset/OwnershipUseCase/Acl.php index 778e6f7..25f5128 100644 --- a/test/TestAsset/UseCase2/Acl.php +++ b/test/TestAsset/OwnershipUseCase/Acl.php @@ -5,12 +5,12 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; -use Zend\Permissions\Acl\Acl as ZendAcl; +use Zend\Permissions\Acl\Acl as BaseAcl; use Zend\Permissions\Acl\Assertion\OwnershipAssertion; -class Acl extends ZendAcl +class Acl extends BaseAcl { public function __construct() { diff --git a/test/TestAsset/UseCase2/Author1.php b/test/TestAsset/OwnershipUseCase/Author1.php similarity index 86% rename from test/TestAsset/UseCase2/Author1.php rename to test/TestAsset/OwnershipUseCase/Author1.php index 8488948..d0474c7 100644 --- a/test/TestAsset/UseCase2/Author1.php +++ b/test/TestAsset/OwnershipUseCase/Author1.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; class Author1 extends User { diff --git a/test/TestAsset/UseCase2/Author2.php b/test/TestAsset/OwnershipUseCase/Author2.php similarity index 86% rename from test/TestAsset/UseCase2/Author2.php rename to test/TestAsset/OwnershipUseCase/Author2.php index ec6fe7a..ff7265a 100644 --- a/test/TestAsset/UseCase2/Author2.php +++ b/test/TestAsset/OwnershipUseCase/Author2.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; class Author2 extends User { diff --git a/test/TestAsset/UseCase2/BlogPost.php b/test/TestAsset/OwnershipUseCase/BlogPost.php similarity index 92% rename from test/TestAsset/UseCase2/BlogPost.php rename to test/TestAsset/OwnershipUseCase/BlogPost.php index 539d06a..88faa12 100644 --- a/test/TestAsset/UseCase2/BlogPost.php +++ b/test/TestAsset/OwnershipUseCase/BlogPost.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; use Zend\Permissions\Acl\Resource\ResourceInterface; use Zend\Permissions\Acl\ProprietaryInterface; diff --git a/test/TestAsset/UseCase2/Comment.php b/test/TestAsset/OwnershipUseCase/Comment.php similarity index 88% rename from test/TestAsset/UseCase2/Comment.php rename to test/TestAsset/OwnershipUseCase/Comment.php index 5eff076..0c3c9c2 100644 --- a/test/TestAsset/UseCase2/Comment.php +++ b/test/TestAsset/OwnershipUseCase/Comment.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; use Zend\Permissions\Acl\Resource\ResourceInterface; diff --git a/test/TestAsset/UseCase2/User.php b/test/TestAsset/OwnershipUseCase/User.php similarity index 91% rename from test/TestAsset/UseCase2/User.php rename to test/TestAsset/OwnershipUseCase/User.php index b5c8707..f0c3d04 100644 --- a/test/TestAsset/UseCase2/User.php +++ b/test/TestAsset/OwnershipUseCase/User.php @@ -5,7 +5,7 @@ * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License */ -namespace ZendTest\Permissions\Acl\TestAsset\UseCase2; +namespace ZendTest\Permissions\Acl\TestAsset\OwnershipUseCase; use Zend\Permissions\Acl\ProprietaryInterface; use Zend\Permissions\Acl\Role\RoleInterface; diff --git a/test/TestAsset/StandardUseCase/Acl.php b/test/TestAsset/StandardUseCase/Acl.php new file mode 100644 index 0000000..30a54fb --- /dev/null +++ b/test/TestAsset/StandardUseCase/Acl.php @@ -0,0 +1,34 @@ +customAssertion = new UserIsBlogPostOwnerAssertion(); + + $this->addRole(new GenericRole('guest')); + $this->addRole(new GenericRole('contributor'), 'guest'); + $this->addRole(new GenericRole('publisher'), 'contributor'); + $this->addRole(new GenericRole('admin')); + + $this->addResource(new GenericResource('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/StandardUseCase/BlogPost.php b/test/TestAsset/StandardUseCase/BlogPost.php new file mode 100644 index 0000000..55f97f0 --- /dev/null +++ b/test/TestAsset/StandardUseCase/BlogPost.php @@ -0,0 +1,20 @@ +role; + } +} diff --git a/test/TestAsset/StandardUseCase/UserIsBlogPostOwnerAssertion.php b/test/TestAsset/StandardUseCase/UserIsBlogPostOwnerAssertion.php new file mode 100644 index 0000000..c4ed4cf --- /dev/null +++ b/test/TestAsset/StandardUseCase/UserIsBlogPostOwnerAssertion.php @@ -0,0 +1,33 @@ +lastAssertRole = $user; + $this->lastAssertResource = $blogPost; + $this->lastAssertPrivilege = $privilege; + return $this->assertReturnValue; + } +} diff --git a/test/TestAsset/UseCase1/Acl.php b/test/TestAsset/UseCase1/Acl.php deleted file mode 100644 index eb74ebe..0000000 --- a/test/TestAsset/UseCase1/Acl.php +++ /dev/null @@ -1,31 +0,0 @@ -customAssertion = new UserIsBlogPostOwnerAssertion(); - - $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('guest')); - $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('contributor'), 'guest'); - $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('publisher'), 'contributor'); - $this->addRole(new \Zend\Permissions\Acl\Role\GenericRole('admin')); - $this->addResource(new \Zend\Permissions\Acl\Resource\GenericResource('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 deleted file mode 100644 index 48975a4..0000000 --- a/test/TestAsset/UseCase1/BlogPost.php +++ /dev/null @@ -1,21 +0,0 @@ -role; - } -} diff --git a/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php b/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php deleted file mode 100644 index e31d5a6..0000000 --- a/test/TestAsset/UseCase1/UserIsBlogPostOwnerAssertion.php +++ /dev/null @@ -1,34 +0,0 @@ -lastAssertRole = $user; - $this->lastAssertResource = $blogPost; - $this->lastAssertPrivilege = $privilege; - return $this->assertReturnValue; - } -} From d8564f2050554c75fb18839d145992edbd3eb46d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 16:43:38 -0500 Subject: [PATCH 11/13] Adds CHANGELOG entry for #23 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ae56a..969529c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file, in reverse ### Added +- [#23](https://github.com/zendframework/zend-permissions-acl/pull/23) adds a new assertion, `ExpressionAssertion`, to allow programatically or + automatically (from configuration) building standard comparison assertions using a variety of operators, + including `=` (`==`), `!=`, `<`, `<=`, `>`, `>=`, `===`, `!==`, `in` (`in_array`), `!in` (`! in_array`), + `regex` (`preg_match`), and `!regex` (`! preg_match`). See https://docs.zendframework.com/zend-permissions-acl/expression/ + for details on usage. + - [#3](https://github.com/zendframework/zend-permissions-acl/pull/3) adds two new interfaces designed to allow creation of ownership-based assertions easier: - `Zend\Permissions\Acl\ProprietaryInterface` is applicable to both roles and resources, and provides the method `getOwnerId()` for retrieving the owner role of an object. - `Zend\Permissions\Acl\Assertion\OwnershipAssertion` ensures that the owner of a proprietary resource matches that of the role. From b956f7464be2399569bbf90cd41a193e95fa3125 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 16:52:29 -0500 Subject: [PATCH 12/13] Prepares CHANGELOG for 2.7.0 release --- CHANGELOG.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 969529c..33f6e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,29 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. -## 2.7.0 - TBD +## 2.7.0 - 2018-05-01 ### Added - [#23](https://github.com/zendframework/zend-permissions-acl/pull/23) adds a new assertion, `ExpressionAssertion`, to allow programatically or - automatically (from configuration) building standard comparison assertions using a variety of operators, - including `=` (`==`), `!=`, `<`, `<=`, `>`, `>=`, `===`, `!==`, `in` (`in_array`), `!in` (`! in_array`), - `regex` (`preg_match`), and `!regex` (`! preg_match`). See https://docs.zendframework.com/zend-permissions-acl/expression/ + automatically (from configuration) building standard comparison assertions + using a variety of operators, including `=` (`==`), `!=`, `<`, `<=`, `>`, + `>=`, `===`, `!==`, `in` (`in_array`), `!in` (`! in_array`), `regex` + (`preg_match`), and `!regex` (`! preg_match`). See https://docs.zendframework.com/zend-permissions-acl/expression/ for details on usage. -- [#3](https://github.com/zendframework/zend-permissions-acl/pull/3) adds two new interfaces designed to allow creation of ownership-based assertions easier: - - `Zend\Permissions\Acl\ProprietaryInterface` is applicable to both roles and resources, and provides the method `getOwnerId()` for retrieving the owner role of an object. - - `Zend\Permissions\Acl\Assertion\OwnershipAssertion` ensures that the owner of a proprietary resource matches that of the role. +- [#3](https://github.com/zendframework/zend-permissions-acl/pull/3) adds two new interfaces designed to allow creation of ownership-based assertions + easier: + + - `Zend\Permissions\Acl\ProprietaryInterface` is applicable to both roles and + resources, and provides the method `getOwnerId()` for retrieving the owner + role of an object. + + - `Zend\Permissions\Acl\Assertion\OwnershipAssertion` ensures that the owner + of a proprietary resource matches that of the role. + + See https://docs.zendframework.com/zend-permissions-acl/ownership/ for details + on usage. ### Changed From e10a01cea2bcb62b444fb02ee01ca9617b4891af Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 1 May 2018 16:52:49 -0500 Subject: [PATCH 13/13] Updates branch aliases - dev-master => 2.7.x-dev - dev-develop => 2.8.x-dev --- composer.json | 4 ++-- composer.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 0b5f755..fc40fde 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,8 @@ }, "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "scripts": { diff --git a/composer.lock b/composer.lock index 72c662c..2e54989 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c4a7f676cf69c2596abe9f68386c51ac", + "content-hash": "ee45bb6b33c3a5cdd93c74705598236d", "packages": [], "packages-dev": [ {