diff --git a/.docheader b/.docheader deleted file mode 100644 index fefcd816..00000000 --- a/.docheader +++ /dev/null @@ -1,16 +0,0 @@ -/* - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * This software consists of voluntary contributions made by many individuals - * and is licensed under the MIT license. - */ 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 diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index e49d52b4..00000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,124 +0,0 @@ -setRiskyAllowed(true); - } - - public function getRules(): array - { - return [ - '@PSR2' => true, - '@PHP71Migration' => true, - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => [ - 'default' => 'single_space', - ], - 'blank_line_after_opening_tag' => true, - 'blank_line_after_namespace' => true, - 'blank_line_before_statement' => ['statements' => ['return']], -// 'braces' => true, - 'braces_position' => true, - 'cast_spaces' => true, - 'class_attributes_separation' => ['elements' => ['method' => 'one']], - 'class_definition' => true, - 'combine_consecutive_unsets' => true, - 'concat_space' => false, - 'constant_case' => ['case' => 'lower'], - 'control_structure_braces' => true, - 'control_structure_continuation_position' => true, - 'declare_strict_types' => true, - 'declare_parentheses' => true, - 'echo_tag_syntax' => true, - 'elseif' => true, - 'encoding' => true, - 'full_opening_tag' => true, - 'function_declaration' => true, -// 'function_typehint_space' => true, - 'header_comment' => false, - 'include' => true, - 'indentation_type' => true, - 'linebreak_after_opening_tag' => true, - 'line_ending' => true, - 'lowercase_keywords' => true, - 'method_argument_space' => true, - 'modernize_types_casting' => true, - 'native_function_casing' => true, -// 'new_with_braces' => true, - 'new_with_parentheses' => true, - 'no_alias_functions' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_closing_tag' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_multiline_whitespace_around_double_arrow' => true, - 'no_multiple_statements_per_line' => true, - 'multiline_whitespace_before_semicolons' => true, - 'no_short_bool_cast' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_around_offset' => true, -// 'no_trailing_comma_in_list_call' => true, -// 'no_trailing_comma_in_singleline_array' => true, - 'no_trailing_comma_in_singleline' => true, - 'no_trailing_whitespace_in_comment' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, -// 'no_spaces_inside_parenthesis' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'normalize_index_brace' => true, - 'not_operator_with_successor_space' => true, - 'object_operator_without_whitespace' => true, - 'ordered_imports' => true, - 'phpdoc_indent' => true, - 'general_phpdoc_tag_rename' => true, - 'phpdoc_inline_tag_normalizer' => true, - 'phpdoc_tag_type' => true, - 'psr_autoloading' => true, - 'return_type_declaration' => true, - 'semicolon_after_instruction' => true, - 'short_scalar_cast' => true, - 'simplified_null_return' => false, - 'single_blank_line_at_eof' => true, - 'single_class_element_per_statement' => true, - 'single_import_per_statement' => true, - 'single_line_after_imports' => true, - 'single_line_comment_style' => ['comment_types' => ['hash']], - 'single_quote' => true, - 'single_space_around_construct' => true, - 'spaces_inside_parentheses' => true, - 'standardize_not_equals' => true, - 'statement_indentation' => true, - 'strict_comparison' => true, - 'switch_case_semicolon_to_colon' => true, - 'switch_case_space' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline' => true, - 'trim_array_spaces' => true, - 'type_declaration_spaces' => true, - 'unary_operator_spaces' => true, - 'visibility_required' => true, - 'whitespace_after_comma_in_array' => true, - ]; - } -} - -$config = new Config(); -$config->getFinder()->in(__DIR__)->exclude(['data', 'docs']); - -$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; - -$config->setCacheFile($cacheDir . '/.php_cs.cache'); - -return $config; diff --git a/composer.lock b/composer.lock index c23f0a13..d55cb82d 100644 --- a/composer.lock +++ b/composer.lock @@ -737,16 +737,16 @@ }, { "name": "composer/pcre", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81", - "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { @@ -796,7 +796,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.0" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -812,7 +812,7 @@ "type": "tidelift" } ], - "time": "2024-08-19T19:43:53+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", @@ -1161,16 +1161,16 @@ }, { "name": "doctrine/dbal", - "version": "4.1.0", + "version": "4.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "2377cd41609aa51bee822c8d207317a3f363a558" + "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/2377cd41609aa51bee822c8d207317a3f363a558", - "reference": "2377cd41609aa51bee822c8d207317a3f363a558", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/7a8252418689feb860ea8dfeab66d64a56a64df8", + "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8", "shasum": "" }, "require": { @@ -1183,16 +1183,16 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "1.11.7", + "phpstan/phpstan": "1.12.0", "phpstan/phpstan-phpunit": "1.4.0", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "10.5.28", + "phpunit/phpunit": "10.5.30", "psalm/plugin-phpunit": "0.19.0", "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.10.2", "symfony/cache": "^6.3.8|^7.0", "symfony/console": "^5.4|^6.3|^7.0", - "vimeo/psalm": "5.24.0" + "vimeo/psalm": "5.25.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -1249,7 +1249,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.1.0" + "source": "https://github.com/doctrine/dbal/tree/4.1.1" }, "funding": [ { @@ -1265,7 +1265,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T07:37:07+00:00" + "time": "2024-09-03T08:58:39+00:00" }, { "name": "doctrine/deprecations", @@ -1554,16 +1554,16 @@ }, { "name": "doctrine/orm", - "version": "3.2.1", + "version": "3.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "722cea6536775206e81744542b36fa7c9a4ea3e5" + "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/722cea6536775206e81744542b36fa7c9a4ea3e5", - "reference": "722cea6536775206e81744542b36fa7c9a4ea3e5", + "url": "https://api.github.com/repos/doctrine/orm/zipball/831a1eb7d260925528cdbb49cc1866c0357cf147", + "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147", "shasum": "" }, "require": { @@ -1636,9 +1636,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.2.1" + "source": "https://github.com/doctrine/orm/tree/3.2.2" }, - "time": "2024-06-26T21:48:58+00:00" + "time": "2024-08-23T10:03:52+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -1743,16 +1743,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": { @@ -1792,7 +1792,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": [ { @@ -1800,7 +1800,7 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "laminas/laminas-coding-standard", @@ -1920,16 +1920,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": { @@ -1965,9 +1965,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": "nikic/php-parser", @@ -2796,16 +2796,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.30", + "version": "10.5.33", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b15524febac0153876b4ba9aab3326c2ee94c897" + "reference": "4def7a9cda75af9c2bc179ed53a8e41313e7f7cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b15524febac0153876b4ba9aab3326c2ee94c897", - "reference": "b15524febac0153876b4ba9aab3326c2ee94c897", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4def7a9cda75af9c2bc179ed53a8e41313e7f7cf", + "reference": "4def7a9cda75af9c2bc179ed53a8e41313e7f7cf", "shasum": "" }, "require": { @@ -2819,7 +2819,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.15", + "phpunit/php-code-coverage": "^10.1.16", "phpunit/php-file-iterator": "^4.1.0", "phpunit/php-invoker": "^4.0.0", "phpunit/php-text-template": "^3.0.1", @@ -2877,7 +2877,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.30" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.33" }, "funding": [ { @@ -2893,7 +2893,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T06:09:37+00:00" + "time": "2024-09-09T06:06:56+00:00" }, { "name": "psalm/plugin-phpunit", @@ -4132,16 +4132,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc" + "reference": "36daef8fce88fe0b9a4f8cf4c342ced5c05616dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/6702d2d777260e6ff3451fee2d7d78ab5f715cdc", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc", + "url": "https://api.github.com/repos/symfony/cache/zipball/36daef8fce88fe0b9a4f8cf4c342ced5c05616dc", + "reference": "36daef8fce88fe0b9a4f8cf4c342ced5c05616dc", "shasum": "" }, "require": { @@ -4208,7 +4208,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.10" + "source": "https://github.com/symfony/cache/tree/v6.4.11" }, "funding": [ { @@ -4224,7 +4224,7 @@ "type": "tidelift" } ], - "time": "2024-07-17T06:05:49+00:00" + "time": "2024-08-05T07:40:31+00:00" }, { "name": "symfony/cache-contracts", @@ -4304,16 +4304,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": { @@ -4377,7 +4377,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.3" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -4393,7 +4393,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4530,20 +4530,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": "*" @@ -4589,7 +4589,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": [ { @@ -4605,24 +4605,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" @@ -4667,7 +4667,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": [ { @@ -4683,24 +4683,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" @@ -4748,7 +4748,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": [ { @@ -4764,24 +4764,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": "*" @@ -4828,7 +4828,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": [ { @@ -4844,7 +4844,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", @@ -4931,16 +4931,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": { @@ -4998,7 +4998,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.3" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -5014,7 +5014,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:25:37+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "symfony/var-exporter", @@ -5144,16 +5144,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": { @@ -5174,7 +5174,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", @@ -5250,7 +5250,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", diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 205bf617..5dda5d8f 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 c0b874b7..4a07f4c5 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 00000000..9f86439d --- /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 00000000..b5829389 --- /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 00000000..c43c345b --- /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 00000000..dc86f1ad --- /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 00000000..ee685439 --- /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 00000000..7d940880 --- /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 00000000..3789ebcb --- /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 00000000..a011763e --- /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 00000000..5f41a72e --- /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 df2d5535..904ff979 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,3 +1,4 @@ [ + "2.0", "1.4" ]