From 46fb978aa6ab2587ac1c04c693b1bd79a51963e2 Mon Sep 17 00:00:00 2001 From: "Eric Richer eric.richer@vistoconsulting.com" Date: Tue, 10 Sep 2024 09:02:57 -0400 Subject: [PATCH 1/4] Added .gitattributes --- .gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..10d6160b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/docs/ export-ignore +/phpcs.xml export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml export-ignore +/psalm.baseline.xml export-ignore +/test/ export-ignore +/autoload-dev/ export-ignore From 065cbb3efeb7f4839abd73712462832ab9c895e2 Mon Sep 17 00:00:00 2001 From: "Eric Richer eric.richer@vistoconsulting.com" Date: Tue, 10 Sep 2024 09:15:49 -0400 Subject: [PATCH 2/4] Excluding /docs from phpcs --- phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml b/phpcs.xml index 1e7a85d4..657b3b54 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,6 +14,7 @@ config src tests + docs/* From 1cd3a3efdf980fc9623a92fd9b38bb4f32bdd9df Mon Sep 17 00:00:00 2001 From: "Eric Richer eric.richer@vistoconsulting.com" Date: Tue, 10 Sep 2024 09:16:42 -0400 Subject: [PATCH 3/4] Prepping for v4 --- composer.json | 4 +- composer.lock | 136 +++++++++++++++++++++++++------------------------- 2 files changed, 69 insertions(+), 71 deletions(-) diff --git a/composer.json b/composer.json index 07bc1a83..66de4017 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "laminas/laminas-eventmanager": "^3.0", "laminas/laminas-mvc": "^3.0", "laminas/laminas-servicemanager": "^3.0", - "lm-commons/lmc-rbac": "2.0.x-dev" + "lm-commons/lmc-rbac": "^2.0" }, "require-dev": { "laminas/laminas-authentication": "^2.2", @@ -49,7 +49,7 @@ "vimeo/psalm": "^5.25" }, "suggest": { - "lm-commons/lmc-rbac-mvc-devtools": "if you want to show information about the roles" + "lm-commons/lmc-rbac-mvc-devtools": "if you want to collect and show information about roles and guards in Laminas Developer Tools" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 07ac9c80..ff7bd43c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2dfd98c98ec16588d853064be96558d9", + "content-hash": "0d01083c8acf717abd4cd4ef1afb8f3c", "packages": [ { "name": "brick/varexporter", @@ -1300,16 +1300,16 @@ }, { "name": "lm-commons/lmc-rbac", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/LM-Commons/LmcRbac.git", - "reference": "ae7b1c67126d5c7a1c8da7fb17ec209b5ac70164" + "reference": "7f2fc3389d2d08e8b3342a21db5dcca0a515ab88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LM-Commons/LmcRbac/zipball/ae7b1c67126d5c7a1c8da7fb17ec209b5ac70164", - "reference": "ae7b1c67126d5c7a1c8da7fb17ec209b5ac70164", + "url": "https://api.github.com/repos/LM-Commons/LmcRbac/zipball/7f2fc3389d2d08e8b3342a21db5dcca0a515ab88", + "reference": "7f2fc3389d2d08e8b3342a21db5dcca0a515ab88", "shasum": "" }, "require": { @@ -1386,9 +1386,9 @@ ], "support": { "issues": "https://github.com/LM-Commons/LmcRbac/issues", - "source": "https://github.com/LM-Commons/LmcRbac/tree/2.0.x" + "source": "https://github.com/LM-Commons/LmcRbac/tree/2.0.0" }, - "time": "2024-08-27T20:02:07+00:00" + "time": "2024-09-10T00:15:43+00:00" }, { "name": "nikic/php-parser", @@ -2401,16 +2401,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -2450,7 +2450,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -2458,7 +2458,7 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "laminas/laminas-authentication", @@ -2653,16 +2653,16 @@ }, { "name": "netresearch/jsonmapper", - "version": "v4.4.1", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", "shasum": "" }, "require": { @@ -2698,9 +2698,9 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" }, - "time": "2024-01-31T06:18:54+00:00" + "time": "2024-09-08T10:13:13+00:00" }, { "name": "phar-io/manifest", @@ -4809,16 +4809,16 @@ }, { "name": "symfony/console", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9" + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", + "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", "shasum": "" }, "require": { @@ -4882,7 +4882,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.3" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -4898,7 +4898,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5035,20 +5035,20 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -5094,7 +5094,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -5110,24 +5110,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -5172,7 +5172,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -5188,24 +5188,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -5253,7 +5253,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -5269,24 +5269,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -5333,7 +5333,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -5349,7 +5349,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", @@ -5436,16 +5436,16 @@ }, { "name": "symfony/string", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ea272a882be7f20cad58d5d78c215001617b7f07", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { @@ -5503,7 +5503,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.3" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -5519,7 +5519,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:25:37+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "theseer/tokenizer", @@ -5573,16 +5573,16 @@ }, { "name": "vimeo/psalm", - "version": "5.25.0", + "version": "5.26.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505" + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", "shasum": "" }, "require": { @@ -5603,7 +5603,7 @@ "felixfbecker/language-server-protocol": "^1.5.2", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.16", + "nikic/php-parser": "^4.17", "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", @@ -5679,7 +5679,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-06-16T15:08:35+00:00" + "time": "2024-09-08T18:53:08+00:00" }, { "name": "webimpress/coding-standard", @@ -5797,14 +5797,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "lm-commons/lmc-rbac": 20 - }, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } From 9e611c8fc7ea8ac29305bd7c5cdb438961e805f4 Mon Sep 17 00:00:00 2001 From: "Eric Richer eric.richer@vistoconsulting.com" Date: Tue, 10 Sep 2024 09:20:19 -0400 Subject: [PATCH 4/4] Prepping docs for v4 --- docs/docusaurus.config.js | 6 + .../version-4.0/Guides/debugging.md | 26 + .../version-4.0/Guides/example.md} | 97 +--- .../version-4.0/Guides/identity-providers.md | 25 + .../version-4.0/Guides/images/guards.png | Bin 0 -> 29230 bytes .../version-4.0/Guides/images/roles.png | Bin 0 -> 12818 bytes .../version-4.0/Guides/images/settings.png | Bin 0 -> 11611 bytes .../version-4.0/Guides/lmcuser.md | 119 +++++ .../version-4.0/Upgrading/upgrade.md | 4 + .../version-4.0/configuration.md | 25 + docs/versioned_docs/version-4.0/guards.md | 477 ++++++++++++++++++ .../images/workflow-with-guards.png | Bin 0 -> 32236 bytes .../images/workflow-without-guards.png | Bin 0 -> 8905 bytes docs/versioned_docs/version-4.0/intro.md | 218 ++++++++ docs/versioned_docs/version-4.0/plugins.md | 51 ++ docs/versioned_docs/version-4.0/strategies.md | 143 ++++++ .../using-the-authorization-service.md | 39 ++ .../version-4.0-sidebars.json | 8 + docs/versions.json | 1 + 19 files changed, 1166 insertions(+), 73 deletions(-) create mode 100644 docs/versioned_docs/version-4.0/Guides/debugging.md rename docs/{docs/cookbook.md.bak => versioned_docs/version-4.0/Guides/example.md} (88%) create mode 100644 docs/versioned_docs/version-4.0/Guides/identity-providers.md create mode 100644 docs/versioned_docs/version-4.0/Guides/images/guards.png create mode 100644 docs/versioned_docs/version-4.0/Guides/images/roles.png create mode 100644 docs/versioned_docs/version-4.0/Guides/images/settings.png create mode 100644 docs/versioned_docs/version-4.0/Guides/lmcuser.md create mode 100644 docs/versioned_docs/version-4.0/Upgrading/upgrade.md create mode 100644 docs/versioned_docs/version-4.0/configuration.md create mode 100644 docs/versioned_docs/version-4.0/guards.md create mode 100644 docs/versioned_docs/version-4.0/images/workflow-with-guards.png create mode 100644 docs/versioned_docs/version-4.0/images/workflow-without-guards.png create mode 100644 docs/versioned_docs/version-4.0/intro.md create mode 100644 docs/versioned_docs/version-4.0/plugins.md create mode 100644 docs/versioned_docs/version-4.0/strategies.md create mode 100644 docs/versioned_docs/version-4.0/using-the-authorization-service.md create mode 100644 docs/versioned_sidebars/version-4.0-sidebars.json diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 72849592..fada0167 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -45,6 +45,12 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/lm-commons/lmcrbacmvc/tree/master/docs/', + includeCurrentVersion: false, + versions: { + "3.4": { + banner: 'none', + } + } }, blog: { showReadingTime: true, diff --git a/docs/versioned_docs/version-4.0/Guides/debugging.md b/docs/versioned_docs/version-4.0/Guides/debugging.md new file mode 100644 index 00000000..95166a85 --- /dev/null +++ b/docs/versioned_docs/version-4.0/Guides/debugging.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 4 +sidebar_label: Debugging Tools +--- + +# Debugging Tools + +[LmcRbacMvcDeveloperTools](https://github.com/LM-Commons/LmcRbacMvcDeveloperTools) is an extension +for the [Laminas Developer Tools](https://github.com/laminas/laminas-developer-tools), +that collects and displays debugging data on settings, guards and roles. + +## Installation + +```shell +$ composer require --dev lm-commons/lmc-rbac-mvc-devtools +``` + +Composer should ask to install the module. Typically, this module will go in `development.config.php`. + +## Toolbar + +LmcRbacMvcDeveloperTools provides toolbars to view settings, guards and roles: + +![Settings](images/settings.png) +![Guards](images/guards.png) +![Roles](images/roles.png) diff --git a/docs/docs/cookbook.md.bak b/docs/versioned_docs/version-4.0/Guides/example.md similarity index 88% rename from docs/docs/cookbook.md.bak rename to docs/versioned_docs/version-4.0/Guides/example.md index e5d5ecbb..a6ce3610 100644 --- a/docs/docs/cookbook.md.bak +++ b/docs/versioned_docs/version-4.0/Guides/example.md @@ -1,12 +1,7 @@ --- -sidebar_position: 8 +title: A Real World Example +sidebar_position: 1 --- -# Cookbook - -This section will help you further understand how LmcRbacMvc 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 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 @@ -15,9 +10,9 @@ cover the `delete`-methods of both parts. Let's start by creating a controller that has the `PostService` as dependency: ```php -class PostController +class PostController extends \Laminas\Mvc\Controller\AbstractActionController { - protected $postService; + protected PostService $postService; public function __construct(PostService $postService) { @@ -28,7 +23,7 @@ class PostController public function deleteAction() { - $id = $this->params()->fromQuery('id'); + $id = $this->params()->fromRoute('id'); $this->postService->deletePost($id); @@ -47,10 +42,9 @@ class Module return [ 'controllers' => [ 'factories' => [ - 'PostController' => function ($cpm) { - // We assume a Service key 'PostService' here that returns the PostService Class + 'PostController' => function ($container) { return new PostController( - $cpm->getServiceLocator()->get('PostService') + $container->get('PostService') ); }, ], @@ -95,9 +89,9 @@ class Module { return [ 'factories' => [ - 'PostService' => function($sm) { + 'PostService' => function($container) { return new PostService( - $sm->get('doctrine.entitymanager.orm_default') + $container->get('doctrine.entitymanager.orm_default') ); } ] @@ -121,7 +115,7 @@ Assuming the application example above we can easily use LmcRbacMvc to protect o return [ 'lmc_rbac' => [ 'guards' => [ - 'LmcRbacMvc\Guard\RouteGuard' => [ + \Lmc\Rbac\Mvc\Guard\RouteGuard::class => [ 'post/delete' => ['admin'] ] ] @@ -153,7 +147,7 @@ permissions before doing anything wrong. So let's modify our previously created ```php use Doctrine\Persistence\ObjectManager; -use LmcRbacMvc\Service\AuthorizationService; +use Lmc\Rbac\Mvc\Service\AuthorizationService; class PostService { @@ -163,10 +157,10 @@ class PostService public function __construct( ObjectManager $objectManager, - AuthorizationService $autorizationService + AuthorizationService $authorizationService ) { $this->objectManager = $objectManager; - $this->authorizationService = $autorizationService; + $this->authorizationService = $authorizationService; } public function deletePost($id) @@ -197,7 +191,7 @@ class Module 'PostService' => function($sm) { return new PostService( $sm->get('doctrine.entitymanager.orm_default'), - $sm->get('LmcRbacMvc\Service\AuthorizationService') // This is new! + $sm->get('Lmc\Rbac\Mvc\Service\AuthorizationService') // This is new! ); } ] @@ -270,7 +264,7 @@ You can quickly reject access to all admin routes using the following guard: return [ 'lmc_rbac' => [ 'guards' => [ - 'LmcRbacMvc\Guard\RouteGuard' => [ + 'Lmc\Rbac\Mvc\Guard\RouteGuard' => [ 'admin*' => ['admin'] ] ] @@ -295,10 +289,10 @@ class PostService public function __construct( ObjectManager $objectManager, - AuthorizationService $autorizationService + AuthorizationService $authorizationService ) { $this->objectManager = $objectManager; - $this->authorizationService = $autorizationService; + $this->authorizationService = $authorizationService; } public function deletePost($id) @@ -319,7 +313,7 @@ As we can see, we check within our Service if the User of our Application is all 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` and here is how to do it: +`AssertionPluginManager` and here is how to do it: First of all we need to write an Assertion. The Assertion will return a boolean statement about the current identity being the owner of the post. @@ -327,8 +321,8 @@ identity being the owner of the post. ```php namespace Your\Namespace; -use LmcRbacMvc\Assertion\AssertionInterface; -use LmcRbacMvc\Service\AuthorizationService; +use Lmc\Rbac\Mvc\Assertion\AssertionInterface; +use Lmc\Rbac\Mvc\Service\AuthorizationService; class MustBeAuthorAssertion implements AssertionInterface { @@ -448,8 +442,8 @@ The assertion must therefore be modified like this: ```php namespace Your\Namespace; -use LmcRbacMvc\Assertion\AssertionInterface; -use LmcRbacMvc\Service\AuthorizationService; +use Lmc\Rbac\Mvc\Assertion\AssertionInterface; +use Lmc\Rbac\Mvc\Service\AuthorizationService; class MustBeAuthorAssertion implements AssertionInterface { @@ -507,11 +501,11 @@ In this last example, the menu item will be hidden for users who don't have the ## Using LmcRbacMvc with Doctrine ORM -First your User entity class must implement `LmcRbacMvc\Identity\IdentityInterface` : +First your User entity class must implement `Lmc\Rbac\Mvc\Identity\IdentityInterface` : ```php use LmccUser\Entity\User as LmcUserEntity; -use LmcRbacMvc\Identity\IdentityInterface; +use Lmc\Rbac\Mvc\Identity\IdentityInterface; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -721,47 +715,4 @@ public function hasPermission($permission) > NOTE: This is only supported starting from Doctrine ORM 2.5! -## Using LmcRbacMvc and ZF2 Assetic - -To use [Assetic](https://github.com/widmogrod/zf2-assetic-module) with LmcRbacMvc guards, you should modify your -`module.config.php` using the following configuration: - -```php -return [ - 'assetic_configuration' => [ - 'acceptableErrors' => [ - \LmcRbacMvc\Guard\GuardInterface::GUARD_UNAUTHORIZED - ] - ] -]; -``` - -## Using LmcRbacMvc and LmcUser -To use the authentication service from LmcUser, just add the following alias in your application.config.php: - -```php -return [ - 'service_manager' => [ - 'aliases' => [ - 'Laminas\Authentication\AuthenticationService' => 'lmcuser_auth_service' - ] - ] -]; -``` - -Finally add the LmcUser routes to your `guards`: - -```php -return [ - 'lmc_rbac' => [ - 'guards' => [ - 'LmcRbac\Guard\RouteGuard' => [ - 'lmcuser/login' => ['guest'], - 'lmcuser/register' => ['guest'], // required if registration is enabled - 'lmcuser*' => ['user'] // includes logout, changepassword and changeemail - ] - ] - ] -]; -``` diff --git a/docs/versioned_docs/version-4.0/Guides/identity-providers.md b/docs/versioned_docs/version-4.0/Guides/identity-providers.md new file mode 100644 index 00000000..e910eb9f --- /dev/null +++ b/docs/versioned_docs/version-4.0/Guides/identity-providers.md @@ -0,0 +1,25 @@ +--- +title: Create a custom identity provider +sidebar_label: Custom Identity Providers +sidebar_position: 2 +--- + +Identity providers return the current identity. Most of the time, this means the logged in user. LmcRbacMvc comes with a +default identity provider (`Lmc\Rbac\Mvc\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 +`Lmc\Rbac\Mvc\Identity\IdentityProviderInterface` class. Then, change the `identity_provider` option in LmcRbacMvc config, +as shown below: + +```php +return [ + 'lmc_rbac' => [ + 'identity_provider' => 'MyCustomIdentityProvider' + ] +]; +``` + +The identity provider is automatically pulled from the service manager. diff --git a/docs/versioned_docs/version-4.0/Guides/images/guards.png b/docs/versioned_docs/version-4.0/Guides/images/guards.png new file mode 100644 index 0000000000000000000000000000000000000000..c008496e6712f2c0928fb641f7f356e6944d7323 GIT binary patch literal 29230 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-4.0/Guides/lmcuser.md b/docs/versioned_docs/version-4.0/Guides/lmcuser.md new file mode 100644 index 00000000..c9b33bfb --- /dev/null +++ b/docs/versioned_docs/version-4.0/Guides/lmcuser.md @@ -0,0 +1,119 @@ +--- +title: Using LmcRbacMvc and LmcUser +sidebar_label: Using LmcRbacMvc and LmcUser +sidebar_position: 3 +--- + +To use the authentication service from LmcUser, there are a few actions to take: + +1. Set up a user entity that supports roles +2. Set up the Laminas authentication service to use LmcUser +3. Set up guards +4. Optionally, set up a redirect strategy + +## Set up the user entity + +The default user entity class in LmcUser does not implement the `Lmc\Rbac\Identity\IdentityInterface`. You will need +to define a user identity class that does by, for example, extending the default user class: + +```php +roles; + } + + public function setRoles(array $roles): User + { + $this->roles = $roles; + return $this; + } +} +```` + +and then set LmcUser to use this entity: + +```php +return [ + 'lmc_user' => [ + 'user_entity_class' => CustomUser::class, + ] +]; +``` +:::note +This example does not cover modifying the database tables and mappers to fetch/hydrate roles into the user object. +::: + +## Set up the Laminas authentication service to use LmcUser + +You need to set up Laminas Authentication to use the Authentication service from LmcUser. + +Add the following alias in your `application.config.php`: + +```php +return [ + 'service_manager' => [ + 'aliases' => [ + 'Laminas\Authentication\AuthenticationService' => 'lmcuser_auth_service' + ] + ] +]; +``` + +## Set up guards + +The login and register pages should be only accessible from 'guest' users and all other pages should be accessible from +logged in users. + +Add the LmcUser routes to your `guards`: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + \Lmc\Rbac\Mvc\Guard\RouteGuard::class => [ + 'lmcuser/login' => ['guest'], + 'lmcuser/register' => ['guest'], // required if registration is enabled + 'lmcuser*' => ['user'] // includes logout, changepassword and changeemail + ] + ] + ] +]; +``` + +## Set up a redirect strategy + +If you would like for your application to redirect users to the login page when an unauthorized access is detected, then +you need to setup the `RedirectStrategy`: + +In your application.config.php: +```php + +return [ + 'listeners' => [ + \Lmc\Rbac\Mvc\View\Strategy\RedirectStrategy::class, + ], +]; +``` + +In your LmcRbac config file + +```php +return [ + 'lmc_rbac' => [ + 'redirect_strategy' => [ + 'redirect_when_connected' => true, + 'redirect_to_route_connected' => 'home', + 'redirect_to_route_disconnected' => 'lmcuser/login', + 'append_previous_uri' => true, + 'previous_uri_query_key' => 'redirectTo' + ], + ] +]; +``` + diff --git a/docs/versioned_docs/version-4.0/Upgrading/upgrade.md b/docs/versioned_docs/version-4.0/Upgrading/upgrade.md new file mode 100644 index 00000000..33be67bf --- /dev/null +++ b/docs/versioned_docs/version-4.0/Upgrading/upgrade.md @@ -0,0 +1,4 @@ +--- +title: Upgrading from v3 to v4 +sidebar_label: From v3 to v4 +--- diff --git a/docs/versioned_docs/version-4.0/configuration.md b/docs/versioned_docs/version-4.0/configuration.md new file mode 100644 index 00000000..7c2f6668 --- /dev/null +++ b/docs/versioned_docs/version-4.0/configuration.md @@ -0,0 +1,25 @@ +--- +sidebar_label: Configuration +sidebar_position: 5 +title: Configuring LmcRbacMvc +--- + +LmcRbacMvc 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. + +:::warning +LmcRbacMvc and LmcRbac share the same config key `'lmc_rbac'`. This allow to configure LmcRbacMvc by adding its specific +configs to the same configuration file as LmcRbac. +::: + +## Reference + +| Key | Type | Values | Description | +|-------------------------|----------------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `guards` | array | `[]` | Defines the guards to implement.
Refer to the section on [Guards](guards.md). | +| 'protection_policy' | string | - `GuardInterface::POLICY_ALLOW` **(default)**
- `GuardInterface::POLICY_DENY` | Defines the protection policy that guards will use when no rules are specified for the role.
Refer to the [Guards](guards.md#protection-policy) section. | +| 'unauthorized_strategy' | array map\|null | Defaults to `null` | Defines the configuration for the UnAuthorized strategy.
Refer to the [Unauthorized Strategy](strategies#unauthorizedstrategy) section. | +| 'redirect_strategy' | array map\|null | Defaults to `null` | Defines the configuration for the Redirect strategy.
Refer to the [Redirect Strategy](strategies#redirectstrategy) section. | +| 'guard_manager' | array map | `[]` **(default)** | Guard Manager Plugin Manager configuration.
Must follow a Service Manager configuration. | diff --git a/docs/versioned_docs/version-4.0/guards.md b/docs/versioned_docs/version-4.0/guards.md new file mode 100644 index 00000000..111050ff --- /dev/null +++ b/docs/versioned_docs/version-4.0/guards.md @@ -0,0 +1,477 @@ +--- +sidebar_position: 2 +title: Guards +--- +In this section, you will learn: + +* What guards are +* How to use and configure built-in guards +* How to create custom guards + +## What are guards and when should you use them? + +Guards are listeners that are registered on a specific event of +the MVC workflow. They allow your application to quickly mark a request as unauthorized. + +Here is a simple workflow without guards: + +![Laminas Framework workflow without guards](images/workflow-without-guards.png?raw=true) + +And here is a simple workflow with a route guard: + +![Laminas Framework workflow with guards](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 +protect your services as well. 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 a check only on the specified guard rules. Any route or controller +that is 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 explicitly 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 LmcRbacMvc config, as follows: + +```php +use LmcRbacMvc\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 + +LmcRbacMvc 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 Framework 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 uses 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 your application to protect a route or a hierarchy of routes. You must provide an array of "key" => "value", +where the key is a route pattern and the value is an array of role names: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\Guard\RouteGuard' => [ + 'admin*' => ['admin'], + 'login' => ['guest'] + ] + ] + ] +]; +``` + +> Only one role in a rule needs 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' => [ + 'LmcRbacMvc\Guard\RouteGuard' => [ + 'home' => ['*'] + ] + ] + ] +]; +``` + +This rule grants access to the "home" route to anyone. + +Finally, you can also omit the roles array to completely block a route, for maintenance purpose for example : + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\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' => [ + 'LmcRbacMvc\Guard\RouteGuard' => [ + 'route_under_construction' => [] + ] + ] + ] +]; +``` + + +### RoutePermissionsGuard + +> The RoutePermissionsGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -8. + +The RoutePermissionsGuard allows your application to protect a route or a hierarchy of routes. You must provide an array of "key" => "value", +where the key is a route pattern and the value is an array of permission names: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\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 LmcRbacMvc\Guard\GuardInterface; + +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\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' => [ + 'LmcRbacMvc\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' => [ + 'LmcRbacMvc\Guard\RoutePermissionsGuard' => [ + 'route_under_construction' => [] + ] + ] + ] +]; +``` + +This route will be inaccessible. + + +### ControllerGuard + +> The ControllerGuard listens to the `MvcEvent::EVENT_ROUTE` event with a priority of -10. + +The ControllerGuard allows your application to protect a controller. You must provide an array of arrays: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\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' => [ + 'LmcRbacMvc\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 follows: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\Guard\ControllerGuard' => [ + [ + 'controller' => 'PostController', + 'roles' => ['member'] + ], + [ + 'controller' => 'PostController', + 'actions' => ['delete'], + 'roles' => ['admin'] + ] + ] + ] + ] +]; +``` + +These rules grant access to each controller action 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 your application to protect a controller using permissions. You must provide an array of arrays: + +```php +return [ + 'lmc_rbac' => [ + 'guards' => [ + 'LmcRbacMvc\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 action 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 in your controller, those guards will not intercept and check requests (because internally +Laminas MVC 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 + +LmcRbacMvc is flexible enough to allow you to create custom guards. 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 LmcRbacMvc\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; + } + + $clientIp = $_SERVER['REMOTE_ADDR']; + + return !in_array($clientIp, $this->ipAddresses); + } +} +``` + +> Guards must implement `LmcRbacMvc\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 checks it against the blacklist. + +However, for this to work, we must register the newly created guard with 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 configuration format. + +Now, let's create the factory: + +```php +namespace Application\Factory; + +use Application\Guard\IpGuard; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class IpGuardFactory implements FactoryInterface +{ + /** + * {@inheritDoc} + */ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + if (null === $options) { + $options = []; + } + return new IpGuard($options); + } +} +``` + +In a real use case, you would likely fetched the blacklist from a database. + +Now we just need to add the guard to the `guards` option, so that LmcRbacMvc can 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' + ] + ] + ] +]; +``` +The array of IP addresses will be passed to `IpGuardFactory::__invoke` in the `$options` parameter. diff --git a/docs/versioned_docs/version-4.0/images/workflow-with-guards.png b/docs/versioned_docs/version-4.0/images/workflow-with-guards.png new file mode 100644 index 0000000000000000000000000000000000000000..3c3d21799b3b02eececb149bcecf20bcc1f41966 GIT binary patch literal 32236 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
-mk-h^4|VO^29iL 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-4.0/images/workflow-without-guards.png b/docs/versioned_docs/version-4.0/images/workflow-without-guards.png new file mode 100644 index 0000000000000000000000000000000000000000..fd49abadd8e3f151c4b9c330eaa9099175e0bcea GIT binary patch literal 8905 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 literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-4.0/intro.md b/docs/versioned_docs/version-4.0/intro.md new file mode 100644 index 00000000..0a0fb3af --- /dev/null +++ b/docs/versioned_docs/version-4.0/intro.md @@ -0,0 +1,218 @@ +--- +title: Getting Started +sidebar_label: Getting Started +sidebar_position: 1 +--- + +LmcRbacMvc is a companion component that extends the functionality of LmcRbac to provide Role-based Access Control +(RBAC) for Laminas MVC applications. + +LmcRbacMvc provides additional features on top of LmcRbac that are suitable for a Laminas MVC application: + +- Guards that acts like a firewall allowing access to routes, controllers and actions to authorized users. +- Strategies to execute when unauthorized access occurs such as redirection and error responses +- Extensions to LmcRbac Authorization service such as controller and view plugins + +:::tip[Important Note:] + +If you are migrating from v3, there are breaking changes to take into account. See the [Upgrading](Upgrading/upgrade.md) section for details. +::: + +## Extending LmcRbac + +LmcRbacMvc extends the functionality of LmcRbac to support Laminas MVC applications. + +It is highly recommended to first go through the concepts and usage of LmcRbac before using the functionalities of +LmcRbacMvc. + + + +## Requirements + +- PHP 8.1 or higher +- LmcRbac v2 (installed by default) + +### Optional requirements + +- [LmcRbacMvcDeveloperTools](https://github.com/LM-Commons/LmcRbacMvcDeveloperTools): a companion component to Laminas Developer Tools to collect and display LmcRbcMvc data + +## Installation + +Install the module using Composer: + +```sh +$ composer require lm-commons/lmc-rbac-mvc:^4.0 +``` + +You will be prompted by the `laminas-component-installer` plugin to inject LM-Commons\LmcRbacMvc. + +:::note[Manual installation] + +Enable the module by adding `Lmc\Rbac\Mvc` key to your `application.config.php` or `modules.config.php` file. +::: + +Customize the module by copy-pasting the `lmc_rbac.global.php.dist` file to your `config/autoload` folder. + +:::warning +LmcRbac and LmcRbacMvc share the same config key `'lmc_rbac'`. Be careful when creating configuration files to avoid +overriding configuration. + +It is recommended that have one configuration file containing both LmcRbac and LmcRbacMvc configuration. This makes it +easier to keep all configuration in one place. +::: + +## Quick Start + +Before you start configuring LmcRbacMvc, you must set up LmcRbac first. Please follow the [instructions](https://lm-commons.github.io/LmcRbac/docs/gettingstarted) in LmcRbac +documentation. + +### Specifying an identity provider + +By default, LmcRbacMvc 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 providing a factory on your configuration. + +For example, in `module.config.php` file: + +```php +return [ + 'service_manager' => [ + 'factories' => [ + \Laminas\Authentication\AuthenticationService::class => function($container) { + // Create your authentication service! + } + ] + ] +]; +``` + +The identity given by `Laminas\Authentication\AuthenticationService` must implement `Lmc\Rbac\Identity\IdentityInterface`. + +:::warning +Note that the default identity provided with Laminas does not implement this interface. +::: + +:::tip +If you are also using the [LmcUser](https://github.com/lm-commons/lmcuser) package, then the `Laminas\Authentication\AuthenticationService` will be +provided for you and there is no need to implement your own. + +:::warning +LmcUser's default User entity does not implement the `IdentityInteface` that is required +by LmcRbac. +::: + +LmcRbacMvc is flexible enough to use something other than the built-in `AuthenticationService`, by specifying custom +identity providers. For more information, refer to the [Create a custom identity provider](Guides/identity-providers.md) +section. + +## Adding a guard + +A guard allows your application 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' => [ + 'Lmc\Rbac\Mvc\Guard\RouteGuard' => [ + 'admin*' => ['admin'] + ] + ] + ] +]; +``` + +LmcRbacMvc has several built-in guards, and you can also register your own guards. For more information, refer +[to this section](guards.md#built-in-guards). + +## Registering a strategy + +When a guard blocks access to a route/controller, or if you throw the `Lmc\Rbac\Mvc\Exception\UnauthorizedException` +exception in your service, LmcRbacMvc automatically performs some logic for you depending on the view strategy used. + +For instance, if you want LmcRbacMvc 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(MvcEvent $event) +{ + $app = $event->getApplication(); + $sm = $app->getServiceManager(); + $em = $app->getEventManager(); + + $listener = $sm->get(\Lmc\Rbac\Mvc\View\Strategy\RedirectStrategy::class); + $listener->attach($em); +} +``` + +or add the listener to 'listeners' config key in a configuration file: + +```php +return [ + // other configs... + + 'listeners' => [ + \Lmc\Rbac\Mvc\View\Strategy\RedirectStrategy::class + ], +]; +``` + + +By default, `RedirectStrategy` redirects all unauthorized requests to a route named "login" when the user is not connected +and to a route named "home" when the user is connected. This is entirely configurable. + +:::warning +For flexibility purposes, LmcRbacMvc **does not** register any strategy for you by default! +::: + +For more information about built-in strategies, refer [to this section](strategies.md#built-in-strategies) +in the [Strategies](strategies.md) section. + +## Using the authorization service + +With LmcRbac and LmcRbacMvc properly configured, you can inject the authorization service into any class and use it to +check if the current identity is granted to do something. + +The LmcRbacMvc Authorization Service is a wrapper to the LmcRbac Authorization Service. + +The difference is in the `isGranted` method: + +```php +public function isGranted(string $permission, mixed $context = null): bool; +``` +where the service will get the identity from the identity provider and there is no need to get it separately. + +The authorization service can be retrieved from the container using the key `Lmc\Rbac\Mvc\Service\AuthorizationServiceInterface`. +Once injected, you can use it as follows: + +```php +use Lmc\Rbac\Mvc\Exception\UnauthorizedException; + +class ActionController extends \Laminas\Mvc\Controller\AbstractActionController { +public function delete() +{ + if (!$this->authorizationService->isGranted('delete')) { + throw new UnauthorizedException(); + } + + // Delete the post +} +} +``` diff --git a/docs/versioned_docs/version-4.0/plugins.md b/docs/versioned_docs/version-4.0/plugins.md new file mode 100644 index 00000000..59d61a58 --- /dev/null +++ b/docs/versioned_docs/version-4.0/plugins.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# Plugins and Helpers + +LmcRbacMvc provides a controller plugin and view helpers to check authorization. + + +## Controller plugins + +#### `isGranted(string $permission, $context=null): bool` + +Basic usage: + +```php + public function doSomethingAction() + { + if (!$this->isGranted('myPermission', $context)) { + // redirect if not granted for example + } + } +``` + +## View Helpers + +#### `isGranted(string $permission, $context=null): bool` + +Basic usage: + +```php + isGranted('myPermission', $myContext)): ?> +
+

Display only if granted

+
+ +``` + +#### `hasRole(array $roleOrRoles): bool` + +Basic usage: + +```php + hasRole(['admin'])): ?> +
+

Display only if user has the admin role

+
+ +``` + + diff --git a/docs/versioned_docs/version-4.0/strategies.md b/docs/versioned_docs/version-4.0/strategies.md new file mode 100644 index 00000000..da41c80c --- /dev/null +++ b/docs/versioned_docs/version-4.0/strategies.md @@ -0,0 +1,143 @@ +--- +title: Strategies +sidebar_position: 3 +--- + +## 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 access to a resource is unauthorized by LmcRbacMvc. + +LmcRbacMvc strategies all check if an `Lmc\Rbac\Mvc\Exception\UnauthorizedExceptionInterface` has been thrown. + +By default, LmcRbacMvc does not register any strategy for you. The best place to register it in a config file under the +`'listeners'` key: + +```php +return [ + // other configs... + + 'listeners' => [ + \Lmc\Rbac\Mvc\View\Strategy\UnauthorizedStrategy::class + ], +]; +``` +## Built-in strategies + +LmcRbacMvc comes with two built-in strategies: +- `\Lmc\Rbac\Mvc\View\Strategy\RedirectStrategy` +- `\Lmc\Rbac\Mvc\View\Strategy\UnauthorizedStrategy`. + +### RedirectStrategy + +This strategy allows your application to redirect any unauthorized request to another route by optionally appending the previous +URL as a query parameter. + +To register it, copy-paste this code into a configuration file: + +```php +return [ + // other configs... + + 'listeners' => [ + \Lmc\Rbac\Mvc\View\Strategy\RedirectStrategy::class + ], +]; +``` + +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 users try to access an unauthorized resource (eg.: http://www.example.com/delete), they will be +redirected to the "login" route if is not connected and to the "home" route otherwise 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 your application to render a template on any unauthorized request. + +To register it, copy-paste this code into your Module.php class: + +```php +return [ + // other configs... + + 'listeners' => [ + \Lmc\Rbac\Mvc\View\Strategy\UnauthorizedStrategy::class + ], +]; +``` + +You can configure the strategy using the `unauthorized_strategy` subkey: + +```php +return [ + 'lmc_rbac' => [ + 'unauthorized_strategy' => [ + 'template' => 'error/custom-403' + ], + ] +]; +``` + +> By default, LmcRbacMvc 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 Api Tools 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 Lmc\Rbac\Mvc\View\Strategy\AbstractStrategy; +use Lmc\Rbac\Mvc\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 +return [ + // other configs... + + 'listeners' => [ + Application\View\Strategy\ApiProblemStrategy::class, + ], +]; +``` diff --git a/docs/versioned_docs/version-4.0/using-the-authorization-service.md b/docs/versioned_docs/version-4.0/using-the-authorization-service.md new file mode 100644 index 00000000..adc3bb96 --- /dev/null +++ b/docs/versioned_docs/version-4.0/using-the-authorization-service.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 4 +sidebar_label: Authorization Service +--- +# Using the Authorization Service + +The LmcRbacMvc Authorization service is a wrapper for the LmcRbac Authorization service. + +`\Lmc\Rbac\Mvc\Service\AuthorizationService` provides the same methods as `\Lmc\Rbac\Service\AuthorizationService` +except for the `isGranted()` method which does not required an identity parameter. + +`\Lmc\Rbac\Mvc\Service\AuthorizationService::isGranted()` will get the identity from the +Laminas Authentication Service directly. + +## Injecting the Authorization Service + +Different techniques for injecting the Authorization Service are described in LmcRbac. The same techniques apply +to LmcRbacMvc using the equivalent wrapper classes and trait: + +- `\Lmc\Rbac\Mvc\Service\AuthorizationServiceAwareInterface` +- `\Lmc\Rbac\Mvc\Service\AuthorizationServiceAwareTrait` +- `\Lmc\Rbac\Mvc\Service\AuthorizationServiceDelegatorFactory` + +Refer to this section in LmcRbac on how to inject the Authorization Service. + +## Permissions and Assertions + +Assertions are provided by LmcRbac. Refer to the Assertion section in LmcRbac on how to configure assertions. + +`\Lmc\Rbac\Mvc\Service\AuthorizationService` provides the same methods as `\Lmc\Rbac\Service\AuthorizationService` to +override assertions at run-time. + +:::warning +If you are upgrading from v3, please not that assertions must now implement the +`Lmc\Rbac\Assertion\AssertionInterface` interface which has a different signature for the `assert()` method. + +Make sure to update your assertions to follow the `Lmc\Rbac\Assertion\AssertionInterface` interface. +::: + diff --git a/docs/versioned_sidebars/version-4.0-sidebars.json b/docs/versioned_sidebars/version-4.0-sidebars.json new file mode 100644 index 00000000..caea0c03 --- /dev/null +++ b/docs/versioned_sidebars/version-4.0-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versions.json b/docs/versions.json index f7ee0375..c0952cec 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,3 +1,4 @@ [ + "4.0", "3.4" ]