├── .gitignore
├── .releaserc.yml
├── src
├── cache
│ ├── CacheHandlerContract.php
│ └── CacheHandler.php
├── exception
│ └── Unauthorized.php
├── traits
│ └── Configurable.php
├── middleware
│ └── Basic.php
├── model
│ └── Rule.php
├── command
│ └── Publish.php
├── facade
│ └── Enforcer.php
├── TauthzService.php
└── adapter
│ └── DatabaseAdapter.php
├── config
├── tauthz-rbac-model.conf
└── tauthz.php
├── tests
├── CommandTest.php
├── TestCase.php
└── DatabaseAdapterTest.php
├── phpunit.xml.dist
├── composer.json
├── .github
└── workflows
│ └── phpunit.yml
├── database
└── migrations
│ └── 20181113071924_create_rules_table.php
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | vendor
3 | .idea
4 | .vscode
5 |
6 | .phpunit*
7 | phpunit.xml
8 |
9 | composer.lock
--------------------------------------------------------------------------------
/.releaserc.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - "@semantic-release/commit-analyzer"
3 | - "@semantic-release/release-notes-generator"
4 | - "@semantic-release/github"
--------------------------------------------------------------------------------
/src/cache/CacheHandlerContract.php:
--------------------------------------------------------------------------------
1 | config('cache.enabled', false)) {
22 | $key = $this->config('cache.key', 'tauthz');
23 | $expire = $this->config('cache.expire', 0);
24 | return $model->cache($key, $expire);
25 | } else {
26 | return $model;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/middleware/Basic.php:
--------------------------------------------------------------------------------
1 | getAuthzIdentifier($request);
23 | if (!$authzIdentifier) {
24 | throw new Unauthorized();
25 | }
26 |
27 | if (!Enforcer::enforce($authzIdentifier, ...$args)) {
28 | throw new Unauthorized();
29 | }
30 |
31 | return $next($request);
32 | }
33 |
34 | public function getAuthzIdentifier(Request $request)
35 | {
36 | return $request->middleware('auth_id');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/CommandTest.php:
--------------------------------------------------------------------------------
1 | refreshApplication();
10 | // delete published files
11 | $this->deletePublishedFiles();
12 | // run command
13 | $this->app->console->call('tauthz:publish');
14 |
15 | $this->assertFileExists($this->app->getRootPath() . '/database/migrations/20181113071924_create_rules_table.php');
16 | $this->assertFileExists(config_path() . 'tauthz-rbac-model.conf');
17 | $this->assertFileExists(config_path() . 'tauthz.php');
18 | }
19 |
20 | protected function deletePublishedFiles()
21 | {
22 | $destination = $this->app->getRootPath() . '/database/migrations';
23 | if (file_exists($destination . '/20181113071924_create_rules_table.php')) {
24 | unlink($destination . '/20181113071924_create_rules_table.php');
25 | rmdir($destination);
26 | }
27 | if (file_exists(config_path() . 'tauthz-rbac-model.conf')) {
28 | unlink(config_path() . 'tauthz-rbac-model.conf');
29 | }
30 | if (file_exists(config_path() . 'tauthz.php')) {
31 | unlink(config_path() . 'tauthz.php');
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/model/Rule.php:
--------------------------------------------------------------------------------
1 | 'int',
34 | 'ptype' => 'string',
35 | 'v0' => 'string',
36 | 'v1' => 'string',
37 | 'v2' => 'string',
38 | 'v3' => 'string',
39 | 'v4' => 'string',
40 | 'v5' => 'string',
41 | ];
42 | /**
43 | * 架构函数
44 | *
45 | * @param array|object $data 数据
46 | */
47 | public function __construct(array|object $data = [])
48 | {
49 | $this->connection = $this->config('database.connection') ?: '';
50 | $this->table = $this->config('database.rules_table');
51 | $this->name = $this->config('database.rules_name');
52 |
53 | parent::__construct($data);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/
14 |
15 |
16 |
17 |
18 | ./src
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "casbin/think-authz",
3 | "keywords": [
4 | "thinkphp",
5 | "casbin",
6 | "permission",
7 | "access-control",
8 | "authorization",
9 | "rbac",
10 | "acl",
11 | "abac",
12 | "authz"
13 | ],
14 | "description": "An authorization library that supports access control models like ACL, RBAC, ABAC for ThinkPHP. ",
15 | "authors": [
16 | {
17 | "name": "TechLee",
18 | "email": "leeqvip@gmail.com"
19 | }
20 | ],
21 | "license": "Apache-2.0",
22 | "require": {
23 | "php": ">=8.0",
24 | "casbin/casbin": "~4.0.2",
25 | "topthink/framework": "^8.0.3",
26 | "topthink/think-migration": "^3.1.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "~9.0",
30 | "php-coveralls/php-coveralls": "^2.7",
31 | "topthink/think": "~8.0"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "tauthz\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "tauthz\\tests\\": "tests/"
41 | }
42 | },
43 | "config": {
44 | "preferred-install": "dist"
45 | },
46 | "extra": {
47 | "think": {
48 | "services": [
49 | "tauthz\\TauthzService"
50 | ]
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/command/Publish.php:
--------------------------------------------------------------------------------
1 | setName('tauthz:publish')->setDescription('Publish Tauthz');
18 | }
19 |
20 | /**
21 | * 执行指令
22 | * @param Input $input
23 | * @param Output $output
24 | * @return null|int
25 | * @throws LogicException
26 | * @see setCode()
27 | */
28 | protected function execute(Input $input, Output $output)
29 | {
30 | $destination = $this->app->getRootPath() . '/database/migrations/';
31 | if(!is_dir($destination)){
32 | mkdir($destination, 0755, true);
33 | }
34 | $source = __DIR__.'/../../database/migrations/';
35 | $handle = dir($source);
36 |
37 | while($entry=$handle->read()) {
38 | if(($entry!=".")&&($entry!="..")){
39 | if(is_file($source.$entry)){
40 | copy($source.$entry, $destination.$entry);
41 | }
42 | }
43 | }
44 |
45 | if (!file_exists(config_path().'tauthz-rbac-model.conf')) {
46 | copy(__DIR__.'/../../config/tauthz-rbac-model.conf', config_path().'tauthz-rbac-model.conf');
47 | }
48 |
49 | if (!file_exists(config_path().'tauthz.php')) {
50 | copy(__DIR__.'/../../config/tauthz.php', config_path().'tauthz.php');
51 | }
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/config/tauthz.php:
--------------------------------------------------------------------------------
1 | 'basic',
8 |
9 | 'log' => [
10 | // changes whether Lauthz will log messages to the Logger.
11 | 'enabled' => false,
12 | // Casbin Logger, Supported: \Psr\Log\LoggerInterface|string
13 | 'logger' => 'log',
14 | ],
15 |
16 | 'enforcers' => [
17 | 'basic' => [
18 | /*
19 | * Model 设置
20 | */
21 | 'model' => [
22 | // 可选值: "file", "text"
23 | 'config_type' => 'file',
24 | 'config_file_path' => config_path().'tauthz-rbac-model.conf',
25 | 'config_text' => '',
26 | ],
27 |
28 | // 适配器 .
29 | 'adapter' => tauthz\adapter\DatabaseAdapter::class,
30 |
31 | /*
32 | * 数据库设置.
33 | */
34 | 'database' => [
35 | // 数据库连接名称,不填为默认配置.
36 | 'connection' => '',
37 | // 策略表名(不含表前缀)
38 | 'rules_name' => 'rules',
39 | // 策略表完整名称.
40 | 'rules_table' => null,
41 | ],
42 |
43 | /*
44 | * 缓存设置.
45 | */
46 | 'cache' => [
47 | // 是否使用缓存
48 | 'enabled' => true,
49 | // 缓存key
50 | 'key' => 'tauthz',
51 | // 缓存有效期 0表示永久缓存
52 | 'expire' => 0,
53 | // 缓存策略
54 | 'handler' => \tauthz\cache\CacheHandler::class
55 | ]
56 | ],
57 | ],
58 | ];
59 |
--------------------------------------------------------------------------------
/src/facade/Enforcer.php:
--------------------------------------------------------------------------------
1 | register(TauthzService::class);
25 |
26 | $app->initialize();
27 |
28 | $app->console->call("tauthz:publish");
29 |
30 | return $app;
31 | }
32 |
33 | /**
34 | * 初始数据
35 | *
36 | * @return void
37 | */
38 | protected function initTable()
39 | {
40 | Rule::where("1 = 1")->delete(true);
41 | Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => 'data1', 'v2' => 'read']);
42 | Rule::create(['ptype' => 'p', 'v0' => 'bob', 'v1' => 'data2', 'v2' => 'write']);
43 | Rule::create(['ptype' => 'p', 'v0' => 'data2_admin', 'v1' => 'data2', 'v2' => 'read']);
44 | Rule::create(['ptype' => 'p', 'v0' => 'data2_admin', 'v1' => 'data2', 'v2' => 'write']);
45 | Rule::create(['ptype' => 'g', 'v0' => 'alice', 'v1' => 'data2_admin']);
46 | }
47 |
48 | /**
49 | * Refresh the application instance.
50 | *
51 | * @return void
52 | */
53 | protected function refreshApplication()
54 | {
55 | $this->app = $this->createApplication();
56 | }
57 |
58 | protected function testing(Closure $closure)
59 | {
60 | $this->_setUp();
61 |
62 | $closure();
63 |
64 | $this->_tearDown();
65 | }
66 |
67 | /**
68 | * This method is called before each test.
69 | */
70 | protected function _setUp()
71 | {
72 | if (!$this->app) {
73 | $this->refreshApplication();
74 | }
75 |
76 | $this->app->console->call("migrate:run");
77 |
78 | $this->initTable();
79 | }
80 |
81 | /**
82 | * This method is called after each test.
83 | */
84 | protected function _tearDown()
85 | {
86 | if ($this->migrate) {
87 | $this->app->console->call("migrate:rollback");
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/TauthzService.php:
--------------------------------------------------------------------------------
1 | app->register(\think\migration\Service::class);
28 |
29 | // 绑定 Casbin决策器
30 | $this->app->bind('enforcer', function () {
31 | $default = $this->app->config->get('tauthz.default');
32 |
33 | $config = $this->app->config->get('tauthz.enforcers.'.$default);
34 | $adapter = $config['adapter'];
35 |
36 | $configType = $config['model']['config_type'];
37 |
38 | $model = new Model();
39 | if ('file' == $configType) {
40 | $model->loadModel($config['model']['config_file_path']);
41 | } elseif ('text' == $configType) {
42 | $model->loadModel($config['model']['config_text']);
43 | }
44 |
45 | if ($logger = $this->app->config->get('tauthz.log.logger')) {
46 | if (is_string($logger)) {
47 | $logger = new DefaultLogger($this->app->make($logger));
48 | }
49 |
50 | Log::setLogger($logger);
51 | }
52 |
53 | return new Enforcer($model, app($adapter), $logger, $this->app->config->get('tauthz.log.enabled', false));
54 | });
55 | }
56 |
57 | /**
58 | * Boot function.
59 | *
60 | * @return void
61 | */
62 | public function boot()
63 | {
64 | $this->mergeConfigFrom(__DIR__.'/../config/tauthz.php', 'tauthz');
65 |
66 | $this->commands(['tauthz:publish' => Publish::class]);
67 | }
68 |
69 | /**
70 | * Merge the given configuration with the existing configuration.
71 | *
72 | * @param string $path
73 | * @param string $key
74 | *
75 | * @return void
76 | */
77 | protected function mergeConfigFrom(string $path, string $key)
78 | {
79 | $config = $this->app->config->get($key, []);
80 |
81 | $this->app->config->set(array_merge(require $path, $config), $key);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: PHPUnit
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 |
9 | services:
10 | mysql:
11 | image: mysql:5.7
12 | env:
13 | MYSQL_ALLOW_EMPTY_PASSWORD: yes
14 | MYSQL_DATABASE: tauthz
15 | ports:
16 | - 3306:3306
17 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
18 |
19 | strategy:
20 | fail-fast: true
21 | matrix:
22 | php: [ 8.0, 8.1, 8.2, 8.3, 8.4 ]
23 | thinkphp: [ "~8.0.3", "^8.1"]
24 | exclude:
25 | - php: 8.4
26 | thinkphp: "~8.0.3"
27 |
28 | name: ThinkPHP${{ matrix.thinkphp }}-PHP${{ matrix.php }}
29 |
30 | steps:
31 | - name: Checkout code
32 | uses: actions/checkout@v3
33 |
34 | - name: Setup PHP
35 | uses: shivammathur/setup-php@v2
36 | with:
37 | php-version: ${{ matrix.php }}
38 | tools: composer:v2
39 | coverage: xdebug
40 |
41 | - name: Validate composer.json and composer.lock
42 | run: composer validate
43 |
44 | - name: Cache Composer packages
45 | id: composer-cache
46 | uses: actions/cache@v3
47 | with:
48 | path: vendor
49 | key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.thinkphp }}-${{ hashFiles('**/composer.lock') }}
50 | restore-keys: |
51 | ${{ runner.os }}-${{ matrix.php }}-${{ matrix.thinkphp }}
52 |
53 | - name: Install dependencies
54 | if: steps.composer-cache.outputs.cache-hit != 'true'
55 | run: |
56 | composer require topthink/framework:${{ matrix.thinkphp }} --no-update --no-interaction
57 | composer install --prefer-dist --no-progress --no-suggest
58 |
59 | - name: Run test suite
60 | run: ./vendor/bin/phpunit
61 |
62 | - name: Run Coveralls
63 | env:
64 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 | COVERALLS_PARALLEL: true
66 | COVERALLS_FLAG_NAME: ${{ runner.os }} - ${{ matrix.php }}
67 | run: |
68 | composer global require php-coveralls/php-coveralls:^2.4
69 | php-coveralls --coverage_clover=build/logs/clover.xml -v
70 |
71 | upload-coverage:
72 | runs-on: ubuntu-latest
73 | needs: [ test ]
74 | steps:
75 | - name: Coveralls Finished
76 | uses: coverallsapp/github-action@master
77 | with:
78 | github-token: ${{ secrets.GITHUB_TOKEN }}
79 | parallel-finished: true
80 |
81 | semantic-release:
82 | runs-on: ubuntu-latest
83 | needs: [ test ]
84 | steps:
85 | - uses: actions/checkout@v3
86 | - uses: actions/setup-node@v3
87 | with:
88 | node-version: 'lts/*'
89 |
90 | - name: Run semantic-release
91 | env:
92 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
93 | run: npx semantic-release
--------------------------------------------------------------------------------
/database/migrations/20181113071924_create_rules_table.php:
--------------------------------------------------------------------------------
1 | getDbConfig();
17 |
18 | $adapter = AdapterFactory::instance()->getAdapter($options['adapter'], $options);
19 |
20 | if ($adapter->hasOption('table_prefix') || $adapter->hasOption('table_suffix')) {
21 | $adapter = AdapterFactory::instance()->getWrapper('prefix', $adapter);
22 | }
23 |
24 | $adapter->connect();
25 |
26 | $this->setAdapter($adapter);
27 | }
28 |
29 | /**
30 | * 获取数据库配置
31 | * @return array
32 | */
33 | protected function getDbConfig(): array
34 | {
35 | $default = config('tauthz.default');
36 | $connection = config("tauthz.enforcers.{$default}.database.connection") ?: config('database.default');
37 |
38 | $config = config("database.connections.{$connection}");
39 |
40 | if (0 == $config['deploy']) {
41 | $dbConfig = [
42 | 'adapter' => $config['type'],
43 | 'host' => $config['hostname'],
44 | 'name' => $config['database'],
45 | 'user' => $config['username'],
46 | 'pass' => $config['password'],
47 | 'port' => $config['hostport'],
48 | 'charset' => $config['charset'],
49 | 'suffix' => $config['suffix'] ?? '',
50 | 'table_prefix' => $config['prefix'],
51 | ];
52 | } else {
53 | $dbConfig = [
54 | 'adapter' => explode(',', $config['type'])[0],
55 | 'host' => explode(',', $config['hostname'])[0],
56 | 'name' => explode(',', $config['database'])[0],
57 | 'user' => explode(',', $config['username'])[0],
58 | 'pass' => explode(',', $config['password'])[0],
59 | 'port' => explode(',', $config['hostport'])[0],
60 | 'charset' => explode(',', $config['charset'])[0],
61 | 'suffix' => explode(',', $config['suffix'] ?? '')[0],
62 | 'table_prefix' => explode(',', $config['prefix'])[0],
63 | ];
64 | }
65 |
66 | $table = config('database.migration_table', 'migrations');
67 |
68 | $dbConfig['migration_table'] = $dbConfig['table_prefix'] . $table;
69 | $dbConfig['version_order'] = Config::VERSION_ORDER_CREATION_TIME;
70 |
71 | return $dbConfig;
72 | }
73 |
74 | /**
75 | * Change Method.
76 | *
77 | * Write your reversible migrations using this method.
78 | *
79 | * More information on writing migrations is available here:
80 | * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
81 | *
82 | * The following commands can be used in this method and Phinx will
83 | * automatically reverse them when rolling back:
84 | *
85 | * createTable
86 | * renameTable
87 | * addColumn
88 | * renameColumn
89 | * addIndex
90 | * addForeignKey
91 | *
92 | * Remember to call "create()" or "update()" and NOT "save()" when working
93 | * with the Table class.
94 | */
95 | public function up()
96 | {
97 | $default = config('tauthz.default');
98 | $table = $this->table(config('tauthz.enforcers.'.$default.'.database.rules_name'));
99 | $table->addColumn('ptype', 'string', ['null' => true])
100 | ->addColumn('v0', 'string', ['null' => true])
101 | ->addColumn('v1', 'string', ['null' => true])
102 | ->addColumn('v2', 'string', ['null' => true])
103 | ->addColumn('v3', 'string', ['null' => true])
104 | ->addColumn('v4', 'string', ['null' => true])
105 | ->addColumn('v5', 'string', ['null' => true])
106 | ->create();
107 | }
108 |
109 | public function down()
110 | {
111 | $default = config('tauthz.default');
112 | $table = $this->table(config('tauthz.enforcers.'.$default.'.database.rules_name'));
113 | $table->drop()->save();
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ThinkPHP Authorization
3 |
4 |
5 |
6 | Think-authz 是一个专为 ThinkPHP 打造的授权(角色和权限控制)工具
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 它基于 [PHP-Casbin](https://github.com/php-casbin/php-casbin), 一个强大的、高效的开源访问控制框架,支持基于`ACL`, `RBAC`, `ABAC`等访问控制模型。
28 |
29 | 在这之前,你需要了解 [Casbin](https://github.com/php-casbin/php-casbin) 的相关知识。
30 |
31 | * [安装](#安装)
32 | * [用法](#用法)
33 | * [快速开始](#快速开始)
34 | * [使用 Enforcer Api](#使用-enforcer-api)
35 | * [使用中间件](#使用中间件)
36 | * [感谢](#thinks)
37 | * [License](#license)
38 |
39 | ## 安装
40 |
41 | 使用`composer`安装:
42 |
43 | ```
44 | composer require casbin/think-authz
45 | ```
46 |
47 | 注册服务,在应用的全局公共文件`service.php`中加入:
48 |
49 | ```php
50 | return [
51 | // ...
52 |
53 | tauthz\TauthzService::class,
54 | ];
55 | ```
56 |
57 | 发布配置文件和数据库迁移文件:
58 |
59 | ```
60 | php think tauthz:publish
61 | ```
62 |
63 | 这将自动生成 `config/tauthz-rbac-model.conf` 和 `config/tauthz.php` 文件。
64 |
65 |
66 | 执行迁移工具(**确保数据库配置信息正确**):
67 |
68 | ```
69 | php think migrate:run
70 | ```
71 |
72 | 这将创建名为 `rules` 的表。
73 |
74 |
75 | ## 用法
76 |
77 | ### 快速开始
78 |
79 | 安装成功后,可以这样使用:
80 |
81 | ```php
82 |
83 | use tauthz\facade\Enforcer;
84 |
85 | // adds permissions to a user
86 | Enforcer::addPermissionForUser('eve', 'articles', 'read');
87 | // adds a role for a user.
88 | Enforcer::addRoleForUser('eve', 'writer');
89 | // adds permissions to a rule
90 | Enforcer::addPolicy('writer', 'articles','edit');
91 |
92 | ```
93 |
94 | 你可以检查一个用户是否拥有某个权限:
95 |
96 | ```php
97 | // to check if a user has permission
98 | if (Enforcer::enforce("eve", "articles", "edit")) {
99 | // permit eve to edit articles
100 | } else {
101 | // deny the request, show an error
102 | }
103 |
104 | ```
105 |
106 | ### 使用 Enforcer Api
107 |
108 | 它提供了非常丰富的 `API`,以促进对 `Policy` 的各种操作:
109 |
110 | 获取所有角色:
111 |
112 | ```php
113 | Enforcer::getAllRoles(); // ['writer', 'reader']
114 | ```
115 |
116 | 获取所有的角色的授权规则:
117 |
118 | ```php
119 | Enforcer::getPolicy();
120 | ```
121 |
122 | 获取某个用户的所有角色:
123 |
124 | ```php
125 | Enforcer::getRolesForUser('eve'); // ['writer']
126 | ```
127 |
128 | 获取某个角色的所有用户:
129 |
130 | ```php
131 | Enforcer::getUsersForRole('writer'); // ['eve']
132 | ```
133 |
134 | 决定用户是否拥有某个角色:
135 |
136 | ```php
137 | Enforcer::hasRoleForUser('eve', 'writer'); // true or false
138 | ```
139 |
140 | 给用户添加角色:
141 |
142 | ```php
143 | Enforcer::addRoleForUser('eve', 'writer');
144 | ```
145 |
146 | 赋予权限给某个用户或角色:
147 |
148 | ```php
149 | // to user
150 | Enforcer::addPermissionForUser('eve', 'articles', 'read');
151 | // to role
152 | Enforcer::addPermissionForUser('writer', 'articles','edit');
153 | ```
154 |
155 | 删除用户的角色:
156 |
157 | ```php
158 | Enforcer::deleteRoleForUser('eve', 'writer');
159 | ```
160 |
161 | 删除某个用户的所有角色:
162 |
163 | ```php
164 | Enforcer::deleteRolesForUser('eve');
165 | ```
166 |
167 | 删除单个角色:
168 |
169 | ```php
170 | Enforcer::deleteRole('writer');
171 | ```
172 |
173 | 删除某个权限:
174 |
175 | ```php
176 | Enforcer::deletePermission('articles', 'read'); // returns false if the permission does not exist (aka not affected).
177 | ```
178 |
179 | 删除某个用户或角色的权限:
180 |
181 | ```php
182 | Enforcer::deletePermissionForUser('eve', 'articles', 'read');
183 | ```
184 |
185 | 删除某个用户或角色的所有权限:
186 |
187 | ```php
188 | // to user
189 | Enforcer::deletePermissionsForUser('eve');
190 | // to role
191 | Enforcer::deletePermissionsForUser('writer');
192 | ```
193 |
194 | 获取用户或角色的所有权限:
195 |
196 | ```php
197 | Enforcer::getPermissionsForUser('eve'); // return array
198 | ```
199 |
200 | 决定某个用户是否拥有某个权限
201 |
202 | ```php
203 | Enforcer::hasPermissionForUser('eve', 'articles', 'read'); // true or false
204 | ```
205 |
206 | 更多 `API` 参考 [Casbin API](https://casbin.org/docs/en/management-api) 。
207 |
208 | ### 使用中间件
209 |
210 | 该扩展包带有一个 `\tauthz\middleware\Basic::class` 中间件:
211 |
212 | ```php
213 | Route::get('news/:id','News/Show')
214 | ->middleware(\tauthz\middleware\Basic::class, ['news', 'read']);
215 | ```
216 |
217 | ### 缓存配置
218 |
219 | 该扩展包通过配置 `config/tauthz.php` 中的 `cache` 选项来开启或关闭缓存,以及配置缓存标识和过期时间。
220 |
221 | 通过继承 `tauthz\cache\CacheHandler` 可以实现自定义缓存策略。例如:
222 |
223 | ```php
224 | class MyCacheHandler extends CacheHandler
225 | {
226 | public function cachePolicies(Rule $model)
227 | {
228 | return $model->cacheAlways('my_cache_key', 3600);
229 | }
230 | }
231 | ```
232 |
233 | 并在 `cache` 配置选项中的`handler`声明此类。
234 |
235 | ## 感谢
236 |
237 | [Casbin](https://github.com/php-casbin/php-casbin),你可以查看全部文档在其 [官网](https://casbin.org/) 上。
238 |
239 | ## License
240 |
241 | This project is licensed under the [Apache 2.0 license](LICENSE).
242 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/adapter/DatabaseAdapter.php:
--------------------------------------------------------------------------------
1 | model = $model;
50 |
51 | $cacheHandlerClass = $this->config('cache.handler', \tauthz\cache\CacheHandler::class);
52 | $this->cacheHandler = new $cacheHandlerClass();
53 | }
54 |
55 | /**
56 | * Filter the rule.
57 | *
58 | * @param array $rule
59 | * @return array
60 | */
61 | public function filterRule(array $rule): array
62 | {
63 | $rule = array_values($rule);
64 |
65 | $i = count($rule) - 1;
66 | for (; $i >= 0; $i--) {
67 | if ($rule[$i] != '' && !is_null($rule[$i])) {
68 | break;
69 | }
70 | }
71 |
72 | return array_slice($rule, 0, $i + 1);
73 | }
74 |
75 | /**
76 | * savePolicyLine function.
77 | *
78 | * @param string $ptype
79 | * @param array $rule
80 | *
81 | * @return void
82 | */
83 | public function savePolicyLine(string $ptype, array $rule): void
84 | {
85 | $col['ptype'] = $ptype;
86 | foreach ($rule as $key => $value) {
87 | $col['v' . strval($key) . ''] = $value;
88 | }
89 |
90 | $this->cacheHandler->cachePolicies($this->model)->insert($col);
91 | }
92 |
93 | /**
94 | * loads all policy rules from the storage.
95 | *
96 | * @param Model $model
97 | */
98 | public function loadPolicy(Model $model): void
99 | {
100 | $rows = $this->cacheHandler->cachePolicies($this->model)->field(['ptype', 'v0', 'v1', 'v2', 'v3', 'v4', 'v5'])
101 | ->select()->toArray();
102 | foreach ($rows as $row) {
103 | $this->loadPolicyArray($this->filterRule($row), $model);
104 | }
105 | }
106 |
107 | /**
108 | * saves all policy rules to the storage.
109 | *
110 | * @param Model $model
111 | */
112 | public function savePolicy(Model $model): void
113 | {
114 | foreach ($model['p'] as $ptype => $ast) {
115 | foreach ($ast->policy as $rule) {
116 | $this->savePolicyLine($ptype, $rule);
117 | }
118 | }
119 |
120 | foreach ($model['g'] as $ptype => $ast) {
121 | foreach ($ast->policy as $rule) {
122 | $this->savePolicyLine($ptype, $rule);
123 | }
124 | }
125 | }
126 |
127 | /**
128 | * adds a policy rule to the storage.
129 | * This is part of the Auto-Save feature.
130 | *
131 | * @param string $sec
132 | * @param string $ptype
133 | * @param array $rule
134 | */
135 | public function addPolicy(string $sec, string $ptype, array $rule): void
136 | {
137 | $this->savePolicyLine($ptype, $rule);
138 | }
139 |
140 | /**
141 | * Adds a policy rules to the storage.
142 | * This is part of the Auto-Save feature.
143 | *
144 | * @param string $sec
145 | * @param string $ptype
146 | * @param string[][] $rules
147 | */
148 | public function addPolicies(string $sec, string $ptype, array $rules): void
149 | {
150 | $cols = [];
151 | $i = 0;
152 |
153 | foreach ($rules as $rule) {
154 | $temp['ptype'] = $ptype;
155 | foreach ($rule as $key => $value) {
156 | $temp['v' . strval($key)] = $value;
157 | }
158 | $cols[$i++] = $temp;
159 | $temp = [];
160 | }
161 |
162 | $this->cacheHandler->cachePolicies($this->model)->insertAll($cols);
163 | }
164 |
165 | /**
166 | * This is part of the Auto-Save feature.
167 | *
168 | * @param string $sec
169 | * @param string $ptype
170 | * @param array $rule
171 | */
172 | public function removePolicy(string $sec, string $ptype, array $rule): void
173 | {
174 | $count = 0;
175 |
176 | $instance = $this->model->where('ptype', $ptype);
177 |
178 | foreach ($rule as $key => $value) {
179 | $instance->where('v' . strval($key), $value);
180 | }
181 |
182 | foreach ($instance->select() as $model) {
183 | if ($this->cacheHandler->cachePolicies($model)->delete()) {
184 | ++$count;
185 | }
186 | }
187 | }
188 |
189 | /**
190 | * Removes policy rules from the storage.
191 | * This is part of the Auto-Save feature.
192 | *
193 | * @param string $sec
194 | * @param string $ptype
195 | * @param string[][] $rules
196 | */
197 | public function removePolicies(string $sec, string $ptype, array $rules): void
198 | {
199 | Db::transaction(function () use ($sec, $ptype, $rules) {
200 | foreach ($rules as $rule) {
201 | $this->removePolicy($sec, $ptype, $rule);
202 | }
203 | });
204 | }
205 |
206 | /**
207 | * @param string $sec
208 | * @param string $ptype
209 | * @param int $fieldIndex
210 | * @param string|null ...$fieldValues
211 | * @return array
212 | * @throws Throwable
213 | */
214 | public function _removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, ?string ...$fieldValues): array
215 | {
216 | $count = 0;
217 | $removedRules = [];
218 |
219 | $instance = $this->model->where('ptype', $ptype);
220 | foreach (range(0, 5) as $value) {
221 | if ($fieldIndex <= $value && $value < $fieldIndex + count($fieldValues)) {
222 | if ('' != $fieldValues[$value - $fieldIndex]) {
223 | $instance->where('v' . strval($value), $fieldValues[$value - $fieldIndex]);
224 | }
225 | }
226 | }
227 |
228 | foreach ($instance->select() as $model) {
229 | $item = $model->hidden(['id', 'ptype'])->toArray();
230 | $item = $this->filterRule($item);
231 | $removedRules[] = $item;
232 | if ($this->cacheHandler->cachePolicies($model)->delete()) {
233 | ++$count;
234 | }
235 | }
236 |
237 | return $removedRules;
238 | }
239 |
240 | /**
241 | * RemoveFilteredPolicy removes policy rules that match the filter from the storage.
242 | * This is part of the Auto-Save feature.
243 | *
244 | * @param string $sec
245 | * @param string $ptype
246 | * @param int $fieldIndex
247 | * @param string|null ...$fieldValues
248 | */
249 | public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, ?string ...$fieldValues): void
250 | {
251 | $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues);
252 | }
253 |
254 | /**
255 | * Updates a policy rule from storage.
256 | * This is part of the Auto-Save feature.
257 | *
258 | * @param string $sec
259 | * @param string $ptype
260 | * @param string[] $oldRule
261 | * @param string[] $newPolicy
262 | */
263 | public function updatePolicy(string $sec, string $ptype, array $oldRule, array $newPolicy): void
264 | {
265 | $instance = $this->model->where('ptype', $ptype);
266 | foreach ($oldRule as $key => $value) {
267 | $instance->where('v' . strval($key), $value);
268 | }
269 | $instance = $instance->find();
270 |
271 | foreach ($newPolicy as $key => $value) {
272 | $column = 'v' . strval($key);
273 | $instance->{$column} = $value;
274 | }
275 |
276 | $instance->save();
277 | }
278 |
279 | /**
280 | * UpdatePolicies updates some policy rules to storage, like db, redis.
281 | *
282 | * @param string $sec
283 | * @param string $ptype
284 | * @param string[][] $oldRules
285 | * @param string[][] $newRules
286 | * @return void
287 | */
288 | public function updatePolicies(string $sec, string $ptype, array $oldRules, array $newRules): void
289 | {
290 | Db::transaction(function () use ($sec, $ptype, $oldRules, $newRules) {
291 | foreach ($oldRules as $i => $oldRule) {
292 | $this->updatePolicy($sec, $ptype, $oldRule, $newRules[$i]);
293 | }
294 | });
295 | }
296 |
297 | /**
298 | * UpdateFilteredPolicies deletes old rules and adds new rules.
299 | *
300 | * @param string $sec
301 | * @param string $ptype
302 | * @param array $newPolicies
303 | * @param integer $fieldIndex
304 | * @param string ...$fieldValues
305 | * @return array
306 | */
307 | public function updateFilteredPolicies(string $sec, string $ptype, array $newPolicies, int $fieldIndex, string ...$fieldValues): array
308 | {
309 | $oldRules = [];
310 | DB::transaction(function () use ($sec, $ptype, $fieldIndex, $fieldValues, $newPolicies, &$oldRules) {
311 | $oldRules = $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues);
312 | $this->addPolicies($sec, $ptype, $newPolicies);
313 | });
314 |
315 | return $oldRules;
316 | }
317 |
318 | /**
319 | * Returns true if the loaded policy has been filtered.
320 | *
321 | * @return bool
322 | */
323 | public function isFiltered(): bool
324 | {
325 | return $this->filtered;
326 | }
327 |
328 | /**
329 | * Sets filtered parameter.
330 | *
331 | * @param bool $filtered
332 | */
333 | public function setFiltered(bool $filtered): void
334 | {
335 | $this->filtered = $filtered;
336 | }
337 |
338 | /**
339 | * Loads only policy rules that match the filter.
340 | *
341 | * @param Model $model
342 | * @param mixed $filter
343 | */
344 | public function loadFilteredPolicy(Model $model, $filter): void
345 | {
346 | $instance = $this->model;
347 |
348 | if (is_string($filter)) {
349 | $instance = $instance->whereRaw($filter);
350 | } elseif ($filter instanceof Filter) {
351 | foreach ($filter->p as $k => $v) {
352 | $where[$v] = $filter->g[$k];
353 | $instance = $instance->where($v, $filter->g[$k]);
354 | }
355 | } elseif ($filter instanceof \Closure) {
356 | $instance = $instance->where($filter);
357 | } else {
358 | throw new InvalidFilterTypeException('invalid filter type');
359 | }
360 | $rows = $instance->select()->hidden(['id'])->toArray();
361 | foreach ($rows as $row) {
362 | $row = array_filter($row, fn ($value) => !is_null($value) && $value !== '');
363 | $line = implode(', ', array_filter($row, fn ($val) => '' != $val && !is_null($val)));
364 |
365 | $this->loadPolicyLine(trim($line), $model);
366 | }
367 |
368 | $this->setFiltered(true);
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/tests/DatabaseAdapterTest.php:
--------------------------------------------------------------------------------
1 | testing(function () {
14 |
15 | $this->assertTrue(Enforcer::enforce('alice', 'data1', 'read'));
16 |
17 | $this->assertFalse(Enforcer::enforce('bob', 'data1', 'read'));
18 | $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write'));
19 |
20 | $this->assertTrue(Enforcer::enforce('alice', 'data2', 'read'));
21 | $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write'));
22 | });
23 | }
24 |
25 | public function testAddPolicy()
26 | {
27 | $this->testing(function () {
28 | $this->assertFalse(Enforcer::enforce('eve', 'data3', 'read'));
29 | Enforcer::addPermissionForUser('eve', 'data3', 'read');
30 | $this->assertTrue(Enforcer::enforce('eve', 'data3', 'read'));
31 | });
32 | }
33 |
34 | public function testAddPolicies()
35 | {
36 | $this->testing(function () {
37 | $policies = [
38 | ['u1', 'd1', 'read'],
39 | ['u2', 'd2', 'read'],
40 | ['u3', 'd3', 'read'],
41 | ];
42 | Enforcer::clearPolicy();
43 | $this->initTable();
44 | $this->assertEquals([], Enforcer::getPolicy());
45 | Enforcer::addPolicies($policies);
46 | $this->assertEquals($policies, Enforcer::getPolicy());
47 | });
48 | }
49 |
50 | public function testSavePolicy()
51 | {
52 | $this->testing(function () {
53 | $this->assertFalse(Enforcer::enforce('alice', 'data4', 'read'));
54 |
55 | $model = Enforcer::getModel();
56 | $model->clearPolicy();
57 | $model->addPolicy('p', 'p', ['alice', 'data4', 'read']);
58 |
59 | $adapter = Enforcer::getAdapter();
60 | $adapter->savePolicy($model);
61 | $this->assertTrue(Enforcer::enforce('alice', 'data4', 'read'));
62 | });
63 | }
64 |
65 | public function testRemovePolicy()
66 | {
67 | $this->testing(function () {
68 | $this->assertFalse(Enforcer::enforce('alice', 'data5', 'read'));
69 |
70 | Enforcer::addPermissionForUser('alice', 'data5', 'read');
71 | $this->assertTrue(Enforcer::enforce('alice', 'data5', 'read'));
72 |
73 | Enforcer::deletePermissionForUser('alice', 'data5', 'read');
74 | $this->assertFalse(Enforcer::enforce('alice', 'data5', 'read'));
75 | });
76 | }
77 |
78 | public function testRemovePolicies()
79 | {
80 | $this->testing(function () {
81 | $this->assertEquals([
82 | ['alice', 'data1', 'read'],
83 | ['bob', 'data2', 'write'],
84 | ['data2_admin', 'data2', 'read'],
85 | ['data2_admin', 'data2', 'write'],
86 | ], Enforcer::getPolicy());
87 |
88 | Enforcer::removePolicies([
89 | ['data2_admin', 'data2', 'read'],
90 | ['data2_admin', 'data2', 'write'],
91 | ]);
92 |
93 | $this->assertEquals([
94 | ['alice', 'data1', 'read'],
95 | ['bob', 'data2', 'write']
96 | ], Enforcer::getPolicy());
97 | });
98 | }
99 |
100 | public function testRemoveFilteredPolicy()
101 | {
102 | $this->testing(function () {
103 | $this->assertTrue(Enforcer::enforce('alice', 'data1', 'read'));
104 | Enforcer::removeFilteredPolicy(1, 'data1');
105 | $this->assertFalse(Enforcer::enforce('alice', 'data1', 'read'));
106 | $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write'));
107 | $this->assertTrue(Enforcer::enforce('alice', 'data2', 'read'));
108 | $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write'));
109 | Enforcer::removeFilteredPolicy(1, 'data2', 'read');
110 | $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write'));
111 | $this->assertFalse(Enforcer::enforce('alice', 'data2', 'read'));
112 | $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write'));
113 | Enforcer::removeFilteredPolicy(2, 'write');
114 | $this->assertFalse(Enforcer::enforce('bob', 'data2', 'write'));
115 | $this->assertFalse(Enforcer::enforce('alice', 'data2', 'write'));
116 | });
117 | }
118 |
119 | public function testUpdatePolicy()
120 | {
121 | $this->testing(function () {
122 | $this->assertEquals([
123 | ['alice', 'data1', 'read'],
124 | ['bob', 'data2', 'write'],
125 | ['data2_admin', 'data2', 'read'],
126 | ['data2_admin', 'data2', 'write'],
127 | ], Enforcer::getPolicy());
128 |
129 | Enforcer::updatePolicy(
130 | ['alice', 'data1', 'read'],
131 | ['alice', 'data1', 'write']
132 | );
133 |
134 | Enforcer::updatePolicy(
135 | ['bob', 'data2', 'write'],
136 | ['bob', 'data2', 'read']
137 | );
138 |
139 | $this->assertEquals([
140 | ['alice', 'data1', 'write'],
141 | ['bob', 'data2', 'read'],
142 | ['data2_admin', 'data2', 'read'],
143 | ['data2_admin', 'data2', 'write'],
144 | ], Enforcer::getPolicy());
145 | });
146 | }
147 |
148 | public function testUpdatePolicies()
149 | {
150 | $this->testing(function () {
151 | $this->assertEquals([
152 | ['alice', 'data1', 'read'],
153 | ['bob', 'data2', 'write'],
154 | ['data2_admin', 'data2', 'read'],
155 | ['data2_admin', 'data2', 'write'],
156 | ], Enforcer::getPolicy());
157 |
158 | $oldPolicies = [
159 | ['alice', 'data1', 'read'],
160 | ['bob', 'data2', 'write']
161 | ];
162 | $newPolicies = [
163 | ['alice', 'data1', 'write'],
164 | ['bob', 'data2', 'read']
165 | ];
166 |
167 | Enforcer::updatePolicies($oldPolicies, $newPolicies);
168 |
169 | $this->assertEquals([
170 | ['alice', 'data1', 'write'],
171 | ['bob', 'data2', 'read'],
172 | ['data2_admin', 'data2', 'read'],
173 | ['data2_admin', 'data2', 'write'],
174 | ], Enforcer::getPolicy());
175 | });
176 | }
177 |
178 | public function arrayEqualsWithoutOrder(array $expected, array $actual)
179 | {
180 | if (method_exists($this, 'assertEqualsCanonicalizing')) {
181 | $this->assertEqualsCanonicalizing($expected, $actual);
182 | } else {
183 | array_multisort($expected);
184 | array_multisort($actual);
185 | $this->assertEquals($expected, $actual);
186 | }
187 | }
188 |
189 | public function testUpdateFilteredPolicies()
190 | {
191 | $this->testing(function () {
192 | $this->assertEquals([
193 | ['alice', 'data1', 'read'],
194 | ['bob', 'data2', 'write'],
195 | ['data2_admin', 'data2', 'read'],
196 | ['data2_admin', 'data2', 'write'],
197 | ], Enforcer::getPolicy());
198 |
199 | Enforcer::updateFilteredPolicies([["alice", "data1", "write"]], 0, "alice", "data1", "read");
200 | Enforcer::updateFilteredPolicies([["bob", "data2", "read"]], 0, "bob", "data2", "write");
201 |
202 | $policies = [
203 | ['alice', 'data1', 'write'],
204 | ['bob', 'data2', 'read'],
205 | ['data2_admin', 'data2', 'read'],
206 | ['data2_admin', 'data2', 'write']
207 | ];
208 |
209 | $this->arrayEqualsWithoutOrder($policies, Enforcer::getPolicy());
210 |
211 | // test use updateFilteredPolicies to update all policies of a user
212 | $this->initTable();
213 | Enforcer::loadPolicy();
214 | $policies = [
215 | ['alice', 'data2', 'write'],
216 | ['bob', 'data1', 'read']
217 | ];
218 | Enforcer::addPolicies($policies);
219 |
220 | $this->arrayEqualsWithoutOrder([
221 | ['alice', 'data1', 'read'],
222 | ['bob', 'data2', 'write'],
223 | ['data2_admin', 'data2', 'read'],
224 | ['data2_admin', 'data2', 'write'],
225 | ['alice', 'data2', 'write'],
226 | ['bob', 'data1', 'read']
227 | ], Enforcer::getPolicy());
228 |
229 | Enforcer::updateFilteredPolicies([['alice', 'data1', 'write'], ['alice', 'data2', 'read']], 0, 'alice');
230 | Enforcer::updateFilteredPolicies([['bob', 'data1', 'write'], ["bob", "data2", "read"]], 0, 'bob');
231 |
232 | $policies = [
233 | ['alice', 'data1', 'write'],
234 | ['alice', 'data2', 'read'],
235 | ['bob', 'data1', 'write'],
236 | ['bob', 'data2', 'read'],
237 | ['data2_admin', 'data2', 'read'],
238 | ['data2_admin', 'data2', 'write']
239 | ];
240 |
241 | $this->arrayEqualsWithoutOrder($policies, Enforcer::getPolicy());
242 |
243 | // test if $fieldValues contains empty string
244 | $this->initTable();
245 | Enforcer::loadPolicy();
246 | $policies = [
247 | ['alice', 'data2', 'write'],
248 | ['bob', 'data1', 'read']
249 | ];
250 | Enforcer::addPolicies($policies);
251 |
252 | $this->assertEquals([
253 | ['alice', 'data1', 'read'],
254 | ['bob', 'data2', 'write'],
255 | ['data2_admin', 'data2', 'read'],
256 | ['data2_admin', 'data2', 'write'],
257 | ['alice', 'data2', 'write'],
258 | ['bob', 'data1', 'read']
259 | ], Enforcer::getPolicy());
260 |
261 | Enforcer::updateFilteredPolicies([['alice', 'data1', 'write'], ['alice', 'data2', 'read']], 0, 'alice', '', '');
262 | Enforcer::updateFilteredPolicies([['bob', 'data1', 'write'], ["bob", "data2", "read"]], 0, 'bob', '', '');
263 |
264 | $policies = [
265 | ['alice', 'data1', 'write'],
266 | ['alice', 'data2', 'read'],
267 | ['bob', 'data1', 'write'],
268 | ['bob', 'data2', 'read'],
269 | ['data2_admin', 'data2', 'read'],
270 | ['data2_admin', 'data2', 'write']
271 | ];
272 |
273 | $this->arrayEqualsWithoutOrder($policies, Enforcer::getPolicy());
274 |
275 | // test if $fieldIndex is not zero
276 | $this->initTable();
277 | Enforcer::loadPolicy();
278 | $policies = [
279 | ['alice', 'data2', 'write'],
280 | ['bob', 'data1', 'read']
281 | ];
282 | Enforcer::addPolicies($policies);
283 |
284 | $this->assertEquals([
285 | ['alice', 'data1', 'read'],
286 | ['bob', 'data2', 'write'],
287 | ['data2_admin', 'data2', 'read'],
288 | ['data2_admin', 'data2', 'write'],
289 | ['alice', 'data2', 'write'],
290 | ['bob', 'data1', 'read']
291 | ], Enforcer::getPolicy());
292 |
293 | Enforcer::updateFilteredPolicies([['alice', 'data1', 'edit'], ['bob', 'data1', 'edit']], 2, 'read');
294 | Enforcer::updateFilteredPolicies([['alice', 'data2', 'read'], ["bob", "data2", "read"]], 2, 'write');
295 |
296 | $policies = [
297 | ['alice', 'data1', 'edit'],
298 | ['alice', 'data2', 'read'],
299 | ['bob', 'data1', 'edit'],
300 | ['bob', 'data2', 'read'],
301 | ];
302 |
303 | $this->arrayEqualsWithoutOrder($policies, Enforcer::getPolicy());
304 | });
305 | }
306 |
307 | public function testLoadFilteredPolicy()
308 | {
309 | $this->testing(function () {
310 | $this->initTable();
311 | Enforcer::clearPolicy();
312 | $adapter = Enforcer::getAdapter();
313 | $adapter->setFiltered(true);
314 | $this->assertEquals([], Enforcer::getPolicy());
315 |
316 | // invalid filter type
317 | try {
318 | $filter = ['alice', 'data1', 'read'];
319 | Enforcer::loadFilteredPolicy($filter);
320 | $e = InvalidFilterTypeException::class;
321 | $this->fail("Expected exception $e not thrown");
322 | } catch (InvalidFilterTypeException $e) {
323 | $this->assertEquals("invalid filter type", $e->getMessage());
324 | }
325 |
326 | // string
327 | $filter = "v0 = 'bob'";
328 | Enforcer::loadFilteredPolicy($filter);
329 | $this->assertEquals([
330 | ['bob', 'data2', 'write']
331 | ], Enforcer::getPolicy());
332 |
333 | // Filter
334 | $filter = new Filter(['v2'], ['read']);
335 | Enforcer::loadFilteredPolicy($filter);
336 | $this->assertEquals([
337 | ['alice', 'data1', 'read'],
338 | ['data2_admin', 'data2', 'read'],
339 | ], Enforcer::getPolicy());
340 |
341 | // Closure
342 | Enforcer::loadFilteredPolicy(function ($query) {
343 | $query->where('v1', 'data1');
344 | });
345 |
346 | $this->assertEquals([
347 | ['alice', 'data1', 'read'],
348 | ], Enforcer::getPolicy());
349 | });
350 | }
351 |
352 | public function testCachePolicies()
353 | {
354 | $this->testing(function () {
355 | $this->initTable();
356 | Enforcer::loadPolicy();
357 | // time cost if cache is enabled
358 | $start = microtime(true);
359 | Enforcer::loadPolicy();
360 | $end = microtime(true);
361 | $timeEnabled = $end - $start;
362 | // time cost if cache is disabled
363 | $driver = config('tauthz.default');
364 | config(['enforcers' => [$driver => [
365 | 'cache' => ['enabled' => false],
366 | 'database' => ['rules_name' => 'rules']
367 | ]]], 'tauthz');
368 | $start = microtime(true);
369 | Enforcer::loadPolicy();
370 | $end = microtime(true);
371 | $timeDisabled = $end - $start;
372 | // ensure time cost is not greater than time cost if cache is disabled
373 | $this->assertTrue($timeEnabled < $timeDisabled);
374 | });
375 | }
376 | }
377 |
--------------------------------------------------------------------------------