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);
+ }
+}