diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000..c8182b8 --- /dev/null +++ b/.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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2fc8404 --- /dev/null +++ b/.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/LICENSE b/LICENSE new file mode 100644 index 0000000..c572245 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..307e2a5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# 基于 mineadmin/security-bundle 提供安全访问控制 \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2a06a92 --- /dev/null +++ b/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/publish/access.php b/publish/access.php new file mode 100644 index 0000000..74b1282 --- /dev/null +++ b/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/publish/rbac_model.conf b/publish/rbac_model.conf new file mode 100644 index 0000000..71159e3 --- /dev/null +++ b/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/publish/rbac_policy.csv b/publish/rbac_policy.csv new file mode 100644 index 0000000..e69de29 diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000..a93c473 --- /dev/null +++ b/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/Contract/Access.php b/src/Contract/Access.php new file mode 100644 index 0000000..c279ffe --- /dev/null +++ b/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/Rbac.php b/src/Rbac.php new file mode 100644 index 0000000..11b6b7a --- /dev/null +++ b/src/Rbac.php @@ -0,0 +1,36 @@ +getAccess()->get('rbac')->{$name}(...$arguments); + } + + public function getAccess(): Access + { + return $this->access; + } +} diff --git a/src/RbacFacade.php b/src/RbacFacade.php new file mode 100644 index 0000000..49a6b86 --- /dev/null +++ b/src/RbacFacade.php @@ -0,0 +1,26 @@ +assertIsArray((new ConfigProvider())()); + } +} diff --git a/tests/Cases/ManagerTest.php b/tests/Cases/ManagerTest.php new file mode 100644 index 0000000..5d02516 --- /dev/null +++ b/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/tests/Cases/RbacFacadeTest.php b/tests/Cases/RbacFacadeTest.php new file mode 100644 index 0000000..34611da --- /dev/null +++ b/tests/Cases/RbacFacadeTest.php @@ -0,0 +1,29 @@ +assertEquals(Rbac::class, RbacFacade::getFacadeRoot()); + } +} diff --git a/tests/Cases/RbacTest.php b/tests/Cases/RbacTest.php new file mode 100644 index 0000000..e6adfb4 --- /dev/null +++ b/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); + } +}