From 849197aad6f9909ab23e97b45aae032729380127 Mon Sep 17 00:00:00 2001 From: Eric Richer Date: Sat, 6 Feb 2021 15:38:29 -0500 Subject: [PATCH] Added dependency on persistence v2. Removed docs. --- composer.json | 5 +- docs/01. Introduction.md | 59 -- docs/02. Quick Start.md | 136 --- docs/03. Role providers.md | 200 ----- docs/04. Guards.md | 496 ----------- docs/05. Strategies.md | 153 ---- docs/06. Using the Authorization Service.md | 276 ------- docs/07. Cookbook.md | 781 ------------------ docs/README.md | 52 -- docs/images/workflow-with-guards.png | Bin 32236 -> 0 bytes docs/images/workflow-without-guards.png | Bin 8905 -> 0 bytes src/Role/ObjectRepositoryRoleProvider.php | 2 +- ...bjectRepositoryRoleProviderFactoryTest.php | 4 +- .../Role/ObjectRepositoryRoleProviderTest.php | 2 +- 14 files changed, 7 insertions(+), 2159 deletions(-) delete mode 100644 docs/01. Introduction.md delete mode 100644 docs/02. Quick Start.md delete mode 100644 docs/03. Role providers.md delete mode 100644 docs/04. Guards.md delete mode 100644 docs/05. Strategies.md delete mode 100644 docs/06. Using the Authorization Service.md delete mode 100644 docs/07. Cookbook.md delete mode 100644 docs/README.md delete mode 100644 docs/images/workflow-with-guards.png delete mode 100644 docs/images/workflow-without-guards.png 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 3c3d21799b3b02eececb149bcecf20bcc1f41966..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32236 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
-mk-h^4|VO^29iL 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 diff --git a/docs/images/workflow-without-guards.png b/docs/images/workflow-without-guards.png deleted file mode 100644 index fd49abadd8e3f151c4b9c330eaa9099175e0bcea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8905 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 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;