From 2f9a5a7689d103089f79fe502bd4c0cac248884b Mon Sep 17 00:00:00 2001 From: "Eric Richer eric.richer@vistoconsulting.com" Date: Mon, 9 Sep 2024 20:11:45 -0400 Subject: [PATCH] Prepping docs for v2.0 Signed-off-by: Eric Richer eric.richer@vistoconsulting.com --- docs/docusaurus.config.js | 8 + docs/src/components/HomepageHeader/index.js | 2 +- .../version-2.0/Guides/integrating.md | 158 +++++++++++++ .../version-2.0/Upgrading/migration.md | 22 ++ .../version-2.0/Upgrading/to-v2.md | 37 +++ docs/versioned_docs/version-2.0/assertions.md | 153 ++++++++++++ .../version-2.0/authorization-service.md | 166 +++++++++++++ .../version-2.0/configuration.md | 19 ++ .../versioned_docs/version-2.0/quick-start.md | 197 ++++++++++++++++ .../version-2.0/role-providers.md | 219 ++++++++++++++++++ .../version-2.0-sidebars.json | 8 + docs/versions.json | 1 + 12 files changed, 989 insertions(+), 1 deletion(-) create mode 100644 docs/versioned_docs/version-2.0/Guides/integrating.md create mode 100644 docs/versioned_docs/version-2.0/Upgrading/migration.md create mode 100644 docs/versioned_docs/version-2.0/Upgrading/to-v2.md create mode 100644 docs/versioned_docs/version-2.0/assertions.md create mode 100644 docs/versioned_docs/version-2.0/authorization-service.md create mode 100644 docs/versioned_docs/version-2.0/configuration.md create mode 100644 docs/versioned_docs/version-2.0/quick-start.md create mode 100644 docs/versioned_docs/version-2.0/role-providers.md create mode 100644 docs/versioned_sidebars/version-2.0-sidebars.json diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 205bf61..5dda5d8 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -46,6 +46,14 @@ const config = { // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/lm-commons/lmcrbac/tree/master/docs/', + includeCurrentVersion: false, + lastVersion: '2.0', + versions: { + "2.0": { + label: '2.0', + path: '2.0', + } + } }, blog: { showReadingTime: true, diff --git a/docs/src/components/HomepageHeader/index.js b/docs/src/components/HomepageHeader/index.js index c0b874b..4a07f4c 100644 --- a/docs/src/components/HomepageHeader/index.js +++ b/docs/src/components/HomepageHeader/index.js @@ -16,7 +16,7 @@ export function HomepageHeader() {
+ to="/docs/2.0/quick-start"> Quick start
diff --git a/docs/versioned_docs/version-2.0/Guides/integrating.md b/docs/versioned_docs/version-2.0/Guides/integrating.md new file mode 100644 index 0000000..9f86439 --- /dev/null +++ b/docs/versioned_docs/version-2.0/Guides/integrating.md @@ -0,0 +1,158 @@ +--- +title: Integrating into applications +--- + +LmcRbac can be used in your application to implement role-based access control. + +However, it is important to note that Authorization service `isGranted()` method expects +an identity to be provided. The identity must also implement the `Lmc\Rbac\Identity\IdentityInterface`. + +User authentication is not in the scope of LmcRbac and must be implemented by your application. + +## Laminas MVC applications + +In a Laminas MVC application, you can use the ['laminas-authentication'](https://docs.laminas.dev/laminas-authentication) +component with an appropriate adapter to provide the identity. + +The `Laminas\Authentication\AuthenticationService` service provides the identity using the `getIdentity()` method. +However, it is not prescriptive on the signature of the returned identity object. It is up to the +authentication adapter to return a authentication result that contains an identity object that implements the +`IdentityInterface`. + +For example: + +```php +authenticationService = $authenticationService; + $this->authorizationService = $authorizationService; + } + + public function doSomething() + { + $identity = $this->authenticationService->hasIdentity() ? $this->authenticationService->getIdentity() : null; + + // Check for permission + if ($this->getAuthorizationService()->isGranted($identity, 'somepermssion')) { + // authorized + } else { + // not authorized + } + } + +} + +``` +### Other Laminas MVC components to use +To facilitate integration in an MVC application, you can use [LmcUser](https://lm-commons.github.io/LmcUser/) for +authentication. + +You can also use [LmcRbacMvc](https://lm-commons.github.io/LmcRbacMvc/) which extends LmcRbac by handling identities. +It also provides additional functionalities like route guards and strategies for handling unauthorized access. For example, +an unauthorized strategy could be to redirect to a login page. + +## Mezzio and PSR-7 applications + +In a Mezzio application, you can use the [`mezzio/mezzio-authentication`](https://docs.mezzio.dev/mezzio-authentication/) +component to provide the identity. `mezzio/mezzio-authentication` will add a `UserInterface` object to the request attributes. + +Although the `UserInterface` interface has a `getRoles` method, LmcRbac's `AuthorizationService` still expects the identity +to implement the `IdentityInterface`. + +This can be overcome by providing `mezzio/mezzio-authentication` with a custom factory to instantiate a user object that +implements the `IdentityInterface` as explained in this [section](https://docs.mezzio.dev/mezzio-authentication/v1/intro/) +of the `mezzio/mezzio-authentication` documentation. + +For example: + +```php +identity = $identity; + $this->roles = $roles; + $this->details = $details; + } + + public function getIdentity(): string + { + return $this->identity; + } + + public function getRoles(): array + { + return $this->roles; + } + + public function getDetails(): array + { + return $this->details; + } + + public function getDetail(string $name, $default = null) + { + return $this->details[$name] ?? $default; + } +} +``` +Then provide a factory for creating the user class somewhere in a config provider: +```php + [ + UserInterface => function (string $identity, array $roles = [], array $details = []): UserInterface { + return new MyUser($identity, $roles, $details); + }; + ], + ]; + +``` + +From this point, assuming that you have configured your application to use the `Mezzio\Authentication\AuthenticationMiddleware`, +you can use `MyUser` in your handler by retrieving it from the request: + +```php +// Retrieve the UserInterface object from the request. +$user = $request->getAttribute(UserInterface::class); + +// Check for permission, this works because $user implements IdentityInterface +if ($this->getAuthorizationService()->isGranted($user, 'somepermssion')) { + // authorized +} else { + // not authorized +} +``` + +How you define roles and permissions in your application is up to you. One way would be to use the route name as +a permission such that authorization can be set up based on routes and optionally on route+verb. + + +### Other Mezzio components to use + +A LmcRbac Mezzio component is under development to provide factories and middleware to facilitate integration of LmcRbac +in Mezzio applications. diff --git a/docs/versioned_docs/version-2.0/Upgrading/migration.md b/docs/versioned_docs/version-2.0/Upgrading/migration.md new file mode 100644 index 0000000..b582938 --- /dev/null +++ b/docs/versioned_docs/version-2.0/Upgrading/migration.md @@ -0,0 +1,22 @@ +--- +sidebar_label: From ZF-Commons Rbac v3 +sidebar_position: 2 +title: Migrating from ZF-Commons RBAC v3 +--- + +The ZF-Commons Rbac was created for the Zend Framework. When the Zend Framework was migrated to +the Laminas project, the LM-Commons organization was created to provide components formerly provided by ZF-Commons. + +When ZfcRbac was moved to LM-Commons, it was split into two repositories: + +- [LmcRbacMvc](https://github.com/LM-Commons/LmcRbacMvc) contains the old version 2 of ZfcRbac. +- LmcRbac contains the version 3 of ZfcRbac, which was only released as v3.alpha.1. + +To upgrade to LmcRbac v2, it is suggested to do it in two steps: + +1. Upgrade to LmcRbac v1 with the following steps: + * Uninstall `zf-commons/zfc-rbac:3.0.0-alpha.1`. + * Install `lm-commons/lmc-rbac:~1.0` + * Change `zfc-rbac.global.php` to `lmcrbac.global.php` and update the key `zfc_rbac` to `lmc_rbac`. + * Review your code for usages of the `ZfcRbac/*` namespace to `LmcRbac/*` namespace. +2. Upgrade to LmcRbac v2 using the instructions in this [section](to-v2.md). diff --git a/docs/versioned_docs/version-2.0/Upgrading/to-v2.md b/docs/versioned_docs/version-2.0/Upgrading/to-v2.md new file mode 100644 index 0000000..c43c345 --- /dev/null +++ b/docs/versioned_docs/version-2.0/Upgrading/to-v2.md @@ -0,0 +1,37 @@ +--- +sidebar_label: From v1 to v2 +sidebar_position: 1 +title: Upgrading from v1 to v2 +--- + +LmcRbac v2 is a major version upgrade with many breaking changes that prevent +straightforward upgrading. + +### Namespace change + +The namespace has been changed from LmcRbac to Lmc\Rbac. + +Please review your code to replace references to the `LmcRbac` namespace +by the `Lmc\Rbac` namespace. + +### LmcRbac is based on laminas-permissions-rbac + +LmcRbac is now based on the role class and interface provided by laminas-permissions-rbac which +provides a hierarchical role model only. + +Therefore the `Role`, `HierarchicalRole` classes and the `RoleInterface` and `HierarchicalRoleInterface` have been removed +in version 2. + +The `PermissionInterface` interface has been removed as permissions in `laminas-permissions-rbac` as just strings or any +objects that can be casted to a string. If you use objects to hold permissions, just make sure that the object can be +casted to a string by, for example, implementing a `__toString()` method. + +### Refactoring the factories + +The factories for services have been refactored from the `LmcRbac\Container` namespace +to be colocated with the service that a factory is creating. All factories in the `LmcRbac\Container` namespace have +been removed. + +### Refactoring the Assertion Plugin Manager + +The `AssertionContainer` class, interface and factory have been replaced by `AssertionPluginManager` class, interface and factory. diff --git a/docs/versioned_docs/version-2.0/assertions.md b/docs/versioned_docs/version-2.0/assertions.md new file mode 100644 index 0000000..dc86f1a --- /dev/null +++ b/docs/versioned_docs/version-2.0/assertions.md @@ -0,0 +1,153 @@ +--- +sidebar_label: Dynamic Assertions +sidebar_position: 6 +title: Dynamic Assertions +--- + +Dynamic Assertions provide the capability to perform extra validations when +the authorization service's `isGranted()` method is called. + +As described in [Authorization Service](authorization-service#reference), it is possible to pass a context to the +`isGranted()` method. This context is then passed to dynamic assertion functions. This context can be any object type. + +You can define dynamic assertion functions and assigned them to permission via configuration. + +## Defining a dynamic assertion function + +A dynamic assertion must implement the `Lmc\Rbac\Assertion\AssertionInterace` which defines only one method: + +```php +public function assert( + string $permission, + ?IdentityInterface $identity = null, + mixed $context = null + ): bool +``` +The assertion returns `true` when the access is granted, `false` otherwise. + +A simple assertion could be to check that user represented by `$identity`, for the permission +represented by `$permission` owns the resource represented by `$context`. + +```php +getOwnerId() === $identity->getId(); + } + // This should not happen since this assertion should only be + // called when the 'edit' permission is checked + return true; + } +} +``` +## Configuring Assertions + +Dynamic assertions are configured in LmcRbac via an assertion map defined in the LmcRbac configuration where assertions +are associated with permissions. + +The `assertion_map` key in the configuration is used to define the assertion map. If an assertion needs to be created via +a factory, use the `assertion_manager` config key. The Assertion Manager is a standard +plugin manager and its configuration should be a service manager configuration array. + +```php + [ + /* the rest of the file */ + 'assertion_map' => [ + 'edit' => \My\Namespace\MyAssertion::class, + ], + 'assertion_manager' => [ + 'factories' => [ + \My\Namespace\MyAssertion::class => InvokableFactory::class + ], + ], + ], +]; +``` +It is also possible to configure an assertion using a callable instead of a class: + +```php + [ + /* the rest of the file */ + 'assertion_map' => [ + 'edit' => function assert(string $permission, ?IdentityInterface $identity = null, $context = null): bool + { + // for 'edit' permission + if ('edit' === $permission) { + /** @var MyObjectClass $context */ + return $context->getOwnerId() === $identity->getId(); + } + // This should not happen since this assertion should only be + // called when the 'edit' permission is checked + return true; + }, + ], + ], +]; +``` +## Dynamic Assertion sets + +LmcRbac supports the creation of dynamic assertion sets where multiple assertions can be combined using 'and/or' logic. +Assertion sets are configured by associating an array of assertions to a permission in the assertion map: + +```php + [ + /* the rest of the file */ + 'assertion_map' => [ + 'edit' => [ + \My\Namespace\AssertionA::class, + \My\Namespace\AssertionB::class, + ], + 'read' => [ + 'condition' => \Lmc\Rbac\Assertion\AssertionSet::CONDITION_OR, + \My\Namespace\AssertionC::class, + \My\Namespace\AssertionD::class, + ], + 'delete' => [ + 'condition' => \Lmc\Rbac\Assertion\AssertionSet::CONDITION_OR, + \My\Namespace\AssertionE::class, + [ + 'condition' => \Lmc\Rbac\Assertion\AssertionSet::CONDITION_AND, + \My\Namespace\AssertionF::class, + \My\Namespace\AssertionC::class, + ], + ], + /** the rest of the file */ + ], +]; +``` +By default, an assertion set combines assertions using a 'and' condition. This is demonstrated by the map associated with +the `'edit'` permission above. + +It is possible to combine assertions using a 'or' condition by adding a `condition` equal to `AssertionSet::CONDITION_OR` +to the assertion set as demonstrated by the map associated with the `'read'` permission above. + +Furthermore, it is possible to nest assertion sets in order to create more complex logic as demonstrated by the map +associated with the `'delete'` permission above. + +The default logic is to combine assertions using 'and' logic but this can be explicitly set as shown above for `'delete'` +permission. + +## Defining dynamic assertions at run-time + +Although dynamic assertions are typically defined in the application's configuration, it is possible to set +dynamic assertions at run-time by using the Authorization Service utility methods for adding/getting assertions. + +These methods are described in the Authorization Service [reference](authorization-service.md#reference). diff --git a/docs/versioned_docs/version-2.0/authorization-service.md b/docs/versioned_docs/version-2.0/authorization-service.md new file mode 100644 index 0000000..ee68543 --- /dev/null +++ b/docs/versioned_docs/version-2.0/authorization-service.md @@ -0,0 +1,166 @@ +--- +sidebar_label: Authorization service +sidebar_position: 5 +title: Authorization Service +--- + +### Usage + +The Authorization service can be retrieved from the service manager using the name +`Lmc\Rbac\Service\AuthorizationServiceInterface` and injected into your code: + +```php +get(Lmc\Rbac\Service\AuthorizationServiceInterface::class); + +``` +### Reference + +`Lmc\Rbac\Service\AuthorizationServiceInterface` defines the following methods: + +#### `isGranted(?IdentityInterface $identity, string $permission, $context = null): bool` + +Checks that the identity has is granted the permission for the (optional) context. + + | Parameter | Description | + |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `$identity` | The identity whose roles to checks.
If `$identity` is null, then the `guest` is used.
The `guest` role is definable via configuration and defaults to `'guest'`. | + | `$permission` | The permission to check against | + | `$context` | A context that will be passed to dynamic assertions that are defined for the permission | + +#### `setAssertions(array $assertions, bool $merge = false): void` + +Allows to define dynamic assertions at run-time. + + | Parameter | Description | + |---------------|-----------------------------------------------------------------------------------------| + | `$assertions` | An array of assertions to merge or to replace | + | `$merge` | if `true` the content of `$assertions` will be merged with existing assertions. | + + +#### `setAssertion(string $permission, AssertionInterface|callable|string $assertion): void` +Allows to define a dynamic assertion at run-time. + + | Parameter | Description | + |---------------|-----------------------------------------| + | `$permission` | Permission name | + | `$assertion` | The assertion to set for `$permission` | + +#### `hasAssertion(string $permission): bool` +Checks if the authorization has a dynamic assertion for a given permission. + + | Parameter | Description | + |---------------|--------------------------| + | `$permission` | Permission name | + + +#### `getAssertions(): array` + +Returns all the dynamic assertions defined. + +#### `getAssertion(string $permission): AssertionInterface|callable|string|null` + +Returns the dynamic assertion for the give permission + + | Parameter | Description | + |---------------|-----------------------------| + | `$permission` | Permission permission name | + +More on dynamic assertions can be found in the [Assertions](assertions.md) section. + +More on the `guest` role can be found in the [Configuration](configuration.md) section. + +## Injecting the Authorization Service + +There are a few methods to inject the Authorization Service into your service. + +### Using a factory + +You can inject the AuthorizationService into your own objects using a factory. The Authorization Service +can be retrieved from the container using `'Lmc\Rbac\Service\AuthorizationServiceInterface'`. + +Here is a classic example for injecting the Authorization Service into your own service + +*in your app's Module* + +```php +use Lmc\Rbac\Service\AuthorizationServiceInterface; +class Module +{ + public function getConfig() + { + return [ + 'service_manager' => [ + 'factories' => [ + 'MyService' => function($sm) { + $authService = $sm->get('AuthorizationServiceInterface'); + return new MyService($authService); + } + ], + ], + ]; + } +} +```` + +### Using traits + +For convenience, LmcRbac provides a `AuthorizationServiceAwareTrait` that adds the `$authorizationService` property and +setter/getter methods. + +### Using delegators + +LmcRbac ships with a `Lmc\Rbac\Service\AuthorizationServiceDelegatorFactory` [delegator factory](https://docs.laminas.dev/laminas-servicemanager/delegators/) +to automatically inject the authorization service into your classes. + +Your class must implement the `Lmc\Rbac\Service\AuthorizationServiceAwareInterface` and use the above trait, as shown below: + +```php +namespace MyModule; + +use Lmc\Rbac\Service\AuthorizationServiceAwareInterface; +use Lmc\Rbac\Service\AuthorizationServiceAwareTrait; + +class MyClass implements AuthorizationServiceAwareInterface +{ + use AuthorizationServiceAwareTrait; + + public function doSomethingThatRequiresAuth() + { + if (! $this->getAuthorizationService()->isGranted($identity, 'deletePost')) { + throw new \Exception('You are not allowed !'); + } + return true; + } +} +``` + +And add your class to the right delegator: + +```php +namespace MyModule; +use Lmc\Rbac\Service\AuthorizationServiceDelegatorFactory; +class Module +{ + // ... + + public function getConfig() + { + return [ + 'service_manager' => [ + 'factories' => [ + MyClass::class => InvokableFactory::class, + ], + 'delegators' => [ + MyClass::class => [ + AuthorizationServiceDelegatorFactory::class, + ], + ], + ], + ]; + } +} +``` + + diff --git a/docs/versioned_docs/version-2.0/configuration.md b/docs/versioned_docs/version-2.0/configuration.md new file mode 100644 index 0000000..7d94088 --- /dev/null +++ b/docs/versioned_docs/version-2.0/configuration.md @@ -0,0 +1,19 @@ +--- +sidebar_label: Configuration +sidebar_position: 7 +title: Configuring LmcRbac +--- + +LmcRbac is configured via the `lmc_rbac` key in the application config. + +This is typically achieved by creating +a `config/autoload/lmcrbac.global.php` file. A sample configuration file is provided in the `config/` folder. + +## Reference + +| Key | Description | +|--|------------------------------------------------------------------------------------------------------------------------------------------------| +| `guest_role` | Defines the name of the `guest` role when no identity exists.
Defaults to `'guest'`. | +| `role_provider` | Defines the role provider.
Defaults to `[]`
See the [Role Providers](role-providers) section. | +| `assertion_map` | Defines the dynamic assertions that are associated to permissions.
Defaults to `[]`.
See the [Dynamic Assertions](assertions) section. | +| `assertion_manager` | Provides a configuration for the Assertion Plugin Manager.
Defaults to `[]`.
See the [Dynamic Assertion](assertions.md) section. | diff --git a/docs/versioned_docs/version-2.0/quick-start.md b/docs/versioned_docs/version-2.0/quick-start.md new file mode 100644 index 0000000..3789ebc --- /dev/null +++ b/docs/versioned_docs/version-2.0/quick-start.md @@ -0,0 +1,197 @@ +--- +title: Quick Start +sidebar_position: 1 +--- + +LmcRbac offers components and services to implement role-based access control (RBAC) in your application. +LmcRbac extends the components provided by [laminas-permissions-rbac](https://github.com/laminas/laminas-permissions-rbac). + +LmcRbac can be used in Laminas MVC and in Mezzio applications. + +:::tip +If you are upgrading from LmcRbac v1 or from zfc-rbac v3, please read the [Upgrading section](Upgrading/to-v2.md) +::: + +## Concepts + +[Role-Based Access Control (RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control) +is an approach to restricting system access to authorized users by putting emphasis +on roles and their permissions. + +In the RBAC model: + +- an **identity** has one of more roles. +- a **role** has one of more permissions. +- a **permission** is typically an action like "read", "write", "delete". +- a **role** can have **child roles** thus providing a hierarchy of roles where a role will inherit the permissions of all its child roles. + +### Authorization + +An identity will be authorized to perform an action, such as accessing a resource, if it is granted +the permission that controls the execution of the action. + +For example, deleting an item could be restricted to identities that have at least one role that has the +`item.delete` permission. This could be implemented by defining a `member` role that has the `item.delete` and assigning +this role of an authenticated user. + +### Dynamic Assertions + +In some cases, just checking if the identity has the `item.delete` permission is not enough. +It would also be necessary to check, for example, that the `item` belongs to the identity. Dynamic assertion allow +to specify some extra checks before granting access to perform an action such as, in this case, being the owner of the +resource. + +### Identities + +An identity is typically provided by an authentication process within the application. + +Authentication is not in the scope of `LmcRbac` and it is assumed that an identity entity that can provide the assigned +roles is available when using the authorization service. If no identity is available, as it would be the case when no +user is "logged in", then a guest role is assumed. + +## Requirements + +- PHP 8.1 or higher + +## Installation + +LmcRbac only officially supports installation through Composer. + +Install the module: + +```sh +$ composer require lm-commons/lmc-rbac "~1.0" +``` + +You will be prompted by the `laminas-component-installer` plugin to inject LM-Commons\LmcRbac. + +:::note +**Manual installation:** + +Enable the module by adding `Lmc\Rbac` key to your `application.config.php` or `modules.config.php` file for Laminas MVC +applications, or to the `config/config.php` file for Mezzio applications. +::: + +Customize the module by copy-pasting +the `lmcrbac.global.php` file to your `config/autoload` folder. + +:::note +On older versions of `LmcRbac`, the configuration file is named `config/config.global.php`. +::: + +## Defining roles + +By default, no roles and no permissions are defined. + +Roles and permissions are defined by a Role Provider. `LmcRbac` ships with two roles providers: +- a simple `InMemoryRoleProvider` that uses an associative array to define roles and their permission. This is the default. +- a `ObjectRepositoyRoleProvider` that is based on Doctrine ORM. + +To quickly get started, let's use the `InMemoryRoleProvider` role provider. + +In the `config/autoload/lmcrbac.global.php`, add the following: + +```php + [ + 'role_provider' => [ + Lmc\Rbac\Role\InMemoryRoleProvider::class => [ + 'guest', + 'user' => [ + 'permissions' => ['create', 'edit'], + ], + 'admin' => [ + 'children' => ['user'], + 'permissions' => ['delete'], + ], + ], + ], + ], +]; +``` + +This defines 3 roles: a `guest` role, a `user` role having 2 permissions, and a `admin` role which has the `user` role as +a child and with its own permission. If the hierarchy is flattened: + +- `guest` has no permission +- `user` has permissions `create` and `edit` +- `admin` has permissions `create`, `edit` and `delete` + +## Basic authorization + +The authorization service can get retrieved from the service manager container and used to check if a permission +is granted to an identity: + +```php +get('\Lmc\Rbac\Service\AuthorizationServiceInterface'); + + /** @var \Lmc\Rbac\Identity\IdentityInterface $identity */ + if ($authorizationService->isGranted($identity, 'create')) { + /** do something */ + } +``` + +If `$identity` has the role `user` and/or `admin` then the authorization is granted. If the identity has the role `guest`, then authorization +is denied. + +:::info +If `$identity` is null (no identity), then the guest role is assumed which is set to `'guest'` by default. The guest role +can be configured in the `lmcrbac.config.php` file. More on this in the [Configuration](configuration.md) section. +::: + +:::warning +`LmcRbac` does not provide any logic to instantiate an identity entity. It is assumed that +the application will instantiate an entity that implements `\Lmc\Rbac\Identity\IdentityInterface` which defines the `getRoles()` +method. +::: + +## Using assertions + +Even if an identity has the `user` role granting it the `edit` permission, it should not have the authorization to edit another identity's resource. + +This can be achieved using dynamic assertion. + +An assertion is a function that implements the `\Lmc\Rbac\Assertion\AssertionInterface` and is configured in the configuration +file. + +Let's modify the `lmcrbac.config.php` file as follows: + +```php + [ + 'role_provider' => [ + /* roles and permissions + ], + 'assertion_map' => [ + 'edit' => function ($permission, IdentityInterface $identity = null, $resource = null) { + if ($resource->getOwnerId() === $identity->getId() { + return true; + } else { + return false; + } + ], + ], +]; +``` + +Then use the authorization service passing the resource (called a 'context') in addition to the permission: + +```php +get('\Lmc\Rbac\Service\AuthorizationServiceInterface'); + + /** @var \Lmc\Rbac\Identity\IdentityInterface $identity */ + if ($authorizationService->isGranted($identity, 'edit', $resource)) { + /** do something */ + } +``` + +Dynanmic assertions are further discussed in the [Dynamic Assertions](assertions) section. diff --git a/docs/versioned_docs/version-2.0/role-providers.md b/docs/versioned_docs/version-2.0/role-providers.md new file mode 100644 index 0000000..a011763 --- /dev/null +++ b/docs/versioned_docs/version-2.0/role-providers.md @@ -0,0 +1,219 @@ +--- +sidebar_label: Roles, permissions and Role providers +title: Roles, Permissions and Role providers +sidebar_position: 4 +--- + +## Roles + +A role is an object that returns a list of permissions that the role has. + +LmcRbac uses the Role class defined by [laminas-permissions-rbac](https://github.com/laminas/laminas-permissions-rbac). + +Roles are defined using by the `\Laminas\Permissions\Rbac\Role` class or by a class +implementing `\Laminas\Permissions\Rbac\RoleInterface`. + +Roles can have child roles and therefore provides a hierarchy of roles where a role inherit the permissions of all its +child roles. + +For example, a 'user' role may have the 'read' and 'write' permissions, and a 'admin' role +may inherit the permissions of the 'user' role plus an additional 'delete' role. In this structure, +the 'admin' role will have 'user' as its child role. + + +:::tip[Flat roles] +Previous version of LmcRbac used to make a distinction between flat roles and hierarchical roles. +A flat role is just a simplification of a hierarchical role, i.e. a hierarchical role without children. + +In `laminas-permissions-rbac`, roles are hierarchical. +::: + +## Permissions + +A permission in `laminas-permissions-rbac` is simply a string that represents the permission such as 'read', 'write' or 'delete'. +But it can also be more precise like 'article.read' or 'article.write'. + +A permission can also be an object as long as it can be casted to a string. This could be the +case, for example, when permissions are stored in a database where they could also have a identified and a description. + +:::tip +An object can be casted to a string by implementing the `__toString()` method. +::: + +## Role Providers +A role provider is an object that returns a list of roles. A role provider must implement the +`Lmc\Rbac\Role\RoleProviderInterface` interface. The only required method is `getRoles`, and must return an array +of `Laminas\Permissions\Rbac\RoleInterface` objects. + +Roles can come from one of many sources: in memory, from a file, from a database, etc. However, you can specify only one role provider per application. + +### Built-in role providers + +LmcRbac comes with two built-in role providers: `Lmc\Rbac\Role\InMemoryRoleProvider` and +`Lmc\Rbac\Role\ObjectRepositoryRoleProvider`. A role provider must be added to the `role_provider` subkey in the +configuration file. For example: + +```php +return [ + 'lmc_rbac' => [ + 'role_provider' => [ + Lmc\Rbac\Role\InMemoryRoleProvider::class => [ + // configuration + ], + ] + ] +]; +``` + +### `Lmc\Rbac\Role\InMemoryRoleProvider` + +This provider is ideal for small/medium sites with few roles/permissions. All the data is specified in a simple associative array in a +PHP file. + +Here is an example of the format you need to use: + +```php +return [ + 'lmc_rbac' => [ + 'role_provider' => [ + Lmc\Rbac\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 optional. Internally, the `Lmc\Rbac\Role\InMemoryRoleProvider` creates +`Lmc\Rbac\Role\Role` objects with children, if any. + +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' => [ + Lmc\Rbac\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'] + ] + ] + ] + ] +]; +``` + +### `Lmc\Rbac\Role\ObjectRepositoryRoleProvider` + +This provider fetches roles from a 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' => [ + Lmc\Rbac\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' => [ + Lmc\Rbac\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 `Lmc\Rbac\Role\RoleInterface` interface. + +Sample ORM entity models are provided in the `/data` folder for flat role, hierarchical role and permission. + +## Creating custom role providers + +To create a custom role provider, you first need to create a class that implements the +`Lmc\Rbac\Role\RoleProviderInterface` interface. + +Then, you need to add it to the role provider manager: + +```php +return [ + 'lmc_rbac' => [ + 'role_provider' => [ + MyCustomRoleProvider::class => [ + // Options + ], + ], + ], +]; +``` +And the role provider is created using the service manager: +```php +return [ + 'service_manager' => [ + 'factories' => [ + MyCustomRoleProvider::class => MyCustomRoleProviderFactory::class, + ], + ], +]; +``` + +## Role Service + +LmcRbac provides a role service that will use the Role Providers to provide the roles +associated with a given identity. + +It can be retrieved from the container be requesting the `Lmc\Rbac\Service\RoleServiceIntgeface`. + +`Lmc\Rbac\Service\RoleServiceInterface` defines the following method: + +- `getIdentityRoles(?IdentityInterface $identity = null): iterable` + + | Parameter | Description | + |----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `$identity` | The identity whose roles to retrieve.
If `$identity` is null, then the `guest` is used.
The `guest` role is definable via configuration and defaults to `'guest'`. | + + diff --git a/docs/versioned_sidebars/version-2.0-sidebars.json b/docs/versioned_sidebars/version-2.0-sidebars.json new file mode 100644 index 0000000..5f41a72 --- /dev/null +++ b/docs/versioned_sidebars/version-2.0-sidebars.json @@ -0,0 +1,8 @@ +{ + "documentationSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versions.json b/docs/versions.json index df2d553..904ff97 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,3 +1,4 @@ [ + "2.0", "1.4" ]