diff --git a/composer.json b/composer.json index 79f3fea8..dc11c5eb 100644 --- a/composer.json +++ b/composer.json @@ -37,14 +37,15 @@ "require": { "php": "^7.2", "laminas/laminas-servicemanager": "^3.3", - "laminas/laminas-stdlib": "^3.1" + "laminas/laminas-stdlib": "^3.1", + "doctrine/persistence": "^2.0" }, "require-dev": { "malukenho/docheader": "^0.1.7", "phpunit/phpunit": "^8.5.2", "phpspec/prophecy": "^1.10", "friendsofphp/php-cs-fixer": "^2.9.3", - "doctrine/common": "^2.4", + "doctrine/doctrine-orm-module": "^3.1", "satooshi/php-coveralls": "^2.0" }, "autoload": { diff --git a/docs/01. Introduction.md b/docs/01. Introduction.md deleted file mode 100644 index 97f00093..00000000 --- a/docs/01. Introduction.md +++ /dev/null @@ -1,59 +0,0 @@ -# Introduction - -Welcome to the official documentation of LmcRbac! - -In this part, the following questions will be answered: - -* Why should I use an authorization module? -* What is the Rbac model? -* How can I integrate LmcRbac into my application? - -## Why should I use an authorization module? - -The authorization part of an application is an essential aspect to secure your application. While the authentication -part tells you who is using your website, the authorization answers if the given identity has the permission to -perform specific actions. - -## What is the Rbac model? - -Rbac stands for **role-based access control**. We use a very simple (albeit powerful) implementation of this model -through the use of [this PHP library](https://github.com/zf-fr/rbac). - -> We are not using the official ZF2 Rbac component since ZfcRbac 2.0 as it has some design flaws. The library we are -using here is actually a prototype for ZF3 Rbac component I've made specifically for ZfcRbac. - -The basic idea of Rbac is to use roles and permissions: - -* **Users** can have one or many **Roles** -* **Roles** request access to **Permissions** -* **Permissions** are granted to **Roles** - -By default, LmcRbac can be used for two kinds of Rbac model: - -* Flat RBAC model: in this model, roles cannot have children. This is ideal for smaller application, as it is easier -to understand, and the database design is simpler (no need for a join table). -* Hierarchical RBAC model: in this model, roles can have children roles. When evaluating if a given role has a -permission, this model also checks recursively if any of its child roles also have the permission. - - -## How can I integrate LmcRbac into my application? - -LmcRbac offers multiple ways to protect your application: - -* Using **Guards**: those classes act as "firewalls" that block access to routes and/or controllers. Guards are usually - configured using PHP arrays, and are executed early in the MVC dispatch process. Typically this happens right after - the route has been matched. -* Using **AuthorizationService**: a complementary method is to use the `AuthorizationService` and inject them into your - service classes to protect them from unwanted access. - -While it is advised to use both methods to make your application even more secure, this is completely optional and you -can choose either of them independently. - -To find out about how you can easily make your existing application more secure, please refer to the following section: - -* [Cookbook: A real world example](/docs/07. Cookbook.md#a-real-world-application) - -### Navigation - -* Continue to [the **Quick Start**](/docs/02. Quick Start.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/02. Quick Start.md b/docs/02. Quick Start.md deleted file mode 100644 index 545b970d..00000000 --- a/docs/02. Quick Start.md +++ /dev/null @@ -1,136 +0,0 @@ -# Quick Start - -In this section, you will learn: - -* How to setup the module -* How to specify an identity provider -* How to add simple role provider - -Before starting the quick start, make sure you have properly installed the module by following the instructions in -the README file. - -## Specifying an identity provider - -By default, LmcRbac internally uses the `Laminas\Authentication\AuthenticationService` service key to retrieve the user (logged or -not). Therefore, you must implement and register this service in your application by adding those lines in your `module.config.php` file: - -```php -return [ - 'service_manager' => [ - 'factories' => [ - 'Laminas\Authentication\AuthenticationService' => function($sm) { - // Create your authentication service! - } - ] - ] -]; -``` -The identity given by `Laminas\Authentication\AuthenticationService` must implement `LmcRbac\Identity\IdentityInterface`. Note that the default identity provided with ZF2 does not implement this interface, neither does the ZfcUser suite. - -LmcRbac is flexible enough to use something else than the built-in `AuthenticationService`, by specifying custom -identity providers. For more information, refer [to this section](/docs/03. Role providers.md#identity-providers). - -## Adding a guard - -A guard allows to block access to routes and/or controllers using a simple syntax. For instance, this configuration -grants access to any route that begins with `admin` (or is exactly `admin`) to the `admin` role only: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'admin*' => ['admin'] - ] - ] - ] -]; -``` - -LmcRbac have several built-in guards, and you can also register your own guards. For more information, refer -[to this section](/docs/04. Guards.md#built-in-guards). - -## Adding a role provider - -RBAC model is based on roles. Therefore, for LmcRbac to work properly, it must be aware of all the roles that are -used inside your application. - -This configuration creates an *admin* role that has a children role called *member*. The *admin* role automatically -inherits the *member* permissions. - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - \LmcRbac\Role\InMemoryRoleProvider::class => [ - 'admin' => [ - 'children' => ['member'], - 'permissions' => ['delete'] - ], - 'member' => [ - 'permissions' => ['edit'] - ] - ] - ] - ] -]; -``` - -In this example, the *admin* role have two permissions: `delete` and `edit` (because it inherits the permissions from -its child), while the *member* role only has the permission `edit`. - -LmcRbac have several built-in role providers, and you can also register your own role providers. For more information, -refer [to this section](/docs/03. Role providers.md#built-in-role-providers). - -## Registering a strategy - -When a guard blocks access to a route/controller, or if you throw the `LmcRbac\Exception\UnauthorizedException` -exception in your service, LmcRbac automatically performs some logic for you depending on the view strategy used. - -For instance, if you want LmcRbac to automatically redirect all unauthorized requests to the "login" route, add -the following code in the `onBootstrap` method of your `Module.php` class: - -```php -public function onBootstrap(EventInterface $e) -{ - $t = $e->getTarget(); - - $t->getEventManager()->attach( - $t->getServiceManager()->get('LmcRbac\View\Strategy\RedirectStrategy') - ); -} -``` - -By default, `RedirectStrategy` redirects all unauthorized requests to a route named "login" when user is not connected -and to a route named "home" when user is connected. This is, of course, entirely configurable. - -> For flexibility purpose, LmcRbac **does not** register any strategy for you by default! - -For more information about built-in strategies, refer [to this section](/docs/05. Strategies.md#built-in-strategies). - -## Using the authorization service - -Now that LmcRbac is properly configured, you can inject the authorization service in any class and use it to check -if the current identity is granted to do something. - -The authorization service is registered inside the service manager using the following key: `LmcRbac\Service\AuthorizationService`. -Once injected, you can use it as follow: - -```php -use LmcRbac\Exception\UnauthorizedException; - -public function delete() -{ - if (!$this->authorizationService->isGranted('delete')) { - throw new UnauthorizedException(); - } - - // Delete the post -} -``` - -### Navigation - -* Continue to [the **Role providers**](/docs/03. Role providers.md) -* Back to [the Introduction](/docs/01. Introduction.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/03. Role providers.md b/docs/03. Role providers.md deleted file mode 100644 index 833682c4..00000000 --- a/docs/03. Role providers.md +++ /dev/null @@ -1,200 +0,0 @@ -# Role providers - -In this section, you will learn: - -* What are role providers -* What are identity providers -* How to use and configure built-in providers -* How to create custom role providers - -## What are role providers? - -A role provider is an object that returns a list of roles. Each role provider must implement the -`LmcRbac\Role\RoleProviderInterface` interface. The only required method is `getRoles`, and must return an array -of `Rbac\Role\RoleInterface` objects. - -Roles can come from any sources: in memory, from a file, from a database... However, please note that since LmcRbac -2.0, you can specify only one role provider per application. The reason is that having multiple role providers make -the workflow harder and can lead to security problems that are very hard to spot. - -## Identity providers? - -Identity providers return the current identity. Most of the time, this means the logged user. LmcRbac comes with a -default identity provider (`LmcRbac\Identity\AuthenticationIdentityProvider`) that uses the -`Laminas\Authentication\AuthenticationService` service. - -### Create your own identity provider - -If you want to implement your own identity provider, create a new class that implements -`LmcRbac\Identity\IdentityProviderInterface` class. Then, change the `identity_provider` option in LmcRbac config, -as shown below: - -```php -return [ - 'lmc_rbac' => [ - 'identity_provider' => 'MyCustomIdentityProvider' - ] -]; -``` - -The identity provider is automatically pulled from the service manager. - -## Built-in role providers - -LmcRbac comes with two built-in role providers: `InMemoryRoleProvider` and `ObjectRepositoryRoleProvider`. A role -provider must be added to the `role_provider` subkey: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - // Role provider config here! - ] - ] -]; -``` - -### InMemoryRoleProvider - -This provider is ideal for small/medium sites with few roles/permissions. All the data is specified in a simple -PHP file, so you never hit a database. - -Here is an example of the format you need to use: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - \LmcRbac\Role\InMemoryRoleProvider::class => [ - 'admin' => [ - 'children' => ['member'], - 'permissions' => ['article.delete'] - ], - 'member' => [ - 'children' => ['guest'], - 'permissions' => ['article.edit', 'article.archive'] - ], - 'guest' => [ - 'permissions' => ['article.read'] - ] - ] - ] - ] -]; -``` - -The `children` and `permissions` subkeys are entirely optionals. Internally, the `InMemoryRoleProvider` creates -either a `Rbac\Role\Role` object if the role does not have any children, or a `Rbac\Role\HierarchicalRole` if -the role has at least one child. - -If you are more confident with flat RBAC, the previous config can be re-written to remove any inheritence between roles: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - \LmcRbac\Role\InMemoryRoleProvider::class => [ - 'admin' => [ - 'permissions' => [ - 'article.delete', - 'article.edit', - 'article.archive', - 'article.read' - ] - ], - 'member' => [ - 'permissions' => [ - 'article.edit', - 'article.archive', - 'article.read' - ] - ], - 'guest' => [ - 'permissions' => ['article.read'] - ] - ] - ] - ] -]; -``` - -### ObjectRepositoryRoleProvider - -This provider fetches roles from the database using `Doctrine\Common\Persistence\ObjectRepository` interface. - -You can configure this provider by giving an object repository service name that is fetched from the service manager -using the `object_repository` key: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - \LmcRbac\Role\ObjectRepositoryRoleProvider::class => [ - 'object_repository' => 'App\Repository\RoleRepository', - 'role_name_property' => 'name' - ] - ] - ] -]; -``` - -Or you can specify the `object_manager` and `class_name` options: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - \LmcRbac\Role\ObjectRepositoryRoleProvider::class => [ - 'object_manager' => 'doctrine.entitymanager.orm_default', - 'class_name' => 'App\Entity\Role', - 'role_name_property' => 'name' - ] - ] - ] -]; -``` - -In both cases, you need to specify the `role_name_property` value, which is the name of the entity's property -that holds the actual role name. This is used internally to only load the identity roles, instead of loading -the whole table every time. - -Please note that your entity fetched from the table MUST implement the `Rbac\Role\RoleInterface` interface. - -## Creating custom role providers - -To create a custom role providers, you first need to create a class that implements the `LmcRbac\Role\RoleProviderInterface` -interface. - -Then, you need to add it to the role provider manager: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider_manager' => [ - 'factories' => [ - 'Application\Role\CustomRoleProvider' => 'Application\Factory\CustomRoleProviderFactory' - ] - ] - ] -]; -``` - -You can now use it like any other role provider: - -```php -return [ - 'lmc_rbac' => [ - 'role_provider' => [ - 'Application\Role\CustomRoleProvider' => [ - // Options - ] - ] - ] -]; -``` - -### Navigation - -* Continue to [the **Guards**](/docs/04. Guards.md) -* Back to [the Quick Start](/docs/02. Quick Start.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/04. Guards.md b/docs/04. Guards.md deleted file mode 100644 index 602bd133..00000000 --- a/docs/04. Guards.md +++ /dev/null @@ -1,496 +0,0 @@ -# Guards - -In this section, you will learn: - -* What are guards -* How to use and configure built-in guards -* How to create custom guards - -## What are guards and when to use them? - -Guards (called firewalls in older versions of LmcRbac) are listeners that are registered on a specific event of -the MVC workflow. They allow to quickly unauthorized requests. - -Here is a simple workflow without guards: - -![Laminas workflow without guards](/docs/images/workflow-without-guards.png?raw=true) - -And here is a simple workflow with a route guard: - -![Laminas workflow with guards](/docs/images/workflow-with-guards.png?raw=true) - -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). - -### Protection policy - -By default, when a guard is added, it will perform check only on the specified guard rules. Any route or controller -that are not specified in the rules will be "granted" by default. Therefore, the default is a "blacklist" -mechanism. - -However, you may want a more restrictive approach (also called "whitelist"). In this mode, once a guard is added, -anything that is not explicitely added will be refused by default. - -For instance, let's say you have two routes: "index" and "login". If you specify a route guard rule to allow "index" -route to "member" role, your "login" route will become defacto unauthorized to anyone, unless you add a new rule for -allowing the route "login" to "member" role. - -You can change it in LmcRbac config, as follows: - -```php -use LmcRbac\Guard\GuardInterface; - -return [ - 'lmc_rbac' => [ - 'protection_policy' => GuardInterface::POLICY_DENY - ] -]; -``` - -> NOTE: this policy will block ANY route/controller (so it will also block any console routes or controllers). The -deny policy is much more secure, but it needs much more configuration to work with. - -## Built-in guards - -LmcRbac comes with four guards, in order of priority : - -* RouteGuard : protect a set of routes based on the identity roles -* RoutePermissionsGuard : protect a set of routes based on roles permissions -* ControllerGuard : protect a controllers and/or actions based on the identity roles -* ControllerPermissionsGuard : protect a controllers and/or actions based on roles permissions - -All guards must be added in the `guards` subkey: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - // Guards config here! - ] - ] -]; -``` - -Because of the way Laminas handles config, you can without problem define some rules in one module, and -more rules in another module. All the rules will be automatically merged. - -> For your mental health, I recommend you to use either the route guard OR the controller guard, but not both. If -you decide to use both conjointly, I recommend you to set the protection policy to "allow" (otherwise, you will -need to define rules for every routes AND every controller, which can become quite frustrating!). - -Please note that if your application use both route and controller guards, route guards are always executed -**before** controller guards (they have a higher priority). - -### RouteGuard - -> The RouteGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -5. - -The RouteGuard 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 role names: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'admin*' => ['admin'], - 'login' => ['guest'] - ] - ] - ] -]; -``` - -> Only one role in a rule need to be matched (it is an OR condition). - -Those rules grant access to all admin routes to users that have the "admin" role, and grant access to the "login" -route to users that have the "guest" role (eg.: most likely unauthenticated users). - -> 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: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'home' => ['*'] - ] - ] - ] -]; -``` - -This rule grants access to the "home" route to anyone. - -Finally, you can also omit the roles array to completly block a route, for maintenance purpose for example : - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'route_under_construction' - ] - ] - ] -]; -``` - -This rule will be inaccessible. - -Note : this last example could be (and should be) written in a more explicit way : - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'route_under_construction' => [] - ] - ] - ] -]; -``` - - -### RoutePermissionsGuard - -> The RoutePermissionsGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -8. - -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 [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RoutePermissionsGuard' => [ - 'admin*' => ['admin'], - 'post/manage' => ['post.update', 'post.delete'] - ] - ] - ] -]; -``` - -> By default, all permissions in a rule must be matched (an AND condition). - -In the previous example, one must have ```post.update``` **AND** ```post.delete``` permissions -to access the ```post/manage``` route. You can also specify an OR condition like so: - -```php -use LmcRbac\Guard\GuardInterface; - -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RoutePermissionsGuard' => [ - 'post/manage' => [ - 'permissions' => ['post.update', 'post.delete'], - 'condition' => GuardInterface::CONDITION_OR - ] - ] - ] - ] -]; -``` - -> 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 -"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 [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\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 [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RoutePermissionsGuard' => [ - 'route_under_construction' => [] - ] - ] - ] -]; -``` - -This rule will be inaccessible. - - -### ControllerGuard - -> The ControllerGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -10. - -The ControllerGuard allows to protect a controller. You must provide an array of array: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\ControllerGuard' => [ - [ - 'controller' => 'MyController', - 'roles' => ['guest', 'member'] - ] - ] - ] - ] -]; -``` - -> Only one role in a rule need to be matched (it is an OR condition). - -Those rules grant access to each actions of the MyController controller to users that have either the "guest" or -"member" roles. - -As for RouteGuard, you can use a wildcard (*) character for roles. - -You can also specify optional actions, so that the rule only apply to one or several actions: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\ControllerGuard' => [ - [ - 'controller' => 'MyController', - 'actions' => ['read', 'edit'], - 'roles' => ['guest', 'member'] - ] - ] - ] - ] -]; -``` - -You can combine a generic rule and a specific action rule for the same controller, as follow: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\ControllerGuard' => [ - [ - 'controller' => 'PostController', - 'roles' => ['member'] - ], - [ - 'controller' => 'PostController', - 'actions' => ['delete'], - 'roles' => ['admin'] - ] - ] - ] - ] -]; -``` - -Those rules grant access to each actions of the controller to users that have the "member" role, but restrict the -"delete" action to "admin" only. - -### ControllerPermissionsGuard - -> The ControllerPermissionsGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -13. - -The ControllerPermissionsGuard allows to protect a controller using permissions. You must provide an array of array: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\ControllerPermissionsGuard' => [ - [ - 'controller' => 'MyController', - 'permissions' => ['post.update', 'post.delete'] - ] - ] - ] - ] -]; -``` - -> All permissions in a rule must be matched (it is an AND condition). - -In the previous example, the user must have ```post.update``` **AND** ```post.delete``` permissions -to access each actions of the MyController controller. - -As for all other guards, you can use a wildcard (*) character for permissions. - -The configuration rules are the same as for ControllerGuard. - -### Security notice - -RouteGuard and ControllerGuard listen to the `MvcEvent::EVENT_ROUTE` event. Therefore, if you use the -`forward` method into your controller, those guards will not intercept and check requests (because internally -ZF2 does not trigger again a new MVC loop). - -Most of the time, this is not an issue, but you must be aware of it, and this is an additional reason why you -should always protect your services too. - -## Creating custom guards - -LmcRbac is flexible enough to allow you to create custom guard. Let's say we want to create a guard that will -refuse access based on an IP addresses blacklist. - -First create the guard: - -```php -namespace Application\Guard; - -use Laminas\Http\Request as HttpRequest; -use Laminas\Mvc\MvcEvent; -use LmcRbac\Guard\AbstractGuard; - -class IpGuard extends AbstractGuard -{ - const EVENT_PRIORITY = 100; - - /** - * List of IPs to blacklist - */ - protected $ipAddresses = []; - - /** - * @param array $ipAddresses - */ - public function __construct(array $ipAddresses) - { - $this->ipAddresses = $ipAddresses; - } - - /** - * @param MvcEvent $event - * @return bool - */ - public function isGranted(MvcEvent $event) - { - $request = $event->getRequest(); - - if (!$request instanceof HttpRequest) { - return true; - } - - if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $clientIp = $_SERVER['HTTP_X_FORWARDED_FOR']; - } else { - $clientIp = $_SERVER['REMOTE_ADDR']; - } - - return !in_array($clientIp, $this->ipAddresses); - } -} -``` - -> Guards must implement `LmcRbac\Guard\GuardInterface`. - -By default, guards are listening to the event `MvcEvent::EVENT_ROUTE` with a priority of -5 (you can change the default -event to listen by overriding the `EVENT_NAME` constant in your guard subclass). However, in this case, we don't -even need to wait for the route to be matched, so we overload the `EVENT_PRIORITY` constant to be executed earlier. - -The `isGranted` method simply retrieves the client IP address, and check it against the blacklist. - -However, for this to work, we must register the newly created guard to the guard plugin manager. To do so, add the -following code in your config: - -```php -return [ - 'lmc_rbac' => [ - 'guard_manager' => [ - 'factories' => [ - 'Application\Guard\IpGuard' => 'Application\Factory\IpGuardFactory' - ] - ] - ] -]; -``` - -The `guard_manager` config follows a conventional service manager config format. - -Now, let's create the factory: - -```php -namespace Application\Factory; - -use Application\Guard\IpGuard; -use Laminas\ServiceManager\FactoryInterface; -use Laminas\ServiceManager\MutableCreationOptionsInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class IpGuardFactory implements FactoryInterface, MutableCreationOptionsInterface -{ - /** - * @var array - */ - protected $options; - - /** - * {@inheritDoc} - */ - public function setCreationOptions(array $options) - { - $this->options = $options; - } - - /** - * {@inheritDoc} - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - return new IpGuard($this->options); - } -} -``` - -The `MutableCreationOptionsInterface` was introduced in Laminas 2.2, and allows your factories to accept -options. In fact, in a real use case, you would likely fetched the blacklist from database. - -Now, we just need to add the guard to the `guards` option, so that LmcRbac execute the logic behind this guard. In -your config, add the following code: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'Application\Guard\IpGuard' => [ - '87.45.66.46', - '65.87.35.43' - ] - ] - ] -]; -``` - -### Navigation - -* Continue to [the **Strategies**](/docs/05. Strategies.md) -* Back to [the Role providers](/docs/03. Role providers.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/05. Strategies.md b/docs/05. Strategies.md deleted file mode 100644 index b70675c6..00000000 --- a/docs/05. Strategies.md +++ /dev/null @@ -1,153 +0,0 @@ -# Strategies - -In this section, you will learn: - -* What are strategies -* How to use built-in strategies -* Creating custom strategies - -## What are strategies? - -A strategy is an object that listens to the `MvcEvent::EVENT_DISPATCH_ERROR` event. It is used to describe what -happens when an access to a resource is unauthorized by LmcRbac. - -LmcRbac strategies all check if an `LmcRbac\Exception\UnauthorizedExceptionInterface` has been thrown. - -By default, LmcRbac does not register any strategy for you. The best place to register it is in your `onBootstrap` -method of the `Module.php` class: - -```php -public function onBootstrap(MvcEvent $event) -{ - $app = $event->getApplication(); - $sm = $app->getServiceManager(); - $em = $app->getEventManager(); - - $listener = $sm->get(\LmcRbac\View\Strategy\RedirectStrategy::class); - $listener->attach($em); -} -``` - -## Built-in strategies - -LmcRbac comes with two built-in strategies: `RedirectStrategy` and `UnauthorizedStrategy`. - -### RedirectStrategy - -This strategy allows to redirect any unauthorized request to another route, by optionally appending the previous -URL as a query param. - -To register it, copy-paste this code in your Module.php class: - -```php -public function onBootstrap(EventInterface $e) -{ - $t = $e->getTarget(); - - $t->getEventManager()->attach( - $t->getServiceManager()->get('LmcRbac\View\Strategy\RedirectStrategy') - ); -} -``` - -You can configure the strategy using the `redirect_strategy` subkey: - -```php -return [ - 'lmc_rbac' => [ - 'redirect_strategy' => [ - 'redirect_when_connected' => true, - 'redirect_to_route_connected' => 'home', - 'redirect_to_route_disconnected' => 'login', - 'append_previous_uri' => true, - 'previous_uri_query_key' => 'redirectTo' - ], - ] -]; -``` - -If a user tries to access to an unauthorized resource (eg.: http://www.example.com/delete), he/she will be -redirect to the "login" route if is not connected and to the "home" route otherwise (it must exists in your route config, -of course) with the previous URL appended : http://www.example.com/login?redirectTo=http://www.example.com/delete - -You can prevent redirection when a user is connected (i.e. so that the user gets a 403 page) by setting `redirect_when_connected` to `false`. - -### UnauthorizedStrategy - -This strategy allows to render a template on any unauthorized request. - -To register it, copy-paste this code in your Module.php class: - -```php -public function onBootstrap(EventInterface $e) -{ - $t = $e->getTarget(); - - $t->getEventManager()->attach( - $t->getServiceManager()->get('LmcRbac\View\Strategy\UnauthorizedStrategy') - ); -} -``` - -You can configure the strategy using the `unauthorized_strategy` subkey: - -```php -return [ - 'lmc_rbac' => [ - 'unauthorized_strategy' => [ - 'template' => 'error/custom-403' - ], - ] -]; -``` - -> By default, LmcRbac uses a template called `error/403`. - -## Creating custom strategies - -Creating a custom strategy is rather easy. Let's say we want to create a strategy that integrates with -the [ApiProblem](https://github.com/laminas-api-tools/api-tools-api-problem) Laminas module: - -```php -namespace Application\View\Strategy; - -use Laminas\Http\Response as HttpResponse; -use Laminas\Mvc\MvcEvent; -use Laminas\ApiTools\ApiProblem\ApiProblem; -use Laminas\ApiTools\ApiProblem\ApiProblemResponse; -use LmcRbac\View\Strategy\AbstractStrategy; -use LmcRbac\Exception\UnauthorizedExceptionInterface; - -class ApiProblemStrategy extends AbstractStrategy -{ - public function onError(MvcEvent $event) - { - // Do nothing if no error or if response is not HTTP response - if (!($exception = $event->getParam('exception') instanceof UnauthorizedExceptionInterface) - || ($result = $event->getResult() instanceof HttpResponse) - || !($response = $event->getResponse() instanceof HttpResponse) - ) { - return; - } - - return new ApiProblemResponse(new ApiProblem($exception->getMessage())); - } -} -``` - -Register your strategy: - -```php -public function onBootstrap(EventInterface $e) -{ - $e->getTarget() - ->getEventManager() - ->attach(new ApiProblemStrategy()); -} -``` - -### Navigation - -* Continue to [the **Authorization Service**](/docs/06. Using the Authorization Service.md) -* Back to [the Guards](/docs/04. Guards.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/06. Using the Authorization Service.md b/docs/06. Using the Authorization Service.md deleted file mode 100644 index af295d58..00000000 --- a/docs/06. Using the Authorization Service.md +++ /dev/null @@ -1,276 +0,0 @@ -# Using the Authorization Service - -This section will teach you how to use the AuthorizationService to its full extend. - -## Injecting the Authorization Service - -### Using initializers - -To automatically inject the authorization service into your classes, you can implement the -`AuthorizationServiceAwareInterface` and use the trait, as shown below: - -```php -namespace YourModule; - -use LmcRbac\Service\AuthorizationServiceAwareInterface; -use LmcRbac\Service\AuthorizationServiceAwareTrait; - -class MyClass implements AuthorizationServiceAwareInterface -{ - use AuthorizationServiceAwareTrait; - - public function doSomethingThatRequiresAuth() - { - if (! $this->getAuthorizationService()->isGranted('deletePost')) { - throw new UnauthorizedException('You are not allowed !'); - } - - return true; - } -} -``` - -Then, registers the initializer in your config (it is not by default): - -```php -class Module -{ - // ... - - public function getServiceConfig() - { - return [ - 'initializers' => [ - 'LmcRbac\Initializer\AuthorizationServiceInitializer' - ] - ]; - } -} -``` - -> While initializers allow rapid prototyping, it can leads to more fragile code. We'd suggest using factories. - -### Using delegator factory - -LmcRbac is shipped with a `LmcRbac\Factory\AuthorizationServiceDelegatorFactory` [delegator factory] -(http://framework.zend.com/manual/2.3/en/modules/zend.service-manager.delegator-factories.html) -to automatically inject the authorization service into your classes. - -As for the initializer, the class must implement the `AuthorizationServiceAwareInterface`. - -You just have to add your classes to the right delegator : - -```php -class Module -{ - // ... - - public function getServiceConfig() - { - return [ - 'invokables' => [ - 'Application\Service\MyClass' => 'Application\Service\MyClassService', - ], - 'delegators' => [ - 'Application\Service\MyClass' => [ - 'LmcRbac\Factory\AuthorizationServiceDelegatorFactory', - // eventually add more delegators here - ], - ], - ]; - } -} -``` - -> While they need a little more configuration, delegators factories have better performances than initializers. - -### Using Factories - -You can inject the AuthorizationService in your factories by using Zend's ServiceManager. The AuthorizationService -is known to the ServiceManager as `\LmcRbac\Service\AuthorizationService::class`. Here is a classic example for injecting -the AuthorizationService: - -*YourModule/Module.php* - -```php -class Module -{ - // getAutoloaderConfig(), etc... - - public function getServiceConfig() - { - return [ - 'factories' => [ - 'MyService' => function($sm) { - $authService = $sm->get(\LmcRbac\Service\AuthorizationService::class); - return new MyService($authService); - } - ] - ]; - } -} -``` - -### Using Zend\DI - -DI is a great way for prototyping, getting results *fast* and maintaining a flexible structure. However it adds overhead and can get very slow. Unless you are using a compiler it is **not** recommended for production. -Here's how you enable Zend\DI to inject the AuthorizationService in MyClass: - -*YourModule/Module.php* - -```php -namespace YourModule; - -class Module -{ - // getAutoloaderConfig(), etc... - - public function getConfig() - { - return [ - 'di' => [ - 'definition' => [ - 'class' => [ - __NAMESPACE__ . '\MyClass' => [ - 'setAuthorizationService' => [ - 'required' => true - ] - ] - ] - ] - ] - ]; - } -} -``` - -## Permissions and Assertions - -Since you now know how to inject the AuthorizationService, let's use it! - -One of the great things the AuthorizationService brings are **assertions**. Assertions get executed *if the identity -in fact holds the permission you are requesting*. A common example is a blog post, which only the author can edit. In -this case, you have a `post.edit` permission and run an assertion checking the author afterwards. - -### Defining assertions - -The AssertionPluginManager is a great way for you to use assertions and IOC. You can add new assertions quite easily -by adding this to your `module.config.php` file: - -```php -return [ - 'lmc_rbac' => [ - 'assertion_manager' => [ - 'factories' => [ - 'MyAssertion' => 'MyAssertionFactory' - ] - ] - ] -]; -``` - -### Defining the assertion map - -The assertion map can automatically map permissions to assertions. This means that every times you check for a -permission with an assertion map, you'll include the assertion in your check. You can define the assertion map by -adding this to your `module.config.php` file: - -```php -return [ - 'lmc_rbac' => [ - 'assertion_map' => [ - 'myPermission' => 'myAssertion' - ] - ] -]; -``` - -Now, everytime you check for `myPermission`, `myAssertion` will be checked as well. - -### Checking permissions in a service - -So let's check for a permission, shall we? - -```php -$authorizationService->isGranted('myPermission'); -``` - -That was easy, wasn't it? - -`isGranted` checks if the current identity is granted the permission and additionally runs the assertion that is -provided by the assertion map. - -### Checking permissions in a controller or in a view - -LmcRbac comes with both a controller plugin and a view helper to check permissions. - -In a controller : - -```php - public function doSomethingAction() - { - if (!$this->isGranted('myPermission')) { - // redirect if not granted for example - } - } -``` - -In a view : - -```php - isGranted('myPermission')): ?> -
-

Display only if granted

-
- -``` - -### Defining additional permissions - -But what if you don't want to use the assertion map? That's quite easy as well! - -Here are four use cases on how you can work without the assertion map: - -Disable the assertion: - -```php -$authorizationService->setAssertion('myPermission', null); -$authorizationService->isGranted('myPermission'); -``` - -Callback assertion: -```php -$something = true; - -$authorizationService->setAssertion( - 'myPermission', - function(AuthorizationService $authorization, $context = true) use ($something) { - return $something === $context - } -); - -$authorizationService->isGranted('myPermission'); // returns true, when the identity holds the permission `myPermission` -``` - -Object implementing `AssertionInterface`: -```php -$context = true; - -$authorizationService->setAssertion('myPermission', new MyAssertion($foo, $bar)); -$authorizationService->isGranted('myPermission', $context); -``` - -Using the AssertionPluginManager: -```php -$context = true; -$authorizationService->setAssertion('myPermission', 'MyAssertion'); -$authorizationService->isGranted('myPermission', $context); -``` - -*Please note: The context parameter is optional!* - -### Navigation - -* Continue to [the **Cookbook**](/docs/07. Cookbook.md) -* Back to [the Strategies](/docs/05. Strategies.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/07. Cookbook.md b/docs/07. Cookbook.md deleted file mode 100644 index 447c62e8..00000000 --- a/docs/07. Cookbook.md +++ /dev/null @@ -1,781 +0,0 @@ -# Cookbook - -This section will help you to further understand how LmcRbac works by providing more concrete examples. If you have -any other recipe you'd like to add, please open an issue! - -- [A Real World Application](/docs/07.%20Cookbook.md#a-real-world-application) - - [Best Practices](https://github.com/manuakasam/zfc-rbac/blob/master/docs/07.%20Cookbook.md#best-practices) - - [When to use Guards](/docs/07.%20Cookbook.md#when-using-guards-then) -- [A Real World Application Part 2 - Only delete your own Posts](/docs/07.%20Cookbook.md#a-real-world-application-part-2---only-delete-your-own-posts) -- [A Real World Application Part 3 - But Admins can delete everything](/docs/07.%20Cookbook.md#a-real-world-application-part-3---admins-can-delete-everything) -- [A Real World Application Part 4 - Only admins have the menu item for managing the posts] - (/docs/07.%20Cookbook.md#a-real-world-application-part-4---checking-permissions-in-the-view) -- [Using LmcRbac with Doctrine ORM](/docs/07.%20Cookbook.md#using-LmcRbac-with-doctrine-orm) - - [How to deal with roles with lot of permissions?](/docs/07.%20Cookbook.md#how-to-deal-with-roles-with-lot-of-permissions) -- [Using LmcRbac and ZF2 Assetic](/docs/07.%20Cookbook.md#using-LmcRbac-and-zf2-assetic) -- [Using LmcRbac and ZfcUser](/docs/07.%20Cookbook.md#using-LmcRbac-and-zfcuser) - -## A Real World Application - -In this example we are going to create a very little real world application. We will create a controller -`PostController` that interacts with a service called `PostService`. For the sake of simplicity we will only -cover the `delete`-methods of both parts. - -Let's start by creating a controller that has the `PostService` as dependency: - -```php -class PostController -{ - protected $postService; - - public function __construct(PostService $postService) - { - $this->postService = $postService; - } - - // addAction(), editAction(), etc... - - public function deleteAction() - { - $id = $this->params()->fromQuery('id'); - - $this->postService->deletePost($id); - - return $this->redirect()->toRoute('posts'); - } -} -``` - -Since we have a dependency, let's inject it using the `ControllerManager`, we will do this inside our `Module`-Class - -```php -class Module -{ - public function getConfig() - { - return [ - 'controllers' => [ - 'factories' => [ - 'PostController' => function ($cpm) { - // We assume a Service key 'PostService' here that returns the PostService Class - return new PostController( - $cpm->getServiceLocator()->get('PostService') - ); - }, - ], - ], - ]; - } -} -``` - -Now that we got this in place let us quickly define our `PostService`. We will be using a Service that makes use -of Doctrine, so we require a `Doctrine\Common\Persistence\ObjectManager` as dependency. - -```php -use Doctrine\Common\Persistence\ObjectManager; - -class PostService -{ - protected $objectManager; - - public function __construct(ObjectManager $objectManager) - { - $this->objectManager = $objectManager; - } - - public function deletePost($id) - { - $post = $this->objectManager->find('Post', $id); - $this->objectManager->remove($post); - $this->objectManager->flush(); - } -} -``` - -And for this one, too, let's quickly create the factory, again within our `Module` class. - -```php -class Module -{ - // getAutoloaderConfig(), getConfig(), etc... - - public function getServiceConfig() - { - return [ - 'factories' => [ - 'PostService' => function($sm) { - return new PostService( - $sm->get('doctrine.entitymanager.orm_default') - ); - } - ] - ]; - } -} -``` - -With this set up we can now cover some best practices. - -## Best practices - -Ideally, you should not protect your applications using only guards (Route or Controller guards). -This leaves your application open for some undesired side-effects. -As a best practice you should protect all your services or controllers by injecting the authorization service. -But let's go step by step: - -Assuming the application example above we can easily use LmcRbac to protect our route using the following guard: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'post/delete' => ['admin'] - ] - ] - ] -]; -``` - -Now, any users that do not have the "admin" role will receive a 403 error (unauthorized) when trying to access -the "post/delete" route. However, this does not prevent the service (which should contain the actual logic in a properly -design application) to be injected and used elsewhere in your code. For instance: - -```php -class PostController -{ - protected $postService; - - public function createAction() - { - // this action may have been reached through the "forward" method, hence bypassing guards - $this->postService->deletePost('2'); - } -} -``` - -You see the issue! - -The solution is to inject the `AuthorizationService` into your services, and checking for the -permissions before doing anything wrong. So let's modify our previously created `PostService`-class - -```php -use Doctrine\Common\Persistence\ObjectManager; -use LmcRbac\Service\AuthorizationService; - -class PostService -{ - protected $objectManager; - - protected $authorizationService; - - public function __construct( - ObjectManager $objectManager, - AuthorizationService $autorizationService - ) { - $this->objectManager = $objectManager; - $this->authorizationService = $autorizationService; - } - - public function deletePost($id) - { - // First check permission - if (!$this->authorizationService->isGranted('deletePost')) { - throw new UnauthorizedException('You are not allowed !'); - } - - $post = $this->objectManager->find('Post', $id); - $this->objectManager->remove($post); - $this->objectManager->flush(); - } -} -``` - -Since we now have an additional dependency we should inject it through our factory, again within our `Module` class. - -```php -class Module -{ - // getAutoloaderConfig(), getConfig(), etc... - - public function getServiceConfig() - { - return [ - 'factories' => [ - 'PostService' => function($sm) { - return new PostService( - $sm->get('doctrine.entitymanager.orm_default'), - $sm->get(\LmcRbac\Service\AuthorizationService::class) // This is new! - ); - } - ] - ]; - } -} -``` - -Alternatively, you can also protect your controllers using the `isGranted` helper (you do not need to inject -the AuthorizationService then): - -```php -class PostController -{ - protected $postService; - - public function createAction() - { - if (!$this->isGranted('deletePost')) { - throw new UnauthorizedException('You are not allowed !'); - } - - $this->postService->deletePost('2'); - } -} -``` - -While protecting services is the more defensive way (because services are usually the last part of the logic flow), -it is often complicated to deal with. -If your application is architectured correctly, it is often simpler to protect your controllers. - -### When using guards then? - -In fact, you should see guards as a very efficient way to quickly reject access to a hierarchy of routes or a -whole controller. For instance, assuming you have the following route config: - -```php -return [ - 'router' => [ - 'routes' => [ - 'admin' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/admin' - ], - 'may_terminate' => true, - 'child_routes' => [ - 'users' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/users' - ] - ], - 'invoices' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/invoices' - ] - ] - ] - ] - ] - ] -}; -``` - -You can quickly unauthorized access to all admin routes using the following guard: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'admin*' => ['admin'] - ] - ] - ] -]; -``` - -## A Real World Application Part 2 - Only delete your own Posts - -If you jumped straight to this section please notice, that we assume you have the knowledge that we presented in the -previous example. In here we will cover a very common use-case. Users of our Application should only have delete -permissions to their own content. So let's quickly refresh our `PostService` class: - -```php -use Doctrine\Common\Persistence\ObjectManager; - -class PostService -{ - protected $objectManager; - - protected $authorizationService; - - public function __construct( - ObjectManager $objectManager, - AuthorizationService $autorizationService - ) { - $this->objectManager = $objectManager; - $this->authorizationService = $autorizationService; - } - - public function deletePost($id) - { - // First check permission - if (!$this->authorizationService->isGranted('deletePost')) { - throw new UnauthorizedException('You are not allowed !'); - } - - $post = $this->objectManager->find('Post', $id); - $this->objectManager->remove($post); - $this->objectManager->flush(); - } -} -``` - -As we can see, we check within our Service if the User of our Application is allowed to delete the post with a check -against the `deletePost` permission. Now how can we achieve that only a user who is the owner of the Post to be able to -delete his own post, but other users can't? We do not want to change our Service with more complex logic because this -is not the task of such service. The Permission-System should handle this. And we can, for this we have the - [`AssertionPluginManager`](/src/LmcRbac/Assertion/AssertionPluginManager.php) and here is how to do it: - -First of all things we need to write an Assertion. The Assertion will return a boolean statement about the current -identity being the owner of the post. - -```php -namespace Your\Namespace; - -use LmcRbac\Assertion\AssertionInterface; -use LmcRbac\Service\AuthorizationService; - -class MustBeAuthorAssertion implements AssertionInterface -{ - /** - * Check if this assertion is true - * - * @param AuthorizationService $authorization - * @param mixed $post - * - * @return bool - */ - public function assert(AuthorizationService $authorization, $post = null) - { - return $authorization->getIdentity() === $post->getAuthor(); - } -} -``` - -This simple `MustBeAuthorAssertion` will check against the current `$authorization` if it equals the identity of the -current context Author. The second parameter is called the "context". A context can be anything (an object, a scalar, -an array...) and makes only sense in the context of the assertion. - -Imagine a user calls `http://my.dom/post/delete/42`, so obviously he wants to delete the Post-Entity with ID#42. In -this case Entity#42 is our Context! If you're wondering of how the context get there, bare with me, we will get to -this later. - -Now that we have written the Assertion, we want to make sure that this assertion will always be called, whenever we -check for the `deletePost` permission. We don't want others to delete our previous content! For this we have the so- -called `assertion_map`. In this map we glue `assertions` and `permissions` together. - -```php -// module.config.php or wherever you configure your RBAC stuff -return [ - 'lmc_rbac' => [ - 'assertion_map' => [ - 'deletePost' => 'Your\Namespace\MustBeAuthorAssertion' - ] - ] -]; -``` - -Now, whenever some test the `deletePost` permission, it will automatically call the `MustBeAuthorAssertion` from -the `AssertionPluginManager`. This plugin manager is configured to automatically add unknown classes to an invokable. -However, some assertions may need dependencies. You can manually configure the assertion plugin manager as -shown below: - -```php -// module.config.php or wherever you configure your RBAC stuff -return [ - 'lmc_rbac' => [ - // ... other rbac stuff - 'assertion_manager' => [ - 'factories' => [ - 'AssertionWithDependency' => 'Your\Namespace\AssertionWithDependencyFactory' - ] - ] - ] -]; -``` - -Now we need to remember about the **context**. Somehow we need to let the `AssertionPluginManager` know about our -context. This is done as simple as to passing it to the `isGranted()` method. For this we need to modify our Service -one last time. - -```php -use Doctrine\Common\Persistence\ObjectManager; - -class PostService -{ - protected $objectManager; - - protected $authorizationService; - - public function __construct( - ObjectManager $objectManager, - AuthorizationService $autorizationService - ) { - $this->objectManager = $objectManager; - $this->authorizationService = $autorizationService; - } - - public function deletePost($id) - { - // Note, we now need to query for the post of interest first! - $post = $this->objectManager->find('Post', $id); - - // Check the permission now with a given context - if (!$this->authorizationService->isGranted('deletePost', $post)) { - throw new UnauthorizedException('You are not allowed !'); - } - - $this->objectManager->remove($post); - $this->objectManager->flush(); - } -} -``` - -And there you have it. The context is injected into the `isGranted()` method and now the `AssertionPluginManager` knows -about it and can do its thing. Note that in reality, after you have queried for the `$post` you would check if `$post` -is actually a real post. Because if it is an empty return value then you should throw an exception earlier without -needing to check against the permission. - -## A Real World Application Part 3 - Admins can delete everything - -Often, you want users with a specific role to be able to have full access to everything. For instance, admins could -delete all the posts, even if they don't own it. - -However, with the previous assertion, even if the admin has the permission `deletePost`, it won't work because -the assertion will evaluate to false. - -Actually, the answer is quite simple: deleting my own posts and deleting others' posts should be treated like -two different permissions (it makes sense if you think about it). Therefore, admins will have the permission -`deleteOthersPost` (as well as the permission `deletePost`, because admin could write posts, too). - -The assertion must therefore be modified like this: - -```php -namespace Your\Namespace; - -use LmcRbac\Assertion\AssertionInterface; -use LmcRbac\Service\AuthorizationService; - -class MustBeAuthorAssertion implements AssertionInterface -{ - /** - * Check if this assertion is true - * - * @param AuthorizationService $authorization - * @param mixed $context - * - * @return bool - */ - public function assert(AuthorizationService $authorization, $context = null) - { - if ($authorization->getIdentity() === $context->getAuthor()) { - return true; - } - - return $authorization->isGranted('deleteOthersPost'); - } -} -``` - -## A Real World Application Part 4 - Checking permissions in the view - -If some part of the view needs to be protected, you can use the shipped ```isGranted``` view helper. - -For example, lets's say that only users with the permissions ```post.manage``` will have a menu item to acces -the adminsitration panel : - -In your template post-index.phtml - -```php - -``` - -You can even protect your menu item regarding a role, by using the ```hasRole``` view helper : - -```php - -``` - -In this last example, the menu item will be hidden for users who don't have the ```admin``` role. - -## Using LmcRbac with Doctrine ORM - -First your User entity class must implement `LmcRbac\Identity\IdentityInterface` : - -```php -use ZfcUser\Entity\User as ZfcUserEntity; -use LmcRbac\Identity\IdentityInterface; -use Doctrine\ORM\Mapping as ORM; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; - -/** - * @ORM\Entity - * @ORM\Table(name="user") - */ -class User extends ZfcUserEntity implements IdentityInterface -{ - /** - * @var Collection - * @ORM\ManyToMany(targetEntity="HierarchicalRole") - */ - private $roles; - - public function __construct() - { - $this->roles = new ArrayCollection(); - } - - /** - * {@inheritDoc} - */ - public function getRoles() - { - return $this->roles->toArray(); - } - - /** - * Set the list of roles - * @param Collection $roles - */ - public function setRoles(Collection $roles) - { - $this->roles->clear(); - foreach ($roles as $role) { - $this->roles[] = $role; - } - } - - /** - * Add one role to roles list - * @param \Rbac\Role\RoleInterface $role - */ - public function addRole(RoleInterface $role) - { - $this->roles[] = $role; - } -} -``` -For this example we will use the more complex situation by using `Rbac\Role\HierarchicalRoleInterface` so the second step is to create HierarchicalRole entity class - -```php -class HierarchicalRole implements HierarchicalRoleInterface -{ - /** - * @var HierarchicalRoleInterface[]|\Doctrine\Common\Collections\Collection - * - * @ORM\ManyToMany(targetEntity="HierarchicalRole") - */ - protected $children; - - /** - * @var PermissionInterface[]|\Doctrine\Common\Collections\Collection - * - * @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="EAGER", cascade={"persist"}) - */ - protected $permissions; - - /** - * Init the Doctrine collection - */ - public function __construct() - { - $this->children = new ArrayCollection(); - $this->permissions = new ArrayCollection(); - } - - /** - * {@inheritDoc} - */ - public function addChild(HierarchicalRoleInterface $child) - { - $this->children[] = $child; - } - - /* - * Set the list of permission - * @param Collection $permissions - */ - public function setPermissions(Collection $permissions) - { - $this->permissions->clear(); - foreach ($permissions as $permission) { - $this->permissions[] = $permission; - } - } - - /** - * {@inheritDoc} - */ - public function addPermission($permission) - { - if (is_string($permission)) { - $permission = new Permission($permission); - } - - $this->permissions[(string) $permission] = $permission; - } - - /** - * {@inheritDoc} - */ - public function hasPermission($permission) - { - // This can be a performance problem if your role has a lot of permissions. Please refer - // to the cookbook to an elegant way to solve this issue - - return isset($this->permissions[(string) $permission]); - } - - /** - * {@inheritDoc} - */ - public function getChildren() - { - return $this->children->toArray(); - } - - /** - * {@inheritDoc} - */ - public function hasChildren() - { - return !$this->children->isEmpty(); - } -} -``` - -And the last step is to create Permission entity class which is a very simple entity class, you don't have to do specific things ! - -You can find all entity example in this folder : [Example](/data) - -You need one more configuration step. Indeed, how can the RoleProvider retrieve your role and permissions ? For this you need to configure `LmcRbac\Role\ObjectRepositoryRoleProvider` in your `lmc_rbac.global.php` file : -```php - /** - * Configuration for role provider - */ - 'role_provider' => [ - \LmcRbac\Role\ObjectRepositoryRoleProvider::class => [ - 'object_manager' => 'doctrine.entitymanager.orm_default', // alias for doctrine ObjectManager - 'class_name' => 'User\Entity\HierarchicalRole', // FQCN for your role entity class - 'role_name_property' => 'name', // Name to show - ], - ], -``` - -Using DoctrineORM with LmcRbac is very simple. You need to be aware from performances where there is a lot of permissions for roles. - -## How to deal with roles with lot of permissions? - -In very complex applications, your roles may have dozens of permissions. In the [/data/FlatRole.php.dist] entity -we provide, we configure the permissions association so that whenever a role is loaded, all its permissions are also -loaded in one query (notice the `fetch="EAGER"`): - -```php -/** - * @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="EAGER") - */ -protected $permissions; -``` - -The `hasPermission` method is therefore really simple: - -```php -public function hasPermission($permission) -{ - return isset($this->permissions[(string) $permission]); -} -``` - -However, with a lot of permissions, this method will quickly kill your database. What you can do is modfiy the Doctrine -mapping so that the collection is not actually loaded: - -```php -/** - * @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="LAZY") - */ -protected $permissions; -``` - -Then, modify the `hasPermission` method to use the Criteria API. The Criteria API is a Doctrine 2.2+ API that allows -to efficiently filter a collection without loading the whole collection: - -```php -use Doctrine\Common\Collections\Criteria; - -public function hasPermission($permission) -{ - $criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) $permission)); - $result = $this->permissions->matching($criteria); - - return count($result) > 0; -} -``` - -> NOTE: This is only supported starting from Doctrine ORM 2.5! - -## Using LmcRbac and ZF2 Assetic - -To use [Assetic](https://github.com/widmogrod/zf2-assetic-module) with LmcRbac guards, you should modify your -`module.config.php` using the following configuration: - -```php -return [ - 'assetic_configuration' => [ - 'acceptableErrors' => [ - \LmcRbac\Guard\GuardInterface::GUARD_UNAUTHORIZED - ] - ] -]; -``` - -## Using LmcRbac and ZfcUser - -To use the authentication service from ZfcUser, just add the following alias in your application.config.php: - -```php -return [ - 'service_manager' => [ - 'aliases' => [ - 'Laminas\Authentication\AuthenticationService' => 'zfcuser_auth_service' - ] - ] -]; -``` - -Finally add the ZfcUser routes to your `guards`: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'zfcuser/login' => ['guest'], - 'zfcuser/register' => ['guest'], // required if registration is enabled - 'zfcuser*' => ['user'] // includes logout, changepassword and changeemail - ] - ] - ] -]; -``` - -### Navigation - -* Back to [the Using the Authorization Service](/docs/06. Using the Authorization Service.md) -* Back to [the Index](/docs/README.md) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 3ed2e93a..00000000 --- a/docs/README.md +++ /dev/null @@ -1,52 +0,0 @@ -_Up-to-date with version 2.3.* of LmcRbac_ - -Welcome to the official ZfcRbac documentation. This documentation will help you to quickly understand how to use -and extend ZfcRbac. - -If you are looking for some information that is not listed in the documentation, please open an issue! - -1. [Introduction](/docs/01. Introduction.md) - 1. [Why should I use an authorization module?](/docs/01. Introduction.md#why-should-i-use-an-authorization-module) - 2. [What is the Rbac model?](/docs/01. Introduction.md#what-is-the-rbac-model) - 3. [How can I integrate LmcRbac into my application?](/docs/01. Introduction.md#how-can-i-integrate-zfcrbac-into-my-application) - -2. [Quick Start](/docs/02. Quick Start.md) - 1. [Specifying an identity provider](/docs/02. Quick Start.md#specifying-an-identity-provider) - 2. [Adding a guard](/docs/02. Quick Start.md#adding-a-guard) - 3. [Adding a role provider](/docs/02. Quick Start.md#adding-a-role-provider) - 5. [Registering a strategy](/docs/02. Quick Start.md#registering-a-strategy) - 6. [Using the authorization service](/docs/02. Quick Start.md#using-the-authorization-service) - -3. [Role providers](/docs/03. Role providers.md) - 1. [What are role providers?](/docs/03. Role providers.md#what-are-role-providers) - 2. [Identity providers](/docs/03. Role providers.md#identity-providers) - 3. [Built-in role providers](/docs/03. Role providers.md#built-in-role-providers) - 4. [Creating custom role providers](/docs/03. Role providers.md#creating-custom-role-providers) - -4. [Guards](/docs/04. Guards.md) - 1. [What are guards and when to use them?](/docs/04. Guards.md#what-are-guards-and-when-to-use-them) - 2. [Built-in guards](/docs/04. Guards.md#built-in-guards) - 3. [Creating custom guards](/docs/04. Guards.md#creating-custom-guards) - -5. [Strategies](/docs/05. Strategies.md) - 1. [What are strategies?](/docs/05. Strategies.md#what-are-strategies) - 2. [Built-in strategies](/docs/05. Strategies.md#built-in-strategies) - 3. [Creating custom strategies](/docs/05. Strategies.md#creating-custom-strategies) - -6. [Using the Authorization Service](/docs/06. Using the Authorization Service.md) - 1. [Injecting the AuthorizationService](/docs/06. Using the Authorization Service.md#injecting-the-authorization-service) - 2. [Checking permissions](/docs/06. Using the Authorization Service.md#checking-permissions-in-a-service) - 1. [In a service](/docs/06. Using the Authorization Service.md#checking-permissions-in-a-service) - 2. [In a controller's action using the isGranted controller pluign] - (/docs/06. Using the Authorization Service.md#checking-permissions-in-a-controller-or-in-a-view) - 3. [In a view using the isGranted view helper] - (/docs/06. Using the Authorization Service.md#checking-permissions-in-a-a-controller-or-in-a-view) - 3. [Permissions and Assertions](/docs/06. Using the Authorization Service.md#permissions-and-assertions) - -7. [Cookbook](/docs/07. Cookbook.md) - 1. [A real world example](/docs/07. Cookbook.md#a-real-world-application) - 2. [Best practices](/docs/07. Cookbook.md#best-practices) - 3. [Using LmcRbac with Doctrine ORM](/docs/07. Cookbook.md#using-zfcrbac-with-doctrine-orm) - 4. [How to deal with roles with lot of permissions?](/docs/07. Cookbook.md#how-to-deal-with-roles-with-lot-of-permissions) - 5. [Using LmcRbac and ZF2 Assetic](/docs/07. Cookbook.md#using-zfcrbac-and-zf2-assetic) - 6. [Using LmcRbac and ZfcUser](/docs/07. Cookbook.md#using-zfcrbac-and-zfcuser) diff --git a/docs/images/workflow-with-guards.png b/docs/images/workflow-with-guards.png deleted file mode 100644 index 3c3d2179..00000000 Binary files a/docs/images/workflow-with-guards.png and /dev/null differ diff --git a/docs/images/workflow-without-guards.png b/docs/images/workflow-without-guards.png deleted file mode 100644 index fd49abad..00000000 Binary files a/docs/images/workflow-without-guards.png and /dev/null differ diff --git a/src/Role/ObjectRepositoryRoleProvider.php b/src/Role/ObjectRepositoryRoleProvider.php index 5f269d60..815f3987 100644 --- a/src/Role/ObjectRepositoryRoleProvider.php +++ b/src/Role/ObjectRepositoryRoleProvider.php @@ -21,7 +21,7 @@ namespace LmcRbac\Role; -use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\Persistence\ObjectRepository; use LmcRbac\Exception\RoleNotFoundException; /** diff --git a/test/Container/ObjectRepositoryRoleProviderFactoryTest.php b/test/Container/ObjectRepositoryRoleProviderFactoryTest.php index e1f919be..7fbff9f6 100644 --- a/test/Container/ObjectRepositoryRoleProviderFactoryTest.php +++ b/test/Container/ObjectRepositoryRoleProviderFactoryTest.php @@ -21,8 +21,8 @@ namespace LmcRbacTest\Container; -use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; use Laminas\ServiceManager\ServiceManager; use LmcRbac\Container\ObjectRepositoryRoleProviderFactory; use LmcRbac\Exception\RuntimeException; diff --git a/test/Role/ObjectRepositoryRoleProviderTest.php b/test/Role/ObjectRepositoryRoleProviderTest.php index 1efaca3d..c130065a 100644 --- a/test/Role/ObjectRepositoryRoleProviderTest.php +++ b/test/Role/ObjectRepositoryRoleProviderTest.php @@ -21,7 +21,7 @@ namespace LmcRbacTest\Role; -use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\Persistence\ObjectRepository; use LmcRbac\Role\ObjectRepositoryRoleProvider; use LmcRbacTest\Asset\FlatRole; use PHPUnit\Framework\TestCase;