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

Add route permissions guard and controller permissions guard #238

Merged
merged 20 commits into from
Jun 19, 2014
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions docs/04. Guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ And here is a simple workflow with a route guard:

![Zend Framework workflow with guards](/docs/images/workflow-with-guards.png?raw=true)

Guards are not really aware of permissions (it does not make any sense) but rather only think about "roles". For
RouteGuard and ControllerGuard are not aware of permissions but rather only think about "roles". For
instance, you may want to refuse access to each routes that begin by "admin/*" to all users that do not have the
"admin" role.

If you want to protect a route for a set of permissions, you must use RoutePermissionsGuard. For instance,
you may want to grant access to a route "post/delete" only to roles having the "delete" permission.
Note that in a RBAC system, a permission is linked to a role, not to a user.

Albeit simple to use, guards should not be the only protection in your application, and you should always also
protect your service. The reason is that your business logic should be handled by your service. Protecting a given
route or controller does not mean that the service cannot be access from elsewhere (another action for instance).
Expand Down Expand Up @@ -57,7 +61,7 @@ deny policy is much more secure, but it needs much more configuration to work wi

## Built-in guards

ZfcRbac comes with two guards: RouteGuard and ControllerGuard. All guards must be added in the `guards` subkey:
ZfcRbac comes with three guards: RouteGuard, RoutePermissionsGuard and ControllerGuard. All guards must be added in the `guards` subkey:

```php
return [
Expand Down Expand Up @@ -104,7 +108,7 @@ route to users that have the "guest" role (eg.: most likely unauthenticated user

> The route pattern is not a regex. It only supports the wildcard (*) character, that replaces any segment.

You can also use the wildcard character for roles:
You can also use the wildcard character * for roles:

```php
return [
Expand Down Expand Up @@ -151,6 +155,69 @@ return [
```


### RoutePermissionsGuard

> The RoutePermissionsGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -10.

The RoutePermissionsGuard allows to protect a route or a hierarchy of route. You must provide an array of "key" => "value",
where the key is a route pattern, and value an array of permissions names:

```php
return [
'zfc_rbac' => [
'guards' => [
'ZfcRbac\Guard\RoutePermissionsGuard' => [
'admin*' => ['admin'],
'post/manage' => ['post.update', 'post.delete']
]
]
]
];
```

All permissions in a rule must be matched (it is an AND condition). In the previous example, one must have
```post.update``` **AND** ```post.delete``` permissions to access the ```post/manage``` route.

> Permissions are linked to roles, not to users

Those rules grant access to all admin routes to roles that have the "admin" permission, and grant access to the
Copy link
Member

Choose a reason for hiding this comment

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

I honestly find it confusing. "admin" is typically not a "permission", it's just a role. Typical permissions that admin may have is something like "post.delete_others". We already had this discussion and we came to the conclusion that this is the cleanest way to solve this problem. Then the admin have both "post.delete" and "post.delete_others" permissions while normal users only have "post.delete".

"post/delete" route to roles that have the "post.delete" or "admin" permissions.

> The route pattern is not a regex. It only supports the wildcard (*) character, that replaces any segment.

You can also use the wildcard character * for permissions:

```php
return [
'zfc_rbac' => [
'guards' => [
'ZfcRbac\Guard\RoutePermissionsGuard' => [
'home' => ['*']
]
]
]
];
```

This rule grants access to the "home" route to anyone.

Finally, you can also use an empty array to completly block a route, for maintenance purpose for example :

```php
return [
'zfc_rbac' => [
'guards' => [
'ZfcRbac\Guard\RoutePermissionsGuard' => [
'route_under_construction' => []
]
]
]
];
```

This rule will be inaccessible.


### ControllerGuard

The ControllerGuard allows to protect a controller. You must provide an array of array:
Expand Down
67 changes: 67 additions & 0 deletions src/ZfcRbac/Factory/ControllerPermissionsGuardFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?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\Factory;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use ZfcRbac\Guard\ControllerPermissionsGuard;
use ZfcRbac\Guard\RouteGuard;

/**
* Create a controller guard for checking permissions
*
* @author JM Lerouxw <[email protected]>
* @licence MIT
*/
class ControllerPermissionsGuardFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* @var array
*/
protected $options = [];

/**
* {@inheritDoc}
*/
public function setCreationOptions(array $options)
{
$this->options = $options;
}

/**
* @param \Zend\ServiceManager\AbstractPluginManager|ServiceLocatorInterface $serviceLocator
* @return RouteGuard
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();

/* @var \ZfcRbac\Options\ModuleOptions $moduleOptions */
$moduleOptions = $parentLocator->get('ZfcRbac\Options\ModuleOptions');

/* @var \ZfcRbac\Service\AuthorizationService $authorizationService */
$authorizationService = $parentLocator->get('ZfcRbac\Service\AuthorizationService');

$guard = new ControllerPermissionsGuard($authorizationService, $this->options);
$guard->setProtectionPolicy($moduleOptions->getProtectionPolicy());

return $guard;
}
}
68 changes: 68 additions & 0 deletions src/ZfcRbac/Factory/RoutePermissionsGuardFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?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\Factory;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use ZfcRbac\Guard\RouteGuard;
use ZfcRbac\Guard\RoutePermissionsGuard;

/**
* Create a route guard for checking permissions
*
* @author Michaël Gallego <[email protected]>
* @author JM Lerouxw <[email protected]>
* @licence MIT
*/
class RoutePermissionsGuardFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* @var array
*/
protected $options = [];

/**
* {@inheritDoc}
*/
public function setCreationOptions(array $options)
{
$this->options = $options;
}

/**
* @param \Zend\ServiceManager\AbstractPluginManager|ServiceLocatorInterface $serviceLocator
* @return RouteGuard
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();

/* @var \ZfcRbac\Options\ModuleOptions $moduleOptions */
$moduleOptions = $parentLocator->get('ZfcRbac\Options\ModuleOptions');

/* @var \ZfcRbac\Service\AuthorizationService $authorizationService */
$authorizationService = $parentLocator->get('ZfcRbac\Service\AuthorizationService');

$routeGuard = new RoutePermissionsGuard($authorizationService, $this->options);
$routeGuard->setProtectionPolicy($moduleOptions->getProtectionPolicy());

return $routeGuard;
}
}
141 changes: 141 additions & 0 deletions src/ZfcRbac/Guard/ControllerPermissionsGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
Copy link
Member

Choose a reason for hiding this comment

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

Why is the whole file marked as changed? Very hard to know what has changed :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

'cause it's a new one 😉

ControllerPermissionsGuard

/*
* 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\Guard;

use Zend\Mvc\MvcEvent;
use ZfcRbac\Service\AuthorizationServiceInterface;

/**
* A controller guard can protect a controller and a set of actions
*
* @author Michaël Gallego <[email protected]>
* @author JM Leroux <[email protected]>
* @licence MIT
*/
class ControllerPermissionsGuard extends AbstractGuard
{
use ProtectionPolicyTrait;

/**
* Event priority
*/
const EVENT_PRIORITY = -15;

/**
* @var AuthorizationServiceInterface
*/
protected $authorizationService;

/**
* Controller guard rules
*
* @var array
*/
protected $rules = [];

/**
* Constructor
*
* @param AuthorizationServiceInterface $authorizationService
* @param array $rules
*/
public function __construct(AuthorizationServiceInterface $authorizationService, array $rules = [])
{
$this->authorizationService = $authorizationService;
$this->setRules($rules);
}

/**
* Set the rules
*
* A controller rule is made the following way:
*
* [
* 'controller' => 'ControllerName',
* 'actions' => []/string
* 'roles' => []/string
* ]
*
* @param array $rules
* @return void
*/
public function setRules(array $rules)
{
$this->rules = [];

foreach ($rules as $rule) {
$controller = strtolower($rule['controller']);
$actions = isset($rule['actions']) ? (array) $rule['actions'] : [];
Copy link
Member

Choose a reason for hiding this comment

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

Fix alignment of "="

$permissions = (array) $rule['permissions'];

if (empty($actions)) {
$this->rules[$controller][0] = $permissions;
continue;
}

foreach ($actions as $action) {
$this->rules[$controller][strtolower($action)] = $permissions;
}
}
}

/**
* {@inheritDoc}
*/
public function isGranted(MvcEvent $event)
{
$routeMatch = $event->getRouteMatch();
$controller = strtolower($routeMatch->getParam('controller'));
$action = strtolower($routeMatch->getParam('action'));

// If no rules apply, it is considered as granted or not based on the protection policy
if (!isset($this->rules[$controller])) {
return $this->protectionPolicy === self::POLICY_ALLOW;
}

// Algorithm is as follow: we first check if there is an exact match (controller + action), if not
// we check if there are rules set globally for the whole controllers (see the index "0"), and finally
// if nothing is matched, we fallback to the protection policy logic

if (isset($this->rules[$controller][$action])) {
$allowedPermissions = $this->rules[$controller][$action];
} elseif (isset($this->rules[$controller][0])) {
$allowedPermissions = $this->rules[$controller][0];
} else {
return $this->protectionPolicy === self::POLICY_ALLOW;
}

// If no rules apply, it is considered as granted or not based on the protection policy
if (empty($allowedPermissions)) {
return $this->protectionPolicy === self::POLICY_ALLOW;
}

if (in_array('*', $allowedPermissions)) {
return true;
}

foreach ($allowedPermissions as $permission) {
if (!$this->authorizationService->isGranted($permission)) {
return false;
}
}

return true;
}
}
Loading