diff --git a/docs/blog/2024-04-11-New-documentation.md b/docs/blog/2024-04-11-New-documentation.md index d225740..f7b2e1f 100644 --- a/docs/blog/2024-04-11-New-documentation.md +++ b/docs/blog/2024-04-11-New-documentation.md @@ -2,8 +2,8 @@ slug: new-documentation title: New documentation authors: [ericr] -tags: [laminas, PHP, LmcUser, authentication, LM-Commons] +tags: [laminas, PHP, LmcRbac, authorization, LM-Commons] --- -This the new documentation site dedicated to the LmcUser module. +This the new documentation site dedicated to the LmcRbac module. There are no changes to the code, just improvements in the documentation. diff --git a/docs/docs/assertions.md b/docs/docs/assertions.md new file mode 100644 index 0000000..552a549 --- /dev/null +++ b/docs/docs/assertions.md @@ -0,0 +1,144 @@ +--- +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 `LmcRbac\Assertion\AssertionInterace` which defines only one method: + +```php +public function assert( + string $permission, + IdentityInterface $identity = null, + $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 => \Laminas\ServiceManager\Factory\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' => \LmcRbac\Assertion\AssertionSet::CONDITION_OR, + \My\Namespace\AssertionC::class, + \My\Namespace\AssertionD::class, + ], + 'delete' => [ + 'condition' => \LmcRbac\Assertion\AssertionSet::CONDITION_OR, + \My\Namespace\AssertionE::class, + [ + 'condition' => \LmcRbac\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. + diff --git a/docs/docs/authorization-service.md b/docs/docs/authorization-service.md new file mode 100644 index 0000000..a8914e9 --- /dev/null +++ b/docs/docs/authorization-service.md @@ -0,0 +1,33 @@ +--- +sidebar_label: Authorization service +sidebar_position: 5 +title: Authorization Service +--- + +### Usage + +The Authorization service can be retrieved from the service manager using the name +`LmcRbac\Service\AuthorizationServiceInterface` and injected into your code: + +```php +get(LmcRbac\Service\AuthorizationServiceInterface::class); + +``` +### Reference + +`LmcRbac\Service\AuthorizationServiceInterface` defines the following method: + +`isGranted(?IdentityInterface $identity, string $permission, $context = null): bool` + +| 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 | + +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. + diff --git a/docs/docs/concepts.md b/docs/docs/concepts.md new file mode 100644 index 0000000..f49a88d --- /dev/null +++ b/docs/docs/concepts.md @@ -0,0 +1,44 @@ +--- +sidebar_label: Concepts +sidebar_position: 2 +title: 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 providing 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. + + + + diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md new file mode 100644 index 0000000..9c1d32d --- /dev/null +++ b/docs/docs/configuration.md @@ -0,0 +1,18 @@ +--- +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'`. | +| `assertion_map` | Defines the dynamic assertions that are associated to permissions.
Defaults to `[]`.
See the [Dynamic Assertions](assertions) section. | +| `role_provider` | Defines the role provider.
Defaults to `[]`
See the [Role Providers](role-providers) section. | diff --git a/docs/docs/gettingstarted.md b/docs/docs/gettingstarted.md new file mode 100644 index 0000000..293ff0e --- /dev/null +++ b/docs/docs/gettingstarted.md @@ -0,0 +1,34 @@ +--- +sidebar_label: Getting Started +sidebar_position: 1 +title: Get started +--- +## Requirements + +- PHP 7.3 or higher + +:::warning +The code is continuously tested against PHP 8.1 and higher only. There is no warranty that it will work for PHP 8.0 and lower. +::: + +## 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 `LmcRbac` 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. diff --git a/docs/docs/installation.md b/docs/docs/installation.md deleted file mode 100644 index aee58ab..0000000 --- a/docs/docs/installation.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -sidebar_label: Requirements and Installation -sidebar_position: 2 ---- -# Requirements and Installation -## Requirements - -- PHP 7.3 or higher - - -## Installation - -LmcRbac only officially supports installation through Composer. For Composer documentation, please refer to -[getcomposer.org](http://getcomposer.org/). - -Install the module: - -```sh -$ composer require lm-commons/lmc-rbac -``` - -Enable the module by adding `LmcRbac` key to your `application.config.php` file. Customize the module by copy-pasting -the `config.global.php` file to your `config/autoload` folder. - -You can also find some Doctrine entities in the [data](https://github.com/LM-Commons/LmcRbac/tree/master/data) folder that will help you to more quickly take advantage -of LmcRbac. diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md deleted file mode 100644 index d1c74e2..0000000 --- a/docs/docs/introduction.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -sidebar_position: 1 ---- -# Introduction -Role-based access control module to provide additional features on top of Laminas\Permissions\Rbac - -Based on [ZF-Commons/zfc-rbac](https://github.com/ZF-Commons/zfc-rbac) v3.x. - -If you are looking for the Laminas version -of zfc-rbac v2, please use [LM-Commons/LmcRbacMvc](https://github.com/LM-Commons/LmcRbacMvc). - -## Support - -- File issues at https://github.com/LM-Commons/LmcRbac/issues. -- Ask questions in the [LM-Commons Slack](https://join.slack.com/t/lm-commons/shared_invite/zt-2gankt2wj-FTS45hp1W~JEj1tWvDsUHQ) chat. diff --git a/docs/docs/migration.md b/docs/docs/migration.md new file mode 100644 index 0000000..c1bca7f --- /dev/null +++ b/docs/docs/migration.md @@ -0,0 +1,22 @@ +--- +sidebar_label: Migration Guide +sidebar_position: 8 +title: Migration Guide +--- + +## 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 + +- 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. diff --git a/docs/docs/quickstart.md b/docs/docs/quickstart.md new file mode 100644 index 0000000..b19cde3 --- /dev/null +++ b/docs/docs/quickstart.md @@ -0,0 +1,129 @@ +--- +sidebar_label: Quick start +sidebar_position: 3 +title: Quick Start +--- + +Once the library has been installed by Composer, you will need to copy the +`config/lmcrbac.global.php` file from `LmcRbac` to the `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' => [ + 'LmcRbac\Role\InMemoryRoleProvider' => [ + '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 service manager container and used to check if a permission +is granted to an identity: + +```php +get('\LmcRbac\Service\AuthorizationServiceInterface'); + + /** @var \LmcRbac\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 `\LmcRbac\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 `\LmcRbac\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('\LmcRbac\Service\AuthorizationServiceInterface'); + + /** @var \LmcRbac\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/docs/role-providers.md b/docs/docs/role-providers.md new file mode 100644 index 0000000..d0103bc --- /dev/null +++ b/docs/docs/role-providers.md @@ -0,0 +1,164 @@ +--- +sidebar_label: Role providers +title: Role providers +sidebar_position: 4 +--- + +A role provider is an object that returns a list of roles. A role provider must implement the +`LmcRbac\Role\RoleProviderInterface` interface. The only required method is `getRoles`, and must return an array +of `LmcRbac\Role\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: `LmcRbac\Role\InMemoryRoleProvider` and `LmcRbac\Role\ObjectRepositoryRoleProvider`. A role +provider must be added to the `role_provider` subkey in the configuration file: + +```php +return [ + 'lmc_rbac' => [ + 'role_provider' => [ + // Role provider config here! + ] + ] +]; +``` + +### `LmcRbac\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' => [ + 'LmcRbac\Role\InMemoryRoleProvider' => [ + '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 `LmcRbac\Role\InMemoryRoleProvider` creates +either a `LmcRbac\Role\Role` object if the role does not have any children, or a `LmcRbac\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' => [ + 'admin' => [ + 'permissions' => [ + 'article.delete', + 'article.edit', + 'article.archive', + 'article.read' + ] + ], + 'member' => [ + 'permissions' => [ + 'article.edit', + 'article.archive', + 'article.read' + ] + ], + 'guest' => [ + 'permissions' => ['article.read'] + ] + ] + ] + ] +]; +``` + +### `LmcRbac\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' => [ + 'LmcRbac\Role\ObjectRepositoryRoleProvider' => [ + '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' => [ + '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 `LmcRbac\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 +`LmcRbac\Role\RoleProviderInterface` interface. + +Then, you need to add it to the role provider manager: + +```php +return [ + 'lmc_rbac' => [ + 'role_provider' => [ + 'Application\Role\CustomRoleProvider' => [ + // Options + ], + ], + ], +]; +``` +And the role provider is created using the service manager: +```php +return [ + 'service_manager' => [ + 'factories' => [ + 'Application\Role\CustomRoleProvider' => 'Application\Factory\CustomRoleProviderFactory' + ], + ], +]; +``` + diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index b8864e5..9f324b0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -9,7 +9,7 @@ import {themes as prismThemes} from 'prism-react-renderer'; /** @type {import('@docusaurus/types').Config} */ const config = { title: 'LmcRbac', - tagline: 'Role-based access control module to provide additional features on top of Laminas\\Permissions\\Rbac', + tagline: 'Role-based access control components for your Laminas or Mezzio application', favicon: 'img/favicon.ico', // Set the production url of your site here @@ -140,7 +140,7 @@ themeConfig: tagName: 'meta', attributes: { name: 'keywords', - content: 'php, LmcUser, Laminas MVC, authentication' + content: 'php, LmcRbac, Laminas MVC, authorization' } } ], diff --git a/docs/src/components/HomepageFeatures/index.js b/docs/src/components/HomepageFeatures/index.js index c1df677..34297ed 100644 --- a/docs/src/components/HomepageFeatures/index.js +++ b/docs/src/components/HomepageFeatures/index.js @@ -1,6 +1,7 @@ import clsx from 'clsx'; import Heading from '@theme/Heading'; import styles from './styles.module.css'; +import Link from "@docusaurus/Link"; const FeatureList = [ { @@ -53,7 +54,31 @@ export default function HomepageFeatures() { return (
-
+
+
+ Introduction +

Components and services to provide role-based access control (RBAC) to your application.

+

LmcRbac can be used in Laminas MVC and in Mezzio applications.

+

Based on the original work of ZF-Commons/zfc-rbac v3.x.

+

If you are looking for the Laminas version of zfc-rbac v2, please use LM-Commons/LmcRbacMvc.

+
+ Get started +
+ Support + + + +
{/*} {FeatureList.map((props, idx) => ( diff --git a/docs/src/components/HomepageFeatures/styles.module.css b/docs/src/components/HomepageFeatures/styles.module.css index b248eb2..f8726e4 100644 --- a/docs/src/components/HomepageFeatures/styles.module.css +++ b/docs/src/components/HomepageFeatures/styles.module.css @@ -9,3 +9,9 @@ height: 200px; width: 200px; } + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index a18205c..0f8c87a 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -1,5 +1,4 @@ import clsx from 'clsx'; -import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; @@ -34,8 +33,8 @@ export default function Home() { const {siteConfig} = useDocusaurusContext(); return ( + title={`LM-Commons LmcRbac`} + description="Role based access controls for Laminas and Mezzio">