From 9729ba3197e5ae4cdc4cbeb913dd255c2e1ba031 Mon Sep 17 00:00:00 2001 From: Zds <49744633+zds-s@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:39:15 +0800 Subject: [PATCH 1/3] Add components for secure access control --- composer.json | 8 +- phpunit.xml | 1 + .../.github/workflows/close-pull-request.yml | 13 ++ .../.github/workflows/release.yml | 24 ++++ src/SecurityAccess/LICENSE | 21 ++++ src/SecurityAccess/README.md | 1 + src/SecurityAccess/composer.json | 39 ++++++ src/SecurityAccess/publish/access.php | 25 ++++ src/SecurityAccess/publish/rbac_model.conf | 14 +++ src/SecurityAccess/publish/rbac_policy.csv | 0 src/SecurityAccess/src/ConfigProvider.php | 47 ++++++++ src/SecurityAccess/src/Contract/Access.php | 24 ++++ .../src/Exception/AccessException.php | 15 +++ src/SecurityAccess/src/Manager.php | 52 ++++++++ src/SecurityAccess/src/Rbac.php | 36 ++++++ src/SecurityAccess/src/RbacFacade.php | 26 ++++ .../tests/Cases/ConfigProviderTest.php | 28 +++++ .../tests/Cases/ManagerTest.php | 112 ++++++++++++++++++ .../tests/Cases/RbacFacadeTest.php | 29 +++++ src/SecurityAccess/tests/Cases/RbacTest.php | 50 ++++++++ 20 files changed, 563 insertions(+), 2 deletions(-) create mode 100644 src/SecurityAccess/.github/workflows/close-pull-request.yml create mode 100644 src/SecurityAccess/.github/workflows/release.yml create mode 100644 src/SecurityAccess/LICENSE create mode 100644 src/SecurityAccess/README.md create mode 100644 src/SecurityAccess/composer.json create mode 100644 src/SecurityAccess/publish/access.php create mode 100644 src/SecurityAccess/publish/rbac_model.conf create mode 100644 src/SecurityAccess/publish/rbac_policy.csv create mode 100644 src/SecurityAccess/src/ConfigProvider.php create mode 100644 src/SecurityAccess/src/Contract/Access.php create mode 100644 src/SecurityAccess/src/Exception/AccessException.php create mode 100644 src/SecurityAccess/src/Manager.php create mode 100644 src/SecurityAccess/src/Rbac.php create mode 100644 src/SecurityAccess/src/RbacFacade.php create mode 100644 src/SecurityAccess/tests/Cases/ConfigProviderTest.php create mode 100644 src/SecurityAccess/tests/Cases/ManagerTest.php create mode 100644 src/SecurityAccess/tests/Cases/RbacFacadeTest.php create mode 100644 src/SecurityAccess/tests/Cases/RbacTest.php diff --git a/composer.json b/composer.json index 92b399f7..da73ec81 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "Mine\\Crontab\\": "src/Crontab/src", "Mine\\Module\\": "src/Module/src", "Mine\\SecurityBundle\\": "src/SecurityBundle/src", - "Mine\\Security\\Http\\": "src/SecurityHttp/src" + "Mine\\Security\\Http\\": "src/SecurityHttp/src", + "Mine\\Security\\Access\\": "src/SecurityAccess/src" }, "files": [ "src/mine-helpers/src/functions.php" @@ -40,7 +41,8 @@ "Mine\\Crontab\\Tests\\": "src/Crontab/tests", "Mine\\Module\\Tests\\": "src/Module/tests", "Mine\\SecurityBundle\\Tests\\": "src/SecurityBundle/tests", - "Mine\\Security\\Http\\Tests\\": "src/SecurityHttp/tests" + "Mine\\Security\\Http\\Tests\\": "src/SecurityHttp/tests", + "Mine\\Security\\Access\\Tests\\": "src/SecurityAccess/tests" } }, "authors": [ @@ -80,8 +82,10 @@ "ext-redis": "*", "ext-swoole": ">=5.0", "ext-zip": "*", + "casbin/casbin": "^v3.21", "doctrine/dbal": "^3.1", "friendsofhyperf/encryption": "^3.1", + "friendsofhyperf/facade": "^v3.1", "hyperf/amqp": "~3.1.0", "hyperf/async-queue": "~3.1.0", "hyperf/cache": "~3.1.0", diff --git a/phpunit.xml b/phpunit.xml index b76efd04..95d7dcea 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,6 +4,7 @@ ./src/SecurityHttp/tests + ./src/SecurityAccess/tests ./src/SecurityBundle/tests ./src/Module/tests ./src/Crontab/tests diff --git a/src/SecurityAccess/.github/workflows/close-pull-request.yml b/src/SecurityAccess/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..c8182b84 --- /dev/null +++ b/src/SecurityAccess/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [ opened ] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Hi, this is a READ-ONLY repository, please submit your PR on the https://github.com/mineadmin/components repository.

This Pull Request will close automatically.

Thanks! " \ No newline at end of file diff --git a/src/SecurityAccess/.github/workflows/release.yml b/src/SecurityAccess/.github/workflows/release.yml new file mode 100644 index 00000000..2fc8404b --- /dev/null +++ b/src/SecurityAccess/.github/workflows/release.yml @@ -0,0 +1,24 @@ +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Release + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false \ No newline at end of file diff --git a/src/SecurityAccess/LICENSE b/src/SecurityAccess/LICENSE new file mode 100644 index 00000000..c5722457 --- /dev/null +++ b/src/SecurityAccess/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 MineAdmin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/SecurityAccess/README.md b/src/SecurityAccess/README.md new file mode 100644 index 00000000..307e2a5a --- /dev/null +++ b/src/SecurityAccess/README.md @@ -0,0 +1 @@ +# 基于 mineadmin/security-bundle 提供安全访问控制 \ No newline at end of file diff --git a/src/SecurityAccess/composer.json b/src/SecurityAccess/composer.json new file mode 100644 index 00000000..2a06a921 --- /dev/null +++ b/src/SecurityAccess/composer.json @@ -0,0 +1,39 @@ +{ + "name": "mineadmin/security-access", + "description": "Security Access Component", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "xmo", + "email": "root@imoi.cn", + "role": "Developer" + }, + { + "name": "zds", + "email": "2771717608@qq.com", + "role": "Developer" + } + ], + "require": { + "php": ">=8.1", + "mineadmin/security-bundle": "2.0.x-dev", + "casbin/casbin": "^v3.21", + "friendsofhyperf/facade": "^3.1" + }, + "autoload": { + "psr-4": { + "Mine\\Security\\Access\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mine\\Security\\Access\\Tests\\": "tests/" + } + }, + "extra": { + "config": { + "hyperf": "Mine\\Security\\Access\\ConfigProvider" + } + } +} \ No newline at end of file diff --git a/src/SecurityAccess/publish/access.php b/src/SecurityAccess/publish/access.php new file mode 100644 index 00000000..74b12829 --- /dev/null +++ b/src/SecurityAccess/publish/access.php @@ -0,0 +1,25 @@ + 'rbac', + 'component' => [ + 'rbac' => [ + 'construct' => [ + __DIR__ . '/rbac_model.conf', + __DIR__ . '/rbac_policy.csv', + ], + 'enforcer' => Enforcer::class, + ], + ], +]; diff --git a/src/SecurityAccess/publish/rbac_model.conf b/src/SecurityAccess/publish/rbac_model.conf new file mode 100644 index 00000000..71159e38 --- /dev/null +++ b/src/SecurityAccess/publish/rbac_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/src/SecurityAccess/publish/rbac_policy.csv b/src/SecurityAccess/publish/rbac_policy.csv new file mode 100644 index 00000000..e69de29b diff --git a/src/SecurityAccess/src/ConfigProvider.php b/src/SecurityAccess/src/ConfigProvider.php new file mode 100644 index 00000000..a93c4730 --- /dev/null +++ b/src/SecurityAccess/src/ConfigProvider.php @@ -0,0 +1,47 @@ + [ + Access::class => Manager::class, + ], + 'publish' => [ + [ + 'id' => 'access rbac conf', + 'description' => 'Access Rbac Conf', + 'source' => __DIR__ . '/../publish/access.php', + 'destination' => BASE_PATH . '/config/autoload/access.php', + ], + [ + 'id' => 'access rbac model conf', + 'description' => 'Access Rbac Model Conf', + 'source' => __DIR__ . '/../publish/rbac_model.conf', + 'destination' => BASE_PATH . '/config/autoload/rbac_model.conf', + ], + [ + 'id' => 'access rbac policy csv', + 'description' => 'Access Rbac Policy csv', + 'source' => __DIR__ . '/../publish/rbac_policy.csv', + 'destination' => BASE_PATH . '/config/autoload/rbac_policy.csv', + ], + ], + ]; + } +} diff --git a/src/SecurityAccess/src/Contract/Access.php b/src/SecurityAccess/src/Contract/Access.php new file mode 100644 index 00000000..c279ffe5 --- /dev/null +++ b/src/SecurityAccess/src/Contract/Access.php @@ -0,0 +1,24 @@ +getConfig('default'); + } + return $this->getAdapter($name); + } + + protected function getConfig(string $key, mixed $default = null): mixed + { + return $this->config->get('access.' . $key, $default); + } + + protected function getAdapter(string $name): Enforcer + { + $adapter = $this->getConfig('component.' . $name); + if (empty($adapter)) { + throw new AccessException(sprintf('Access adapter [%s] not exists.', $name)); + } + if (empty($adapter['construct']) || empty($adapter['enforcer'])) { + throw new AccessException(sprintf('Access adapter [%s] construct or enforcer not exists.', $name)); + } + $construct = $adapter['construct']; + $enforcer = $adapter['enforcer']; + return new $enforcer(...$construct); + } +} diff --git a/src/SecurityAccess/src/Rbac.php b/src/SecurityAccess/src/Rbac.php new file mode 100644 index 00000000..11b6b7a0 --- /dev/null +++ b/src/SecurityAccess/src/Rbac.php @@ -0,0 +1,36 @@ +getAccess()->get('rbac')->{$name}(...$arguments); + } + + public function getAccess(): Access + { + return $this->access; + } +} diff --git a/src/SecurityAccess/src/RbacFacade.php b/src/SecurityAccess/src/RbacFacade.php new file mode 100644 index 00000000..49a6b86f --- /dev/null +++ b/src/SecurityAccess/src/RbacFacade.php @@ -0,0 +1,26 @@ +assertIsArray((new ConfigProvider())()); + } +} diff --git a/src/SecurityAccess/tests/Cases/ManagerTest.php b/src/SecurityAccess/tests/Cases/ManagerTest.php new file mode 100644 index 00000000..5d02516b --- /dev/null +++ b/src/SecurityAccess/tests/Cases/ManagerTest.php @@ -0,0 +1,112 @@ +config = \Mockery::mock(Config::class); + $this->manager = new Manager($this->config); + } + + public function testGetWithoutName(): void + { + // Set up the expected default value from the config + $expectedDefault = 'rbac'; + $this->config->allows('get') + ->with('access.default', null) + ->andReturn($expectedDefault); + $this->config->allows('get') + ->with('access.component.rbac', null) + ->andReturn([ + 'construct' => [ + dirname(__DIR__, 2) . '/publish/rbac_model.conf', + dirname(__DIR__, 2) . '/publish/rbac_policy.csv', + ], + 'enforcer' => Enforcer::class, + ]); + + // Call the get method without passing a name + $enforcer = $this->manager->get(); + + // Assert that the getAdapter method is called with the expected default value + $this->assertInstanceOf(Enforcer::class, $enforcer); + } + + public function testGetWithName(): void + { + // Set up the expected adapter name and config + $adapterName = 'customAdapter'; + $adapterConfig = [ + 'construct' => [ + dirname(__DIR__, 2) . '/publish/rbac_model.conf', + dirname(__DIR__, 2) . '/publish/rbac_policy.csv', + ], + 'enforcer' => Enforcer::class, + ]; + $this->config->allows('get') + ->with('access.component.' . $adapterName, null) + ->andReturn($adapterConfig); + + // Call the get method with the adapter name + $enforcer = $this->manager->get($adapterName); + + // Assert that the getAdapter method is called with the expected adapter name + $this->assertInstanceOf(Enforcer::class, $enforcer); + } + + public function testGetAdapterWithNonExistentAdapter(): void + { + // Set up the expected adapter name and return null for the config + $adapterName = 'nonExistentAdapter'; + $this->config->allows('get') + ->with('access.component.' . $adapterName, null) + ->andReturn(null); + + // Assert that an AccessException is thrown when the adapter does not exist + $this->expectException(AccessException::class); + $this->manager->get($adapterName); + } + + public function testGetAdapterWithMissingConstructOrEnforcer(): void + { + // Set up the expected adapter name and incomplete config + $adapterName = 'incompleteAdapter'; + $adapterConfig = [ + 'construct' => [], + 'enforcer' => 'IncompleteEnforcerClass', + ]; + $this->config->allows('get') + ->with('access.component.' . $adapterName, null) + ->andReturn($adapterConfig); + + // Assert that an AccessException is thrown when the adapter construct or enforcer is missing + $this->expectException(AccessException::class); + $this->manager->get($adapterName); + } +} diff --git a/src/SecurityAccess/tests/Cases/RbacFacadeTest.php b/src/SecurityAccess/tests/Cases/RbacFacadeTest.php new file mode 100644 index 00000000..34611da8 --- /dev/null +++ b/src/SecurityAccess/tests/Cases/RbacFacadeTest.php @@ -0,0 +1,29 @@ +assertEquals(Rbac::class, RbacFacade::getFacadeRoot()); + } +} diff --git a/src/SecurityAccess/tests/Cases/RbacTest.php b/src/SecurityAccess/tests/Cases/RbacTest.php new file mode 100644 index 00000000..e6adfb4c --- /dev/null +++ b/src/SecurityAccess/tests/Cases/RbacTest.php @@ -0,0 +1,50 @@ +enforcer = \Mockery::mock(Enforcer::class); + + $accessMock->allows('get') + ->with('rbac') + ->andReturn($this->enforcer); + + $this->rbac = new Rbac($accessMock); + } + + public function testCallMethod(): void + { + $methodName = 'testMethod'; + $arguments = ['arg1', 'arg2']; + $this->enforcer->allows($methodName)->with(...$arguments)->andReturn('result'); + $res = $this->rbac->{$methodName}(...$arguments); + $this->assertEquals('result', $res); + } +} From 9c305e80aa2ca884271d369e5aa0a51e122d9252 Mon Sep 17 00:00:00 2001 From: Zds <49744633+zds-s@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:41:40 +0800 Subject: [PATCH 2/3] Update --- CHANGELOG-2.0.md | 3 ++- CHANGELOG-2.0.zh_CN.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-2.0.md b/CHANGELOG-2.0.md index 9c0f287f..dfb6face 100644 --- a/CHANGELOG-2.0.md +++ b/CHANGELOG-2.0.md @@ -7,4 +7,5 @@ - [#53](https://github.com/mineadmin/components/pull/53) Splitting components http-server - [#55](https://github.com/mineadmin/components/pull/55) Splitting the crontab component - [#56](https://github.com/mineadmin/components/pull/56) Splitting modular components -- [#58](https://github.com/mineadmin/components/pull/58) New base Secure Authentication Component \ No newline at end of file +- [#58](https://github.com/mineadmin/components/pull/58) New base Secure Authentication Component +- [#59](https://github.com/mineadmin/components/pull/59) Add components for secure access control \ No newline at end of file diff --git a/CHANGELOG-2.0.zh_CN.md b/CHANGELOG-2.0.zh_CN.md index f96f6752..08844cd4 100644 --- a/CHANGELOG-2.0.zh_CN.md +++ b/CHANGELOG-2.0.zh_CN.md @@ -7,4 +7,5 @@ - [#53](https://github.com/mineadmin/components/pull/53) 拆分组件 http-server - [#55](https://github.com/mineadmin/components/pull/55) 拆分优化组件 crontab - [#56](https://github.com/mineadmin/components/pull/56) 拆分`模块化`组件 -- [#58](https://github.com/mineadmin/components/pull/58) 新增基础的 安全认证组件 \ No newline at end of file +- [#58](https://github.com/mineadmin/components/pull/58) 新增基础的 安全认证组件 +- [#59](https://github.com/mineadmin/components/pull/59) 添加安全访问控制组件 \ No newline at end of file From 1f43442eb2a9dbd9ec1a726b1a54e6bbfbdfbba4 Mon Sep 17 00:00:00 2001 From: Zds <49744633+zds-s@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:50:24 +0800 Subject: [PATCH 3/3] Update --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index 27a0467d..7ef0b50d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -18,6 +18,7 @@ parameters: - src/tests/ - src/app-store/test/ - src/httpServer/tests/ + - src/SecurityAccess/tests/ ignoreErrors: - '#Unsafe usage of new static\(\)#' - '#Call to static method find\(\) on an unknown class#'