├── .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 | Build Status 12 | 13 | 14 | Coverage Status 15 | 16 | 17 | Latest Stable Version 18 | 19 | 20 | Total Downloads 21 | 22 | 23 | License 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 | --------------------------------------------------------------------------------