Skip to content
This repository has been archived by the owner on Jul 3, 2020. It is now read-only.

Added ability to set multiple assertions and their condition for permissions #320

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
124 changes: 124 additions & 0 deletions docs/06. Using the Authorization Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,130 @@ return [

Now, every time you check for `myPermission`, `myAssertion` will be checked as well.



### Multiple assertions

The assertion map also accepts multiple assertions as a simple array:

```php
return [
'zfc_rbac' => [
'assertion_map' => [
'myPermission' => 'myAssertion', // single assertion
'myPermission2' => [ // multiple assertions
'myAssertion',
'myAssertion2'
]
]
]
];
```

Or with an additional condition definition:

```php
return [
'zfc_rbac' => [
'assertion_map' => [
// single assertion
'myPermission' => 'myAssertion',
'myPermission2' => [
// multiple assertions
'assertions' => [
'myAssertion',
'myAssertion2'
],
// condition
'condition' => \ZfcRbac\Assertion\AssertionSet::CONDITION_AND
]
]
]
];
```

If 'AND' condition is specified (this is default) all of the assertions must pass the check.
If 'OR' condition is specified at least one of the assertions must pass the check.
This in the background will create an instance of ZfcRbac\Assertion\AssertionSet and adds the given assertions to it.

### Assertion Set

ZfcRbac\Assertion\AssertionSet class is basically a container for multiple assertions as well as assertion condition.
An instance of the class get's actually created automatically when you specify multiple assertions (see above)
in the background, but you can also create your own instance containing your custom assertions
and specify that in assertion map instead.

So you can create a factory for your assertion set like this for example:

```php

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use ZfcRbac\Assertion\AssertionSet;

class MyAssertionSetFactory implements FactoryInterface
{
/**
* {@inheritDoc}
*
* @return AssertionSet
*/
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$assertionManager = $container->get('ZfcRbac\Assertion\AssertionPluginManager');
$assertion1 = $assertionManager->get('myAssertion1');
$assertion2 = $assertionManager->get('myAssertion2');

// create instance, set condition and add assertions
$assertionSet = new AssertionSet([
'assertions' => [$assertion1, $assertion2],
'condition' => AssertionSet::CONDITION_OR
]);
return $assertionSet;
}

/**
* {@inheritDoc}
*
* For use with zend-servicemanager v2; proxies to __invoke().
*
* @param ServiceLocatorInterface $container
* @return \ZfcRbac\Assertion\AssertionSet
*/
public function createService(ServiceLocatorInterface $container)
{
// Retrieve the parent container when under zend-servicemanager v2
if (method_exists($container, 'getServiceLocator')) {
$container = $container->getServiceLocator() ?: $container;
}

return $this($container, AssertionSet::class);
}
}

```

And then add it to assertion manager and assertion map config:

```php
return [
'zfc_rbac' => [
'assertion_manager' => [
'factories' => [
'myAssertionSet' => MyAssertionSetFactory::class
]
],

'assertion_map' => [
'myPermission' => 'myAssertion', // single assertion
'myPermission2' => 'myAssertionSet' // multiple assertions in set
]
]
];
```


### Checking permissions in a service

So let's check for a permission, shall we?
Expand Down
113 changes: 113 additions & 0 deletions src/ZfcRbac/Assertion/AssertionSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license.
*/
namespace ZfcRbac\Assertion;

use ZfcRbac\Exception\InvalidArgumentException;
use ZfcRbac\Service\AuthorizationService;

/**
* Assertion set to hold and process multiple assertions
*
* @author David Havl
* @licence MIT
*/
class AssertionSet implements AssertionInterface
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's mark final

{
/**
* Condition constants
*/
const CONDITION_OR = 'OR';
const CONDITION_AND = 'AND';

/**
* @var $assertions array
*/
protected $assertions = [];

/**
* @var $condition string
*/
protected $condition = AssertionSet::CONDITION_AND;

/**
* Constructor.
*
* @param array|AssertionInterface[] $assertions An array of assertions.
*/
public function __construct(array $assertions = array())
{
$this->assertions = [];

// if definition contains condition, set it.
if (isset($assertions['condition'])) {
if ($assertions['condition'] != AssertionSet::CONDITION_AND
&& $assertions['condition'] != AssertionSet::CONDITION_OR) {
throw new InvalidArgumentException('Invalid assertion condition given.');
}
$this->condition = $assertions['condition'];
}
// if there are multiple assertions under a key 'assertions', get them.
if (isset($assertions['assertions']) && is_array($assertions['assertions'])) {
$assertions = $assertions['assertions'];
}
// set each assertion
foreach ($assertions as $name => $assertion) {
if (is_int($name)) {
$this->assertions[] = $assertion;
} else {
$this->assertions[$name] = $assertion;
}
}
}

/**
* Check if assertions are successful
*
* @param AuthorizationService $authorizationService
* @param mixed $context
* @return bool
*/
public function assert(AuthorizationService $authorizationService, $context = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe not logical but it's possible to have zero assertions here then maybe return true by default or throw exception?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty assertion is not logical indeed and should either return FALSE or an exception.

Better to have defensive defaults.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Thanx.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
if (empty($this->assertions)) {
return true;
}

if (AssertionSet::CONDITION_AND === $this->condition) {
foreach ($this->assertions as $assertion) {
if (!$assertion->assert($authorizationService, $context)) {
return false;
}
}

return true;
}

if (AssertionSet::CONDITION_OR === $this->condition) {
foreach ($this->assertions as $assertion) {
if ($assertion->assert($authorizationService, $context)) {
return true;
}
}

return false;
}

return false;
}
}
28 changes: 23 additions & 5 deletions src/ZfcRbac/Service/AuthorizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Rbac\Permission\PermissionInterface;
use ZfcRbac\Assertion\AssertionPluginManager;
use ZfcRbac\Assertion\AssertionInterface;
use ZfcRbac\Assertion\AssertionSet;
use ZfcRbac\Exception;
use ZfcRbac\Identity\IdentityInterface;

Expand Down Expand Up @@ -72,7 +73,7 @@ public function __construct(Rbac $rbac, RoleService $roleService, AssertionPlugi
* Set an assertion
*
* @param string|PermissionInterface $permission
* @param string|callable|AssertionInterface $assertion
* @param string|callable|array|AssertionInterface $assertion
* @return void
*/
public function setAssertion($permission, $assertion)
Expand Down Expand Up @@ -139,8 +140,8 @@ public function isGranted($permission, $context = null)
}

/**
* @param string|callable|AssertionInterface $assertion
* @param mixed $context
* @param string|callable|array|AssertionInterface $assertion
* @param mixed $context
* @return bool
* @throws Exception\InvalidArgumentException If an invalid assertion is passed
*/
Expand All @@ -150,9 +151,26 @@ protected function assert($assertion, $context = null)
return $assertion($this, $context);
} elseif ($assertion instanceof AssertionInterface) {
return $assertion->assert($this, $context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now back, you are right, it makes it more performant to have the retrieval only when it's called/needed.

} elseif (is_string($assertion)) {
} elseif (is_string($assertion)) { // retrieve an actual instance from assertion plugin manager
$assertion = $this->assertionPluginManager->get($assertion);

return $assertion->assert($this, $context);
} elseif (is_array($assertion)) { // else if multiple assertion definition, create assertion set.
// move assertion definition under a key 'assertions'.
if (!isset($assertion['assertions'])) {
$assertion['assertions'] = (array)$assertion;
}
// convert to an array
if (!is_array($assertion['assertions'])) {
$assertion['assertions'] = (array)$assertion['assertions'];
}
// retrieve an actual instance from assertion plugin manager if necessary
foreach ($assertion['assertions'] as $key => $value) {
if (is_string($value)) {
$assertion['assertions'][$key] = $this->assertionPluginManager->get($value);
}
}
// create assertion set
$assertion = new AssertionSet($assertion);
return $assertion->assert($this, $context);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add this:

elseif (is_array($assertion)) {
    $assertionSet = new AssertionSet($assertion, $this->assertionPluginManager);
    return $assertionSet->assert($this, $context);
}

see my above comments why


Expand Down