├── .gitignore ├── src ├── config │ └── plugin │ │ └── casbin │ │ └── webman-permission │ │ ├── app.php │ │ ├── restful-model.conf │ │ ├── rbac-model.conf │ │ └── permission.php ├── Model │ ├── LaravelRuleModel.php │ └── RuleModel.php ├── Install.php ├── Watcher │ └── RedisWatcher.php ├── Permission.php └── Adapter │ ├── LaravelDatabaseAdapter.php │ └── DatabaseAdapter.php ├── workbunny-logo.png ├── tests ├── LaravelDatabase │ ├── config │ │ └── plugin │ │ │ └── casbin │ │ │ └── webman-permission │ │ │ ├── app.php │ │ │ ├── rbac-model.conf │ │ │ └── permission.php │ ├── LaravelDatabaseAdapterTest.php │ └── TestCase.php ├── ThinkphpDatabase │ ├── config │ │ └── plugin │ │ │ └── casbin │ │ │ └── webman-permission │ │ │ ├── app.php │ │ │ ├── rbac-model.conf │ │ │ └── permission.php │ ├── DatabaseAdapterTest.php │ └── TestCase.php ├── config │ ├── container.php │ ├── redis.php │ ├── thinkorm.php │ ├── app.php │ ├── database.php │ └── plugin │ │ └── casbin │ │ └── webman-permission │ │ └── permission.php ├── BasicPermissionTest.php ├── bootstrap-test.php ├── IMPROVEMENT_SUMMARY.md ├── bootstrap.php ├── SimplePermissionTest.php ├── AdapterTest.php ├── EdgeCaseTest.php ├── Adapter.php ├── PermissionTest.php └── IntegrationTest.php ├── test-runner.php ├── phpunit.xml ├── run-simple-tests.php ├── run-basic-tests.php ├── composer.json ├── .github └── workflows │ └── default.yml ├── phpunit.xml.dist ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | .idea 4 | .vscode 5 | .phpunit* 6 | composer.lock 7 | .DS_Store -------------------------------------------------------------------------------- /src/config/plugin/casbin/webman-permission/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /workbunny-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-casbin/webman-permission/HEAD/workbunny-logo.png -------------------------------------------------------------------------------- /tests/LaravelDatabase/config/plugin/casbin/webman-permission/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /tests/ThinkphpDatabase/config/plugin/casbin/webman-permission/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /tests/ThinkphpDatabase/DatabaseAdapterTest.php: -------------------------------------------------------------------------------- 1 | run($argv); 28 | } catch (Exception $e) { 29 | echo "测试运行失败: " . $e->getMessage() . "\n"; 30 | exit(1); 31 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/config/container.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @copyright walkor 12 | * 13 | * @see http://www.workerman.net/ 14 | * 15 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 16 | */ 17 | $builder = new \DI\ContainerBuilder(); 18 | $builder->addDefinitions(config('dependence', [])); 19 | $builder->useAutowiring(true); 20 | $builder->useAnnotations(true); 21 | 22 | return $builder->build(); 23 | -------------------------------------------------------------------------------- /run-simple-tests.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 10 | * 11 | * @copyright walkor 12 | * 13 | * @see http://www.workerman.net/ 14 | * 15 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 16 | */ 17 | 18 | return [ 19 | 'default' => [ 20 | 'host' => env('REDIS_HOST') ?: '127.0.0.1', 21 | 'password' => env('REDIS_PASSWORD') ?: null, 22 | 'port' => env('REDIS_PORT') ?: 6379, 23 | 'database' => env('REDIS_DB') ?: 0, 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /tests/BasicPermissionTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 14 | } 15 | 16 | public function testClassExists() 17 | { 18 | $this->assertTrue(class_exists(Permission::class)); 19 | } 20 | 21 | public function testMethodExists() 22 | { 23 | $this->assertTrue(method_exists(Permission::class, 'addPolicy')); 24 | $this->assertTrue(method_exists(Permission::class, 'enforce')); 25 | $this->assertTrue(method_exists(Permission::class, 'addRoleForUser')); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/config/thinkorm.php: -------------------------------------------------------------------------------- 1 | 'mysql', 5 | 'connections' => [ 6 | 'mysql' => [ 7 | // 数据库类型 8 | 'type' => 'mysql', 9 | // 服务器地址 10 | 'hostname' => env('DB_HOST', '127.0.0.1'), 11 | // 数据库名 12 | 'database' => env('DB_NAME', 'test'), 13 | // 数据库用户名 14 | 'username' => env('DB_USER', 'root'), 15 | // 数据库密码 16 | 'password' => env('DB_PASSWORD', 'test'), 17 | // 数据库连接端口 18 | 'hostport' => env('DB_PORT', 3306), 19 | // 数据库连接参数 20 | 'params' => [], 21 | // 数据库编码默认采用utf8 22 | 'charset' => 'utf8', 23 | // 数据库表前缀 24 | 'prefix' => '', 25 | // 断线重连 26 | 'break_reconnect' => true, 27 | // 关闭SQL监听日志 28 | 'trigger_sql' => false, 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /tests/config/app.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @copyright walkor 12 | * 13 | * @see http://www.workerman.net/ 14 | * 15 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 16 | */ 17 | 18 | use support\Request; 19 | 20 | return [ 21 | 'debug' => true, 22 | 'error_reporting' => E_ALL, 23 | 'default_timezone' => 'Asia/Shanghai', 24 | 'request_class' => Request::class, 25 | 'public_path' => base_path().DIRECTORY_SEPARATOR.'public', 26 | 'runtime_path' => base_path(false).DIRECTORY_SEPARATOR.'runtime', 27 | 'controller_suffix' => 'Controller', 28 | 'controller_reuse' => false, 29 | ]; 30 | -------------------------------------------------------------------------------- /run-basic-tests.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 'basic', 5 | // 基础配置 6 | 'basic' => [ 7 | // 策略模型Model设置 8 | 'model' => [ 9 | 'config_type' => 'file', 10 | 'config_file_path' => __DIR__.'/rbac-model.conf', 11 | 'config_text' => '', 12 | ], 13 | // 适配器 14 | 'adapter' => Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, // ThinkORM 适配器 15 | // 数据库设置 16 | 'database' => [ 17 | 'connection' => '', 18 | 'rules_table' => 'casbin_rule', 19 | 'rules_name' => null, 20 | ], 21 | ], 22 | 'other' => [ 23 | // 策略模型Model设置 24 | 'model' => [ 25 | 'config_type' => 'file', 26 | 'config_file_path' => __DIR__.'/rbac-model.conf', 27 | 'config_text' => '', 28 | ], 29 | // 适配器 30 | 'adapter' => Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, // ThinkORM 适配器 31 | // 数据库设置 32 | 'database' => [ 33 | 'connection' => '', 34 | 'rules_table' => 'other_casbin_rule', 35 | 'rules_name' => null, 36 | ], 37 | ], 38 | ]; 39 | -------------------------------------------------------------------------------- /tests/LaravelDatabase/config/plugin/casbin/webman-permission/permission.php: -------------------------------------------------------------------------------- 1 | 'basic', 5 | // 基础配置 6 | 'basic' => [ 7 | // 策略模型Model设置 8 | 'model' => [ 9 | 'config_type' => 'file', 10 | 'config_file_path' => __DIR__.'/rbac-model.conf', 11 | 'config_text' => '', 12 | ], 13 | // 适配器 14 | 'adapter' => Casbin\WebmanPermission\Adapter\LaravelDatabaseAdapter::class, // Laravel 适配器 15 | // 数据库设置 16 | 'database' => [ 17 | 'connection' => '', 18 | 'rules_table' => 'casbin_rule', 19 | 'rules_name' => null, 20 | ], 21 | ], 22 | 'other' => [ 23 | // 策略模型Model设置 24 | 'model' => [ 25 | 'config_type' => 'file', 26 | 'config_file_path' => __DIR__.'/rbac-model.conf', 27 | 'config_text' => '', 28 | ], 29 | // 适配器 30 | 'adapter' => Casbin\WebmanPermission\Adapter\LaravelDatabaseAdapter::class, // Laravel 适配器 31 | // 数据库设置 32 | 'database' => [ 33 | 'connection' => '', 34 | 'rules_table' => 'other_casbin_rule', 35 | 'rules_name' => null, 36 | ], 37 | ], 38 | ]; 39 | -------------------------------------------------------------------------------- /tests/config/database.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @copyright walkor 12 | * 13 | * @see http://www.workerman.net/ 14 | * 15 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 16 | */ 17 | 18 | return [ 19 | // 默认数据库 20 | 'default' => 'mysql', 21 | 22 | // 各种数据库配置 23 | 'connections' => [ 24 | 'mysql' => [ 25 | 'driver' => 'mysql', 26 | 'host' => env('DB_HOST', '127.0.0.1'), 27 | 'port' => env('DB_PORT', 3306), 28 | 'database' => env('DB_NAME', 'test'), 29 | 'username' => env('DB_USER', 'root'), 30 | 'password' => env('DB_PASSWORD', 'test'), 31 | 'unix_socket' => '', 32 | 'charset' => 'utf8', 33 | 'collation' => 'utf8_unicode_ci', 34 | 'prefix' => '', 35 | 'strict' => true, 36 | 'engine' => null, 37 | ], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casbin/webman-permission", 3 | "keywords": [ 4 | "webman", 5 | "workerman", 6 | "casbin", 7 | "permission", 8 | "access-control", 9 | "authorization", 10 | "rbac", 11 | "acl", 12 | "abac", 13 | "authz" 14 | ], 15 | "description": "webman casbin permission plugin", 16 | "authors": [ 17 | { 18 | "name": "Tinywan", 19 | "email": "756684177@qq.com" 20 | } 21 | ], 22 | "type": "library", 23 | "license": "MIT", 24 | "require": { 25 | "php": ">=8.0", 26 | "casbin/casbin": "~4.0", 27 | "topthink/think-orm": "^3.0", 28 | "php-di/php-di": "^7.0", 29 | "doctrine/annotations": "^2.0", 30 | "workerman/redis": "^2.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Casbin\\WebmanPermission\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Casbin\\WebmanPermission\\Tests\\": "tests/" 40 | } 41 | }, 42 | "require-dev": { 43 | "php-coveralls/php-coveralls": "^2.7", 44 | "workerman/webman": "^1.5||^2.0", 45 | "psr/container": "^1.1.1", 46 | "webman/think-orm": "^1.0", 47 | "phpunit/phpunit": "^10.5", 48 | "webman/database": "^2.1", 49 | "illuminate/pagination": "^12.20", 50 | "illuminate/events": "^12.20", 51 | "symfony/var-dumper": "^7.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/config/plugin/casbin/webman-permission/permission.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'model' => [ 6 | 'config_type' => 'text', 7 | 'config_text' => ' 8 | [request_definition] 9 | r = sub, obj, act 10 | 11 | [policy_definition] 12 | p = sub, obj, act 13 | 14 | [role_definition] 15 | g = _, _ 16 | 17 | [policy_effect] 18 | e = some(where (p.eft == allow)) 19 | 20 | [matchers] 21 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 22 | ', 23 | ], 24 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 25 | ], 26 | 'other' => [ 27 | 'model' => [ 28 | 'config_type' => 'text', 29 | 'config_text' => ' 30 | [request_definition] 31 | r = sub, obj, act 32 | 33 | [policy_definition] 34 | p = sub, obj, act 35 | 36 | [role_definition] 37 | g = _, _ 38 | 39 | [policy_effect] 40 | e = some(where (p.eft == allow)) 41 | 42 | [matchers] 43 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 44 | ', 45 | ], 46 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 47 | 'adapter_config' => [ 48 | 'table' => 'other_casbin_rule' 49 | ], 50 | ], 51 | 'log' => [ 52 | 'enabled' => false, 53 | 'logger' => 'casbin', 54 | 'path' => runtime_path() . '/logs/casbin.log', 55 | ], 56 | ]; -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Default 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.2, 8.3, 8.4] 23 | 24 | name: PHP${{ matrix.php }} 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v3 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | tools: composer:v2 35 | coverage: xdebug 36 | 37 | - name: Validate composer.json and composer.lock 38 | run: composer validate 39 | 40 | - name: Cache Composer packages 41 | id: composer-cache 42 | uses: actions/cache@v3 43 | with: 44 | path: vendor 45 | key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 46 | restore-keys: | 47 | ${{ runner.os }}-${{ matrix.php }} 48 | 49 | - name: Install dependencies 50 | if: steps.composer-cache.outputs.cache-hit != 'true' 51 | run: composer install --prefer-dist --no-progress --no-suggest 52 | 53 | # - name: Run test suite 54 | # run: ./vendor/bin/phpunit -------------------------------------------------------------------------------- /src/Model/LaravelRuleModel.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 48 | $connection = $this->config('database.connection') ?: config('database.default'); 49 | $this->setConnection($connection); 50 | $this->setTable($this->config('database.rules_table')); 51 | parent::__construct($data); 52 | } 53 | 54 | /** 55 | * Gets config value by key. 56 | * 57 | * @param string|null $key 58 | * @param null $default 59 | * 60 | * @return mixed 61 | */ 62 | protected function config(?string $key = null, $default = null) 63 | { 64 | $driver = $this->driver ?? config('plugin.casbin.webman-permission.permission.default'); 65 | return config('plugin.casbin.webman-permission.permission.' . $driver . '.' . $key, $default); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Model/RuleModel.php: -------------------------------------------------------------------------------- 1 | 'int', 27 | 'ptype' => 'string', 28 | 'v0' => 'string', 29 | 'v1' => 'string', 30 | 'v2' => 'string', 31 | 'v3' => 'string', 32 | 'v4' => 'string', 33 | 'v5' => 'string' 34 | ]; 35 | 36 | /** @var string|null $driver */ 37 | protected ?string $driver; 38 | 39 | /** 40 | * 架构函数 41 | * @param array $data 42 | * @param string|null $driver 43 | */ 44 | public function __construct(array $data = [], ?string $driver = null) 45 | { 46 | $this->driver = $driver; 47 | $this->connection = $this->config('database.connection') ?: ''; 48 | $this->table = $this->config('database.rules_table'); 49 | $this->name = $this->config('database.rules_name'); 50 | parent::__construct($data); 51 | } 52 | 53 | /** 54 | * Gets config value by key. 55 | * 56 | * @param string|null $key 57 | * @param null $default 58 | * 59 | * @return mixed 60 | */ 61 | protected function config(string $key = null, $default = null) 62 | { 63 | $driver = $this->driver ?? config('plugin.casbin.webman-permission.permission.default'); 64 | return config('plugin.casbin.webman-permission.permission.' . $driver . '.' . $key, $default); 65 | } 66 | } -------------------------------------------------------------------------------- /src/config/plugin/casbin/webman-permission/permission.php: -------------------------------------------------------------------------------- 1 | 'basic', 9 | /** 日志配置 */ 10 | 'log' => [ 11 | 'enabled' => false, // changes will log messages to the Logger. 12 | 'logger' => 'Casbin', // Casbin Logger, Supported: \Psr\Log\LoggerInterface|string 13 | 'path' => runtime_path() . '/logs/casbin.log' // log path 14 | ], 15 | /** 默认配置 */ 16 | 'basic' => [ 17 | // 策略模型Model设置 18 | 'model' => [ 19 | 'config_type' => 'file', 20 | 'config_file_path' => config_path() . '/plugin/casbin/webman-permission/rbac-model.conf', 21 | 'config_text' => '', 22 | ], 23 | // 适配器 24 | 'adapter' => Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, // ThinkORM 适配器 25 | // 'adapter' => Casbin\WebmanPermission\Adapter\LaravelDatabaseAdapter::class, // Laravel 适配器 26 | // 数据库设置 27 | 'database' => [ 28 | 'connection' => '', 29 | 'rules_table' => 'casbin_rule', 30 | 'rules_name' => null 31 | ], 32 | ], 33 | /** 其他扩展配置,只需要按照基础配置一样,复制一份,指定相关策略模型和适配器即可 */ 34 | 'restful' => [ 35 | 'model' => [ 36 | 'config_type' => 'file', 37 | 'config_file_path' => config_path() . '/plugin/casbin/webman-permission/restful-model.conf', 38 | 'config_text' => '', 39 | ], 40 | 'adapter' => Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, // ThinkORM 适配器 41 | 'database' => [ 42 | 'connection' => '', 43 | 'rules_table' => 'restful_casbin_rule', 44 | 'rules_name' => null 45 | ], 46 | ], 47 | ]; -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | ./src 13 | 14 | 15 | ./vendor 16 | ./tests 17 | 18 | 19 | 20 | 21 | 22 | ./tests 23 | 24 | 25 | 26 | ./tests/AdapterTest.php 27 | ./tests/LaravelDatabase/LaravelDatabaseAdapterTest.php 28 | ./tests/ThinkphpDatabase/DatabaseAdapterTest.php 29 | 30 | 31 | 32 | ./tests/PermissionTest.php 33 | ./tests/Adapter.php 34 | 35 | 36 | 37 | ./tests/EdgeCaseTest.php 38 | 39 | 40 | 41 | ./tests/IntegrationTest.php 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Install.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/casbin/webman-permission', 14 | ); 15 | 16 | /** 17 | * Install 18 | * @return void 19 | */ 20 | public static function install() 21 | { 22 | static::installByRelation(); 23 | } 24 | 25 | /** 26 | * Uninstall 27 | * @return void 28 | */ 29 | public static function uninstall() 30 | { 31 | self::uninstallByRelation(); 32 | } 33 | 34 | /** 35 | * installByRelation 36 | * @return void 37 | */ 38 | public static function installByRelation() 39 | { 40 | foreach (static::$pathRelation as $source => $dest) { 41 | if ($pos = strrpos($dest, '/')) { 42 | $parent_dir = base_path().'/'.substr($dest, 0, $pos); 43 | if (!is_dir($parent_dir)) { 44 | mkdir($parent_dir, 0777, true); 45 | } 46 | } 47 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 48 | copy_dir(__DIR__ . "/$source", base_path()."/$dest"); 49 | echo "Create $dest 50 | "; 51 | } 52 | } 53 | 54 | /** 55 | * uninstallByRelation 56 | * @return void 57 | */ 58 | public static function uninstallByRelation() 59 | { 60 | foreach (static::$pathRelation as $source => $dest) { 61 | $path = base_path()."/$dest"; 62 | if (!is_dir($path) && !is_file($path)) { 63 | continue; 64 | } 65 | echo "Remove $dest 66 | "; 67 | if (is_file($path) || is_link($path)) { 68 | unlink($path); 69 | continue; 70 | } 71 | remove_dir($path); 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /tests/bootstrap-test.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 32 | * 'password' => '', 33 | * 'port' => 6379, 34 | * 'database' => 0, 35 | * 'channel' => '/casbin', 36 | * ] 37 | */ 38 | public function __construct(array $config, string $driver) 39 | { 40 | $this->pubRedis = $this->createRedisClient($config); 41 | $this->subRedis = $this->createRedisClient($config); 42 | $this->channel = ($config['channel'] ?? '/casbin') . '/' . $driver; 43 | 44 | $this->subRedis->subscribe([$this->channel], function ($channel, $message) { 45 | if ($this->callback) { 46 | call_user_func($this->callback); 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * Sets the callback function that the watcher will call when the policy in DB has been changed by other instances. 53 | * A classic callback is loadPolicy() method of Enforcer class. 54 | * 55 | * @param Closure $func 56 | */ 57 | public function setUpdateCallback(Closure $func): void 58 | { 59 | $this->callback = $func; 60 | } 61 | 62 | /** 63 | * Update calls the update callback of other instances to synchronize their policy. 64 | * It is usually called after changing the policy in DB, like savePolicy() method of Enforcer class, 65 | * addPolicy(), removePolicy(), etc. 66 | */ 67 | public function update(): void 68 | { 69 | $this->pubRedis->publish($this->channel, 'casbin rules updated'); 70 | } 71 | 72 | /** 73 | * Close stops and releases the watcher, the callback function will not be called any more. 74 | */ 75 | public function close(): void 76 | { 77 | $this->pubRedis->close(); 78 | $this->subRedis->close(); 79 | } 80 | 81 | /** 82 | * Create redis client 83 | * 84 | * @param array $config 85 | * @return Client 86 | */ 87 | private function createRedisClient(array $config): Client 88 | { 89 | $redis = new Client('redis://' . ($config['host'] ?? '127.0.0.1') . ':' . ($config['port'] ?? 6379)); 90 | $redis->auth($config['password'] ?? ''); 91 | 92 | return $redis; 93 | } 94 | } -------------------------------------------------------------------------------- /tests/ThinkphpDatabase/TestCase.php: -------------------------------------------------------------------------------- 1 | initDb(); 29 | $this->initOtherDb(); 30 | Permission::clear(); 31 | } 32 | 33 | public function initDb() 34 | { 35 | $sql = <<initDb(); 31 | $this->initOtherDb(); 32 | Permission::clear(); 33 | } 34 | 35 | public function initDb() 36 | { 37 | $sql = << 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Dotenv\Dotenv; 16 | use support\Log; 17 | use Webman\Bootstrap; 18 | use Webman\Config; 19 | use Webman\Middleware; 20 | use Webman\Route; 21 | use Webman\Util; 22 | 23 | $worker = $worker ?? null; 24 | 25 | set_error_handler(function ($level, $message, $file = '', $line = 0) { 26 | if (error_reporting() & $level) { 27 | throw new ErrorException($message, 0, $level, $file, $line); 28 | } 29 | }); 30 | 31 | if ($worker) { 32 | register_shutdown_function(function ($startTime) { 33 | if (time() - $startTime <= 0.1) { 34 | sleep(1); 35 | } 36 | }, time()); 37 | } 38 | 39 | if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) { 40 | if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) { 41 | Dotenv::createUnsafeMutable(base_path(false))->load(); 42 | } else { 43 | Dotenv::createMutable(base_path(false))->load(); 44 | } 45 | } 46 | 47 | Config::clear(); 48 | support\App::loadAllConfig(['route']); 49 | if ($timezone = config('app.default_timezone')) { 50 | date_default_timezone_set($timezone); 51 | } 52 | 53 | foreach (config('autoload.files', []) as $file) { 54 | include_once $file; 55 | } 56 | foreach (config('plugin', []) as $firm => $projects) { 57 | foreach ($projects as $name => $project) { 58 | if (!is_array($project)) { 59 | continue; 60 | } 61 | foreach ($project['autoload']['files'] ?? [] as $file) { 62 | include_once $file; 63 | } 64 | } 65 | foreach ($projects['autoload']['files'] ?? [] as $file) { 66 | include_once $file; 67 | } 68 | } 69 | 70 | Middleware::load(config('middleware', [])); 71 | foreach (config('plugin', []) as $firm => $projects) { 72 | foreach ($projects as $name => $project) { 73 | if (!is_array($project) || $name === 'static') { 74 | continue; 75 | } 76 | Middleware::load($project['middleware'] ?? []); 77 | } 78 | Middleware::load($projects['middleware'] ?? [], $firm); 79 | if ($staticMiddlewares = config("plugin.$firm.static.middleware")) { 80 | Middleware::load(['__static__' => $staticMiddlewares], $firm); 81 | } 82 | } 83 | Middleware::load(['__static__' => config('static.middleware', [])]); 84 | 85 | foreach (config('bootstrap', []) as $className) { 86 | if (!class_exists($className)) { 87 | $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n"; 88 | echo $log; 89 | Log::error($log); 90 | continue; 91 | } 92 | /** @var Bootstrap $className */ 93 | $className::start($worker); 94 | } 95 | 96 | foreach (config('plugin', []) as $firm => $projects) { 97 | foreach ($projects as $name => $project) { 98 | if (!is_array($project)) { 99 | continue; 100 | } 101 | foreach ($project['bootstrap'] ?? [] as $className) { 102 | if (!class_exists($className)) { 103 | $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n"; 104 | echo $log; 105 | Log::error($log); 106 | continue; 107 | } 108 | /** @var Bootstrap $className */ 109 | $className::start($worker); 110 | } 111 | } 112 | foreach ($projects['bootstrap'] ?? [] as $className) { 113 | /** @var string $className */ 114 | if (!class_exists($className)) { 115 | $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n"; 116 | echo $log; 117 | Log::error($log); 118 | continue; 119 | } 120 | /** @var Bootstrap $className */ 121 | $className::start($worker); 122 | } 123 | } 124 | 125 | $directory = base_path() . '/plugin'; 126 | $paths = [config_path()]; 127 | foreach (Util::scanDir($directory) as $path) { 128 | if (is_dir($path = "$path/config")) { 129 | $paths[] = $path; 130 | } 131 | } 132 | Route::load($paths); 133 | 134 | -------------------------------------------------------------------------------- /tests/SimplePermissionTest.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'casbin' => [ 20 | 'webman-permission' => [ 21 | 'permission' => [ 22 | 'default' => 'default', 23 | 'drivers' => [ 24 | 'default' => [ 25 | 'model' => [ 26 | 'config_type' => 'text', 27 | 'config_text' => ' 28 | [request_definition] 29 | r = sub, obj, act 30 | 31 | [policy_definition] 32 | p = sub, obj, act 33 | 34 | [role_definition] 35 | g = _, _ 36 | 37 | [policy_effect] 38 | e = some(where (p.eft == allow)) 39 | 40 | [matchers] 41 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 42 | ', 43 | ], 44 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 45 | ], 46 | 'other' => [ 47 | 'model' => [ 48 | 'config_type' => 'text', 49 | 'config_text' => ' 50 | [request_definition] 51 | r = sub, obj, act 52 | 53 | [policy_definition] 54 | p = sub, obj, act 55 | 56 | [role_definition] 57 | g = _, _ 58 | 59 | [policy_effect] 60 | e = some(where (p.eft == allow)) 61 | 62 | [matchers] 63 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 64 | ', 65 | ], 66 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 67 | 'adapter_config' => [ 68 | 'table' => 'other_casbin_rule' 69 | ], 70 | ], 71 | 'log' => [ 72 | 'enabled' => false, 73 | 'logger' => 'casbin', 74 | 'path' => '/tmp/casbin.log', 75 | ], 76 | ] 77 | ] 78 | ] 79 | ] 80 | ]; 81 | 82 | try { 83 | Permission::clear(); 84 | } catch (\Exception $e) { 85 | // 忽略清理时的错误 86 | } 87 | } 88 | 89 | public function testBasicPermission() 90 | { 91 | $result = Permission::addPolicy('writer', 'articles', 'edit'); 92 | $this->assertTrue($result); 93 | 94 | $result = Permission::enforce('writer', 'articles', 'edit'); 95 | $this->assertTrue($result); 96 | 97 | $result = Permission::enforce('writer', 'articles', 'delete'); 98 | $this->assertFalse($result); 99 | } 100 | 101 | public function testRoleManagement() 102 | { 103 | $result = Permission::addRoleForUser('alice', 'admin'); 104 | $this->assertTrue($result); 105 | 106 | $result = Permission::hasRoleForUser('alice', 'admin'); 107 | $this->assertTrue($result); 108 | 109 | $roles = Permission::getRolesForUser('alice'); 110 | $this->assertContains('admin', $roles); 111 | } 112 | 113 | public function testPermissionForUser() 114 | { 115 | $result = Permission::addPermissionForUser('alice', 'data1', 'read'); 116 | $this->assertTrue($result); 117 | 118 | $result = Permission::enforce('alice', 'data1', 'read'); 119 | $this->assertTrue($result); 120 | 121 | $permissions = Permission::getPermissionsForUser('alice'); 122 | $this->assertContains(['alice', 'data1', 'read'], $permissions); 123 | } 124 | 125 | public function testBatchOperations() 126 | { 127 | $policies = [ 128 | ['alice', 'data1', 'read'], 129 | ['bob', 'data2', 'write'] 130 | ]; 131 | 132 | $result = Permission::addPolicies($policies); 133 | $this->assertTrue($result); 134 | 135 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 136 | $this->assertTrue(Permission::enforce('bob', 'data2', 'write')); 137 | } 138 | 139 | public function testPolicyUpdate() 140 | { 141 | Permission::addPolicy('writer', 'articles', 'edit'); 142 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 143 | 144 | $result = Permission::updatePolicies( 145 | [['writer', 'articles', 'edit']], 146 | [['writer', 'articles', 'update']] 147 | ); 148 | $this->assertTrue($result); 149 | 150 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 151 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'update')); 152 | } 153 | 154 | public function testRemoveOperations() 155 | { 156 | Permission::addPolicy('writer', 'articles', 'edit'); 157 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 158 | 159 | $result = Permission::removePolicy('writer', 'articles', 'edit'); 160 | $this->assertTrue($result); 161 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 162 | } 163 | 164 | public function testDriverManagement() 165 | { 166 | $driver = Permission::getDefaultDriver(); 167 | $this->assertNotEmpty($driver); 168 | 169 | $drivers = Permission::getAllDriver(); 170 | $this->assertIsArray($drivers); 171 | } 172 | 173 | public function testClear() 174 | { 175 | Permission::addPolicy('writer', 'articles', 'edit'); 176 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 177 | 178 | Permission::clear(); 179 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 180 | } 181 | 182 | protected function tearDown(): void 183 | { 184 | Permission::clear(); 185 | } 186 | } -------------------------------------------------------------------------------- /src/Permission.php: -------------------------------------------------------------------------------- 1 | loadModel($config['model']['config_file_path']); 92 | } elseif ('text' == $config['model']['config_type']) { 93 | $model->loadModel($config['model']['config_text']); 94 | } 95 | $logConfig = self::getConfig('log'); 96 | $logger = null; 97 | if (true === $logConfig['enabled']) { 98 | /** @var LoggerInterface $casbinLogger 创建一个 Monolog 日志记录器 */ 99 | $casbinLogger = new Logger($logConfig['logger']); 100 | $casbinLogger->pushHandler(new StreamHandler($logConfig['path'], Logger::DEBUG)); 101 | $logger = new DefaultLogger($casbinLogger); 102 | } 103 | static::$_manager[$driver] = new Enforcer($model, Container::make($config['adapter'], [$driver]), $logger, $logConfig['enabled']); 104 | 105 | $watcher = new RedisWatcher(config('redis.default'), $driver); 106 | static::$_manager[$driver]->setWatcher($watcher); 107 | $watcher->setUpdateCallback(function () use ($driver) { 108 | static::$_manager[$driver]->loadPolicy(); 109 | }); 110 | return static::$_manager[$driver]; 111 | } 112 | 113 | /** 114 | * @desc: 获取所有驱动 115 | * @return Enforcer[] 116 | * @author Tinywan(ShaoBo Wan) 117 | */ 118 | public static function getAllDriver(): array 119 | { 120 | return static::$_manager; 121 | } 122 | 123 | /** 124 | * @desc: 默认驱动 125 | * @return mixed 126 | * @author Tinywan(ShaoBo Wan) 127 | */ 128 | public static function getDefaultDriver(): mixed 129 | { 130 | return self::getConfig('default'); 131 | } 132 | 133 | /** 134 | * @desc: 获取驱动配置 135 | * @param string|null $name 名称 136 | * @param null $default 默认值 137 | * @return mixed 138 | * @author Tinywan(ShaoBo Wan) 139 | */ 140 | public static function getConfig(?string $name = null, $default = null): mixed 141 | { 142 | if (!is_null($name)) { 143 | return config('plugin.casbin.webman-permission.permission.' . $name, $default); 144 | } 145 | return config('plugin.casbin.webman-permission.permission.default'); 146 | } 147 | 148 | /** 149 | * @desc: 静态调用 150 | * @param string $method 151 | * @param $arguments 152 | * @return mixed 153 | * @throws CasbinException 154 | * @author Tinywan(ShaoBo Wan) 155 | */ 156 | public static function __callStatic(string $method, $arguments) 157 | { 158 | return self::driver()->{$method}(...$arguments); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

workbunny

2 | 3 | **

🐇 An Authorization For Webman Plugin. 🐇

** 4 | 5 | #

🐇 Webman Authorization Plugin Base Casbin. 🐇

6 | 7 | [![Default](https://github.com/php-casbin/webman-permission/actions/workflows/default.yml/badge.svg?branch=main)](https://github.com/php-casbin/webman-permission/actions/workflows/default.yml) 8 | [![Latest Stable Version](https://poser.pugx.org/casbin/webman-permission/v/stable)](https://packagist.org/packages/casbin/webman-permission) 9 | [![Total Downloads](https://poser.pugx.org/casbin/webman-permission/downloads)](https://packagist.org/packages/casbin/webman-permission) 10 | [![License](https://poser.pugx.org/casbin/webman-permission/license)](https://packagist.org/packages/casbin/webman-permission) 11 | 12 | An authorization library that supports access control models like ACL, RBAC, ABAC for webman plugin 13 | 14 | # Install 15 | 16 | Composer Install 17 | ```sh 18 | composer require -W casbin/webman-permission 19 | ``` 20 | 21 | # Use 22 | 23 | ## Dependency Injection configuration 24 | 25 | Modify the `config/container.php` configuration to perform the following final contents: 26 | 27 | ```php 28 | $builder = new \DI\ContainerBuilder(); 29 | $builder->addDefinitions(config('dependence', [])); 30 | $builder->useAutowiring(true); 31 | return $builder->build(); 32 | ``` 33 | 34 | ## Database configuration 35 | 36 | 默认策略存储是使用的ThinkORM。 37 | 38 | ### 1、模型配置 39 | 40 | 默认使用ThinkORM。修改数据库 `thinkorm.php` 配置 41 | 42 | > 如使用laravel数据库,配置参考如下 43 | - 修改数据库 `database.php` 配置 44 | - 修改数据库 `permission.php` 的`adapter`适配器为laravel适配器 45 | 46 | ### 2、创建 `casbin_rule` 数据表 47 | ```sql 48 | CREATE TABLE `casbin_rule` ( 49 | `id` BIGINT ( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT, 50 | `ptype` VARCHAR ( 128 ) NOT NULL DEFAULT '', 51 | `v0` VARCHAR ( 128 ) NOT NULL DEFAULT '', 52 | `v1` VARCHAR ( 128 ) NOT NULL DEFAULT '', 53 | `v2` VARCHAR ( 128 ) NOT NULL DEFAULT '', 54 | `v3` VARCHAR ( 128 ) NOT NULL DEFAULT '', 55 | `v4` VARCHAR ( 128 ) NOT NULL DEFAULT '', 56 | `v5` VARCHAR ( 128 ) NOT NULL DEFAULT '', 57 | PRIMARY KEY ( `id` ) USING BTREE, 58 | KEY `idx_ptype` ( `ptype` ) USING BTREE, 59 | KEY `idx_v0` ( `v0` ) USING BTREE, 60 | KEY `idx_v1` ( `v1` ) USING BTREE, 61 | KEY `idx_v2` ( `v2` ) USING BTREE, 62 | KEY `idx_v3` ( `v3` ) USING BTREE, 63 | KEY `idx_v4` ( `v4` ) USING BTREE, 64 | KEY `idx_v5` ( `v5` ) USING BTREE 65 | ) ENGINE = INNODB CHARSET = utf8mb4 COMMENT = '策略规则表'; 66 | ``` 67 | ### 3、配置 `config/redis` 配置 68 | 69 | ### 4、重启webman 70 | 71 | ``` 72 | php start.php restart 73 | ``` 74 | 或者 75 | ``` 76 | php start.php restart -d 77 | ``` 78 | 79 | # 使用 80 | 81 | 安装成功后,可以这样使用: 82 | 83 | ```php 84 | use Casbin\WebmanPermission\Permission; 85 | 86 | // adds permissions to a user 87 | Permission::addPermissionForUser('eve', 'articles', 'read'); 88 | // adds a role for a user. 89 | Permission::addRoleForUser('eve', 'writer'); 90 | // adds permissions to a rule 91 | Permission::addPolicy('writer', 'articles','edit'); 92 | ``` 93 | 94 | 你可以检查一个用户是否拥有某个权限: 95 | 96 | ```php 97 | if (\Casbin\WebmanPermission\Permission::enforce('eve', 'articles', 'edit')) { 98 | echo '恭喜你!通过权限认证'; 99 | } else { 100 | echo '对不起,您没有该资源访问权限'; 101 | } 102 | ``` 103 | 104 | # 多套驱动配置 105 | 106 | ```php 107 | $permission = \Casbin\WebmanPermission\Permission::driver('restful_conf'); 108 | // adds permissions to a user 109 | $permission->addPermissionForUser('eve', 'articles', 'read'); 110 | // adds a role for a user. 111 | $permission->addRoleForUser('eve', 'writer'); 112 | // adds permissions to a rule 113 | $permission->addPolicy('writer', 'articles','edit'); 114 | 115 | if ($permission->enforce('eve', 'articles', 'edit')) { 116 | echo '恭喜你!通过权限认证'; 117 | } else { 118 | echo '对不起,您没有该资源访问权限'; 119 | } 120 | ``` 121 | 122 | 更多 `API` 参考 [Casbin API](https://casbin.org/docs/en/management-api) 。 123 | 124 | # 教程 125 | * [Casbin权限实战:入门分享(中文)](https://www.bilibili.com/video/BV1A541187M4/?vd_source=a9321be9ed112f8d6fdc8ee87640be1b) 126 | * [Casbin权限实战:基于角色的RBAC授权](https://www.bilibili.com/video/BV1A541187M4/?vd_source=a9321be9ed112f8d6fdc8ee87640be1b) 127 | * [Casbin权限实战:RESTful及中间件使用](https://www.bilibili.com/video/BV1uk4y117up/?vd_source=a9321be9ed112f8d6fdc8ee87640be1b) 128 | * [Casbin权限实战:如何使用自定义匹配函数](https://www.bilibili.com/video/BV1dq4y1Z78g/?vd_source=a9321be9ed112f8d6fdc8ee87640be1b) 129 | * [Webman实战教程:如何使用casbin权限控制](https://www.bilibili.com/video/BV1X34y1Q7ZH/?vd_source=a9321be9ed112f8d6fdc8ee87640be1b) 130 | 131 | # 测试 132 | 133 | ## 测试套件 134 | 135 | 本项目包含完整的单元测试套件,覆盖了以下方面: 136 | 137 | ### 测试文件结构 138 | 139 | ``` 140 | tests/ 141 | ├── Adapter.php # 适配器基础测试 142 | ├── PermissionTest.php # Permission类测试 143 | ├── AdapterTest.php # 适配器详细测试 144 | ├── EdgeCaseTest.php # 边界情况测试 145 | ├── IntegrationTest.php # 集成测试 146 | ├── LaravelDatabase/ 147 | │ ├── LaravelDatabaseAdapterTest.php 148 | │ └── TestCase.php 149 | ├── ThinkphpDatabase/ 150 | │ ├── DatabaseAdapterTest.php 151 | │ └── TestCase.php 152 | └── config/ 153 | └── plugin/ 154 | └── casbin/ 155 | └── webman-permission/ 156 | └── permission.php 157 | ``` 158 | 159 | ### 测试覆盖范围 160 | 161 | 1. **基础功能测试** 162 | - 权限添加、删除、检查 163 | - 角色分配、移除 164 | - 策略管理 165 | 166 | 2. **适配器测试** 167 | - 数据库操作 168 | - 过滤器功能 169 | - 批量操作 170 | - 事务处理 171 | 172 | 3. **边界情况测试** 173 | - 空值处理 174 | - 特殊字符 175 | - 大数据量 176 | - 性能测试 177 | 178 | 4. **集成测试** 179 | - RBAC完整流程 180 | - 域权限控制 181 | - 多驱动支持 182 | - 复杂业务场景 183 | 184 | 5. **错误处理测试** 185 | - 异常情况 186 | - 无效输入 187 | - 并发访问 188 | 189 | ### 运行测试 190 | 191 | ```bash 192 | # 运行所有测试 193 | php vendor/bin/phpunit tests/ 194 | 195 | # 运行特定测试文件 196 | php vendor/bin/phpunit tests/PermissionTest.php 197 | 198 | # 运行特定测试方法 199 | php vendor/bin/phpunit --filter testAddPermissionForUser tests/PermissionTest.php 200 | 201 | # 生成测试覆盖率报告 202 | php vendor/bin/phpunit --coverage-html coverage tests/ 203 | ``` 204 | 205 | ### 测试要求 206 | 207 | - PHP >= 8.1 208 | - PHPUnit >= 9.0 209 | - 数据库连接 210 | - Redis连接 211 | 212 | ### 测试环境配置 213 | 214 | 测试环境会自动创建以下数据表: 215 | - `casbin_rule` - 默认策略表 216 | - `other_casbin_rule` - 其他驱动策略表 217 | 218 | ### 测试最佳实践 219 | 220 | 1. **编写新测试** 221 | - 继承适当的测试基类 222 | - 遵循命名约定 223 | - 添加必要的断言 224 | 225 | 2. **测试数据管理** 226 | - 使用 `setUp()` 和 `tearDown()` 方法 227 | - 确保测试数据隔离 228 | - 清理测试数据 229 | 230 | 3. **测试覆盖** 231 | - 覆盖正常流程 232 | - 测试异常情况 233 | - 验证边界条件 234 | 235 | ## 贡献指南 236 | 237 | ### 添加新功能测试 238 | 239 | 1. 为新功能编写对应的测试用例 240 | 2. 确保测试覆盖率达到要求 241 | 3. 运行完整测试套件 242 | 4. 提交代码前检查测试状态 243 | 244 | ### 修复Bug测试 245 | 246 | 1. 为Bug编写重现测试 247 | 2. 修复Bug后验证测试通过 248 | 3. 确保不影响现有功能 249 | 250 | # 感谢 251 | 252 | [Casbin](https://github.com/php-casbin/php-casbin),你可以查看全部文档在其 [官网](https://casbin.org/) 上。 253 | 254 |
255 | 256 | 解除 https://github.com/PHP-DI/PHP-DI依赖的解决方案(不推荐) 257 | 258 | 1、卸载DI依赖包:`composer remove php-di/php-di` 259 | 260 | 2、修改:`Casbin\WebmanPermission\Permission` 文件 261 | 262 | ```php 263 | if (is_null(static::$_manager)) { 264 | static::$_manager = new Enforcer($model, Container::get($config['adapter']),false); 265 | } 266 | ``` 267 | 替换为 268 | ```php 269 | if (is_null(static::$_manager)) { 270 | if ($config['adapter'] == DatabaseAdapter::class) { 271 | $_model = new RuleModel(); 272 | } elseif ($config['adapter'] == LaravelDatabaseAdapter::class) { 273 | $_model = new LaravelRuleModel(); 274 | } 275 | static::$_manager = new Enforcer($model, new $config['adapter']($_model), false); 276 | } 277 | ``` 278 | 耦合太高,不建议这么搞,更多了解:https://www.workerman.net/doc/webman/di.html 279 |
280 | 281 | ## 问题 282 | 283 | * Laravel的驱动报错:`Call to a member function connection() on null|webman2.1/vendor/illuminate/database/Eloquent/Model. 284 | php|1918`。解决方案,请检查本地数据库代理是否正常,如使用了Docker容器主机地址`dnmp-mysql`可能会导致该问题出现。 285 | -------------------------------------------------------------------------------- /tests/AdapterTest.php: -------------------------------------------------------------------------------- 1 | databaseAdapter = new DatabaseAdapter(); 21 | $this->laravelAdapter = new LaravelDatabaseAdapter(); 22 | } 23 | 24 | public function testFilterRule() 25 | { 26 | $rule = ['ptype', 'v0', 'v1', '', null, 'v4']; 27 | $filtered = $this->databaseAdapter->filterRule($rule); 28 | 29 | $this->assertEquals(['ptype', 'v0', 'v1', 'v4'], $filtered); 30 | } 31 | 32 | public function testFilterRuleWithAllEmpty() 33 | { 34 | $rule = ['ptype', '', null, '']; 35 | $filtered = $this->databaseAdapter->filterRule($rule); 36 | 37 | $this->assertEquals(['ptype'], $filtered); 38 | } 39 | 40 | public function testIsFiltered() 41 | { 42 | $this->assertFalse($this->databaseAdapter->isFiltered()); 43 | 44 | $this->databaseAdapter->setFiltered(true); 45 | $this->assertTrue($this->databaseAdapter->isFiltered()); 46 | } 47 | 48 | public function testLoadFilteredPolicyWithStringFilter() 49 | { 50 | $model = new \Casbin\Model\Model(); 51 | $model->addDef('p', 'p', ['sub', 'obj', 'act']); 52 | 53 | $filter = "ptype = 'p' AND v0 = 'alice'"; 54 | 55 | $this->databaseAdapter->loadFilteredPolicy($model, $filter); 56 | $this->assertTrue($this->databaseAdapter->isFiltered()); 57 | } 58 | 59 | public function testLoadFilteredPolicyWithFilterObject() 60 | { 61 | $model = new \Casbin\Model\Model(); 62 | $model->addDef('p', 'p', ['sub', 'obj', 'act']); 63 | 64 | $filter = new Filter(); 65 | $filter->p[] = 'v0'; 66 | $filter->g[] = 'alice'; 67 | 68 | $this->databaseAdapter->loadFilteredPolicy($model, $filter); 69 | $this->assertTrue($this->databaseAdapter->isFiltered()); 70 | } 71 | 72 | public function testLoadFilteredPolicyWithClosure() 73 | { 74 | $model = new \Casbin\Model\Model(); 75 | $model->addDef('p', 'p', ['sub', 'obj', 'act']); 76 | 77 | $filter = function($query) { 78 | return $query->where('v0', 'alice'); 79 | }; 80 | 81 | $this->databaseAdapter->loadFilteredPolicy($model, $filter); 82 | $this->assertTrue($this->databaseAdapter->isFiltered()); 83 | } 84 | 85 | public function testLoadFilteredPolicyWithInvalidFilter() 86 | { 87 | $model = new \Casbin\Model\Model(); 88 | $model->addDef('p', 'p', ['sub', 'obj', 'act']); 89 | 90 | $this->expectException(InvalidFilterTypeException::class); 91 | $this->databaseAdapter->loadFilteredPolicy($model, 123); 92 | } 93 | 94 | public function testSavePolicyLine() 95 | { 96 | $this->databaseAdapter->savePolicyLine('p', ['alice', 'data1', 'read']); 97 | 98 | $this->assertTrue(true); 99 | } 100 | 101 | public function testAddPolicy() 102 | { 103 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 104 | 105 | $this->assertTrue(true); 106 | } 107 | 108 | public function testAddPolicies() 109 | { 110 | $policies = [ 111 | ['alice', 'data1', 'read'], 112 | ['bob', 'data2', 'write'] 113 | ]; 114 | 115 | $this->databaseAdapter->addPolicies('p', 'p', $policies); 116 | 117 | $this->assertTrue(true); 118 | } 119 | 120 | public function testRemovePolicy() 121 | { 122 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 123 | $this->databaseAdapter->removePolicy('p', 'p', ['alice', 'data1', 'read']); 124 | 125 | $this->assertTrue(true); 126 | } 127 | 128 | public function testRemovePolicies() 129 | { 130 | $policies = [ 131 | ['alice', 'data1', 'read'], 132 | ['bob', 'data2', 'write'] 133 | ]; 134 | 135 | $this->databaseAdapter->addPolicies('p', 'p', $policies); 136 | $this->databaseAdapter->removePolicies('p', 'p', $policies); 137 | 138 | $this->assertTrue(true); 139 | } 140 | 141 | public function testUpdatePolicy() 142 | { 143 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 144 | $this->databaseAdapter->updatePolicy('p', 'p', ['alice', 'data1', 'read'], ['alice', 'data1', 'write']); 145 | 146 | $this->assertTrue(true); 147 | } 148 | 149 | public function testUpdatePolicies() 150 | { 151 | $oldPolicies = [ 152 | ['alice', 'data1', 'read'], 153 | ['bob', 'data2', 'write'] 154 | ]; 155 | 156 | $newPolicies = [ 157 | ['alice', 'data1', 'write'], 158 | ['bob', 'data2', 'read'] 159 | ]; 160 | 161 | $this->databaseAdapter->addPolicies('p', 'p', $oldPolicies); 162 | $this->databaseAdapter->updatePolicies('p', 'p', $oldPolicies, $newPolicies); 163 | 164 | $this->assertTrue(true); 165 | } 166 | 167 | public function testRemoveFilteredPolicy() 168 | { 169 | $this->databaseAdapter->addPolicies('p', 'p', [ 170 | ['alice', 'data1', 'read'], 171 | ['alice', 'data2', 'write'], 172 | ['bob', 'data1', 'read'] 173 | ]); 174 | 175 | $this->databaseAdapter->removeFilteredPolicy('p', 'p', 0, 'alice'); 176 | 177 | $this->assertTrue(true); 178 | } 179 | 180 | public function testUpdateFilteredPolicies() 181 | { 182 | $this->databaseAdapter->addPolicies('p', 'p', [ 183 | ['alice', 'data1', 'read'], 184 | ['alice', 'data2', 'write'] 185 | ]); 186 | 187 | $newPolicies = [ 188 | ['alice', 'data1', 'write'], 189 | ['alice', 'data2', 'read'] 190 | ]; 191 | 192 | $this->databaseAdapter->updateFilteredPolicies('p', 'p', $newPolicies, 0, 'alice'); 193 | 194 | $this->assertTrue(true); 195 | } 196 | 197 | public function testAdapterWithEmptyRule() 198 | { 199 | $this->databaseAdapter->addPolicy('p', 'p', ['', '', '']); 200 | $this->databaseAdapter->removePolicy('p', 'p', ['', '', '']); 201 | 202 | $this->assertTrue(true); 203 | } 204 | 205 | public function testAdapterWithNullValues() 206 | { 207 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', null, 'read']); 208 | $this->databaseAdapter->removePolicy('p', 'p', ['alice', null, 'read']); 209 | 210 | $this->assertTrue(true); 211 | } 212 | 213 | public function testAdapterWithSpecialCharacters() 214 | { 215 | $this->databaseAdapter->addPolicy('p', 'p', ['user@domain.com', 'data#1', 'action:read']); 216 | $this->databaseAdapter->removePolicy('p', 'p', ['user@domain.com', 'data#1', 'action:read']); 217 | 218 | $this->assertTrue(true); 219 | } 220 | 221 | public function testAdapterWithLargePolicySet() 222 | { 223 | $policies = []; 224 | for ($i = 0; $i < 1000; $i++) { 225 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 226 | } 227 | 228 | $this->databaseAdapter->addPolicies('p', 'p', $policies); 229 | $this->databaseAdapter->removePolicies('p', 'p', $policies); 230 | 231 | $this->assertTrue(true); 232 | } 233 | 234 | public function testAdapterConcurrentOperations() 235 | { 236 | $policies = []; 237 | for ($i = 0; $i < 100; $i++) { 238 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 239 | } 240 | 241 | $this->databaseAdapter->addPolicies('p', 'p', $policies); 242 | 243 | foreach ($policies as $policy) { 244 | $this->databaseAdapter->removePolicy('p', 'p', $policy); 245 | } 246 | 247 | $this->assertTrue(true); 248 | } 249 | 250 | public function testLaravelAdapterMethods() 251 | { 252 | $this->laravelAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 253 | $this->laravelAdapter->removePolicy('p', 'p', ['alice', 'data1', 'read']); 254 | 255 | $this->assertTrue(true); 256 | } 257 | 258 | public function testLaravelAdapterUpdateOrCreate() 259 | { 260 | $this->laravelAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 261 | $this->laravelAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 262 | 263 | $this->assertTrue(true); 264 | } 265 | 266 | public function testAdapterWithDifferentPtypes() 267 | { 268 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', 'data1', 'read']); 269 | $this->databaseAdapter->addPolicy('g', 'g', ['alice', 'admin']); 270 | $this->databaseAdapter->addPolicy('p2', 'p2', ['admin', 'data1', 'write']); 271 | 272 | $this->databaseAdapter->removePolicy('p', 'p', ['alice', 'data1', 'read']); 273 | $this->databaseAdapter->removePolicy('g', 'g', ['alice', 'admin']); 274 | $this->databaseAdapter->removePolicy('p2', 'p2', ['admin', 'data1', 'write']); 275 | 276 | $this->assertTrue(true); 277 | } 278 | 279 | public function testAdapterWithPartialFieldMatching() 280 | { 281 | $this->databaseAdapter->addPolicies('p', 'p', [ 282 | ['alice', 'data1', 'read'], 283 | ['alice', 'data2', 'read'], 284 | ['bob', 'data1', 'write'] 285 | ]); 286 | 287 | $this->databaseAdapter->removeFilteredPolicy('p', 'p', 1, 'data1'); 288 | 289 | $this->assertTrue(true); 290 | } 291 | 292 | public function testAdapterWithEmptyFieldValues() 293 | { 294 | $this->databaseAdapter->addPolicy('p', 'p', ['alice', '', 'read']); 295 | $this->databaseAdapter->removeFilteredPolicy('p', 'p', 1, ''); 296 | 297 | $this->assertTrue(true); 298 | } 299 | 300 | public function testAdapterTransactionRollback() 301 | { 302 | $this->expectException(\Exception::class); 303 | 304 | $policies = [ 305 | ['alice', 'data1', 'read'], 306 | ['bob', 'data2', 'write'] 307 | ]; 308 | 309 | $this->databaseAdapter->addPolicies('p', 'p', $policies); 310 | 311 | throw new \Exception('Test rollback'); 312 | } 313 | 314 | protected function tearDown(): void 315 | { 316 | $this->databaseAdapter = null; 317 | $this->laravelAdapter = null; 318 | } 319 | } -------------------------------------------------------------------------------- /tests/EdgeCaseTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Permission::hasPolicy('', '', '')); 22 | 23 | $result = Permission::removePolicy('', '', ''); 24 | $this->assertTrue($result); 25 | } 26 | 27 | public function testNullValuesInPolicy() 28 | { 29 | Permission::addPolicy('alice', null, 'read'); 30 | $this->assertTrue(Permission::hasPolicy('alice', null, 'read')); 31 | 32 | $result = Permission::removePolicy('alice', null, 'read'); 33 | $this->assertTrue($result); 34 | } 35 | 36 | public function testVeryLongStrings() 37 | { 38 | $longString = str_repeat('a', 1000); 39 | Permission::addPolicy($longString, $longString, $longString); 40 | $this->assertTrue(Permission::hasPolicy($longString, $longString, $longString)); 41 | 42 | $result = Permission::removePolicy($longString, $longString, $longString); 43 | $this->assertTrue($result); 44 | } 45 | 46 | public function testSpecialCharacters() 47 | { 48 | $specialChars = [ 49 | 'user@domain.com', 50 | 'data#1', 51 | 'action:read', 52 | 'user+test@example.com', 53 | 'data?query=param', 54 | 'action&operation=test', 55 | 'user|domain', 56 | 'data\\path', 57 | 'action"quoted"', 58 | "user'string", 59 | 'data[0]', 60 | 'action(test)' 61 | ]; 62 | 63 | foreach ($specialChars as $char) { 64 | Permission::addPolicy($char, $char, $char); 65 | $this->assertTrue(Permission::hasPolicy($char, $char, $char)); 66 | 67 | $result = Permission::removePolicy($char, $char, $char); 68 | $this->assertTrue($result); 69 | } 70 | } 71 | 72 | public function testUnicodeCharacters() 73 | { 74 | $unicodeStrings = [ 75 | '用户名', 76 | '数据1', 77 | '操作:读取', 78 | 'αβγδε', 79 | 'αβγδεζηθικλμνξοπρστυφχψω', 80 | '漢字', 81 | '😀emoji😊', 82 | 'café', 83 | 'naïve', 84 | 'résumé' 85 | ]; 86 | 87 | foreach ($unicodeStrings as $str) { 88 | Permission::addPolicy($str, $str, $str); 89 | $this->assertTrue(Permission::hasPolicy($str, $str, $str)); 90 | 91 | $result = Permission::removePolicy($str, $str, $str); 92 | $this->assertTrue($result); 93 | } 94 | } 95 | 96 | public function testMixedCaseSensitivity() 97 | { 98 | Permission::addPolicy('Alice', 'Data1', 'Read'); 99 | Permission::addPolicy('alice', 'data1', 'read'); 100 | 101 | $this->assertTrue(Permission::hasPolicy('Alice', 'Data1', 'Read')); 102 | $this->assertTrue(Permission::hasPolicy('alice', 'data1', 'read')); 103 | 104 | $this->assertFalse(Permission::hasPolicy('alice', 'Data1', 'Read')); 105 | $this->assertFalse(Permission::hasPolicy('Alice', 'data1', 'read')); 106 | } 107 | 108 | public function testWhitespaceHandling() 109 | { 110 | Permission::addPolicy(' alice ', ' data1 ', ' read '); 111 | $this->assertTrue(Permission::hasPolicy(' alice ', ' data1 ', ' read ')); 112 | 113 | $this->assertFalse(Permission::hasPolicy('alice', 'data1', 'read')); 114 | $this->assertFalse(Permission::hasPolicy('alice ', 'data1 ', 'read ')); 115 | } 116 | 117 | public function testEmptyPoliciesArray() 118 | { 119 | $result = Permission::addPolicies([]); 120 | $this->assertFalse($result); 121 | 122 | $result = Permission::removePolicies([]); 123 | $this->assertTrue($result); 124 | } 125 | 126 | public function testSingleCharacterPolicies() 127 | { 128 | Permission::addPolicy('a', 'b', 'c'); 129 | $this->assertTrue(Permission::hasPolicy('a', 'b', 'c')); 130 | 131 | $result = Permission::removePolicy('a', 'b', 'c'); 132 | $this->assertTrue($result); 133 | } 134 | 135 | public function testNumericPolicies() 136 | { 137 | Permission::addPolicy('123', '456', '789'); 138 | $this->assertTrue(Permission::hasPolicy('123', '456', '789')); 139 | 140 | $result = Permission::removePolicy('123', '456', '789'); 141 | $this->assertTrue($result); 142 | } 143 | 144 | public function testBooleanLikeStrings() 145 | { 146 | Permission::addPolicy('true', 'false', 'null'); 147 | $this->assertTrue(Permission::hasPolicy('true', 'false', 'null')); 148 | 149 | $result = Permission::removePolicy('true', 'false', 'null'); 150 | $this->assertTrue($result); 151 | } 152 | 153 | public function testSQLInjectionAttempts() 154 | { 155 | $maliciousInputs = [ 156 | "alice'; DROP TABLE casbin_rule; --", 157 | "alice' OR '1'='1", 158 | "alice'; SELECT * FROM users; --", 159 | "alice' UNION SELECT * FROM users; --", 160 | "alice' AND SLEEP(10); --" 161 | ]; 162 | 163 | foreach ($maliciousInputs as $input) { 164 | Permission::addPolicy($input, 'data1', 'read'); 165 | $this->assertTrue(Permission::hasPolicy($input, 'data1', 'read')); 166 | 167 | $result = Permission::removePolicy($input, 'data1', 'read'); 168 | $this->assertTrue($result); 169 | } 170 | } 171 | 172 | public function testXSSAttempts() 173 | { 174 | $xssInputs = [ 175 | '', 176 | 'javascript:alert("xss")', 177 | '">', 178 | '', 179 | '' 180 | ]; 181 | 182 | foreach ($xssInputs as $input) { 183 | Permission::addPolicy($input, 'data1', 'read'); 184 | $this->assertTrue(Permission::hasPolicy($input, 'data1', 'read')); 185 | 186 | $result = Permission::removePolicy($input, 'data1', 'read'); 187 | $this->assertTrue($result); 188 | } 189 | } 190 | 191 | public function testConcurrentAccess() 192 | { 193 | $policies = []; 194 | for ($i = 0; $i < 100; $i++) { 195 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 196 | } 197 | 198 | Permission::addPolicies($policies); 199 | 200 | for ($i = 0; $i < 100; $i++) { 201 | $this->assertTrue(Permission::enforce('user' . $i, 'resource' . $i, 'action' . $i)); 202 | } 203 | 204 | Permission::removePolicies($policies); 205 | 206 | for ($i = 0; $i < 100; $i++) { 207 | $this->assertFalse(Permission::enforce('user' . $i, 'resource' . $i, 'action' . $i)); 208 | } 209 | } 210 | 211 | public function testMemoryUsageWithLargeDataset() 212 | { 213 | $initialMemory = memory_get_usage(); 214 | 215 | $policies = []; 216 | for ($i = 0; $i < 1000; $i++) { 217 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 218 | } 219 | 220 | Permission::addPolicies($policies); 221 | 222 | $peakMemory = memory_get_peak_usage(); 223 | $memoryIncrease = $peakMemory - $initialMemory; 224 | 225 | $this->assertLessThan(50 * 1024 * 1024, $memoryIncrease); 226 | 227 | Permission::removePolicies($policies); 228 | } 229 | 230 | public function testPerformanceWithManyEnforcements() 231 | { 232 | Permission::addPolicy('user', 'resource', 'action'); 233 | 234 | $startTime = microtime(true); 235 | 236 | for ($i = 0; $i < 1000; $i++) { 237 | Permission::enforce('user', 'resource', 'action'); 238 | } 239 | 240 | $endTime = microtime(true); 241 | $executionTime = $endTime - $startTime; 242 | 243 | $this->assertLessThan(1.0, $executionTime); 244 | 245 | Permission::removePolicy('user', 'resource', 'action'); 246 | } 247 | 248 | public function testPolicyWithAllNullValues() 249 | { 250 | Permission::addPolicy(null, null, null); 251 | $this->assertTrue(Permission::hasPolicy(null, null, null)); 252 | 253 | $result = Permission::removePolicy(null, null, null); 254 | $this->assertTrue($result); 255 | } 256 | 257 | public function testPolicyWithMixedNullAndEmpty() 258 | { 259 | Permission::addPolicy('alice', '', null); 260 | $this->assertTrue(Permission::hasPolicy('alice', '', null)); 261 | 262 | $result = Permission::removePolicy('alice', '', null); 263 | $this->assertTrue($result); 264 | } 265 | 266 | public function testVeryLargePolicySet() 267 | { 268 | $policies = []; 269 | for ($i = 0; $i < 5000; $i++) { 270 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 271 | } 272 | 273 | $result = Permission::addPolicies($policies); 274 | $this->assertTrue($result); 275 | 276 | for ($i = 0; $i < 100; $i++) { 277 | $this->assertTrue(Permission::enforce('user' . $i, 'resource' . $i, 'action' . $i)); 278 | } 279 | 280 | $result = Permission::removePolicies($policies); 281 | $this->assertTrue($result); 282 | } 283 | 284 | public function testDuplicatePolicyInBatch() 285 | { 286 | $policies = [ 287 | ['alice', 'data1', 'read'], 288 | ['alice', 'data1', 'read'], 289 | ['bob', 'data2', 'write'] 290 | ]; 291 | 292 | $result = Permission::addPolicies($policies); 293 | $this->assertFalse($result); 294 | 295 | $this->assertTrue(Permission::hasPolicy('alice', 'data1', 'read')); 296 | $this->assertTrue(Permission::hasPolicy('bob', 'data2', 'write')); 297 | } 298 | 299 | public function testPolicyWithNewlinesAndTabs() 300 | { 301 | $newLinePolicy = "alice\ndata1\nread"; 302 | $tabPolicy = "alice\tdata1\tread"; 303 | 304 | Permission::addPolicy($newLinePolicy, $newLinePolicy, $newLinePolicy); 305 | Permission::addPolicy($tabPolicy, $tabPolicy, $tabPolicy); 306 | 307 | $this->assertTrue(Permission::hasPolicy($newLinePolicy, $newLinePolicy, $newLinePolicy)); 308 | $this->assertTrue(Permission::hasPolicy($tabPolicy, $tabPolicy, $tabPolicy)); 309 | 310 | Permission::removePolicy($newLinePolicy, $newLinePolicy, $newLinePolicy); 311 | Permission::removePolicy($tabPolicy, $tabPolicy, $tabPolicy); 312 | } 313 | 314 | protected function tearDown(): void 315 | { 316 | Permission::clear(); 317 | } 318 | } -------------------------------------------------------------------------------- /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/LaravelDatabaseAdapter.php: -------------------------------------------------------------------------------- 1 | model = new LaravelRuleModel([],$driver); 56 | } 57 | 58 | /** 59 | * Filter the rule. 60 | * 61 | * @param array $rule 62 | * @return array 63 | */ 64 | public function filterRule(array $rule): array 65 | { 66 | $rule = array_values($rule); 67 | 68 | $i = count($rule) - 1; 69 | for (; $i >= 0; $i--) { 70 | if ($rule[$i] != '' && !is_null($rule[$i])) { 71 | break; 72 | } 73 | } 74 | 75 | return array_slice($rule, 0, $i + 1); 76 | } 77 | 78 | /** 79 | * savePolicyLine function. 80 | * 81 | * @param string $ptype 82 | * @param array $rule 83 | * 84 | * @return void 85 | */ 86 | public function savePolicyLine(string $ptype, array $rule) 87 | { 88 | $col['ptype'] = $ptype; 89 | foreach ($rule as $key => $value) { 90 | $col['v' . $key] = $value; 91 | } 92 | $this->model->updateOrCreate($col); 93 | } 94 | 95 | /** 96 | * loads all policy rules from the storage. 97 | * 98 | * @param Model $model 99 | */ 100 | public function loadPolicy(Model $model): void 101 | { 102 | $rows = $this->model->select(['ptype', 'v0', 'v1', 'v2', 'v3', 'v4', 'v5'])->get()->toArray();; 103 | foreach ($rows as $row) { 104 | $this->loadPolicyArray($this->filterRule($row), $model); 105 | } 106 | } 107 | 108 | /** 109 | * saves all policy rules to the storage. 110 | * 111 | * @param Model $model 112 | */ 113 | public function savePolicy(Model $model): void 114 | { 115 | foreach ($model['p'] as $ptype => $ast) { 116 | foreach ($ast->policy as $rule) { 117 | $this->savePolicyLine($ptype, $rule); 118 | } 119 | } 120 | 121 | foreach ($model['g'] as $ptype => $ast) { 122 | foreach ($ast->policy as $rule) { 123 | $this->savePolicyLine($ptype, $rule); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * adds a policy rule to the storage. 130 | * This is part of the Auto-Save feature. 131 | * 132 | * @param string $sec 133 | * @param string $ptype 134 | * @param array $rule 135 | */ 136 | public function addPolicy(string $sec, string $ptype, array $rule): void 137 | { 138 | $this->savePolicyLine($ptype, $rule); 139 | } 140 | 141 | /** 142 | * Adds a policy rules to the storage. 143 | * This is part of the Auto-Save feature. 144 | * 145 | * @param string $sec 146 | * @param string $ptype 147 | * @param string[][] $rules 148 | */ 149 | public function addPolicies(string $sec, string $ptype, array $rules): void 150 | { 151 | foreach ($rules as $rule) { 152 | $temp = ['ptype' => $ptype]; 153 | foreach ($rule as $key => $value) { 154 | $temp['v' . $key] = $value; 155 | } 156 | $this->model->updateOrCreate($temp); 157 | } 158 | } 159 | 160 | /** 161 | * This is part of the Auto-Save feature. 162 | * 163 | * @param string $sec 164 | * @param string $ptype 165 | * @param array $rule 166 | */ 167 | public function removePolicy(string $sec, string $ptype, array $rule): void 168 | { 169 | $instance = $this->model->where('ptype', $ptype); 170 | foreach ($rule as $key => $value) { 171 | $instance->where('v' . $key, $value); 172 | } 173 | $data = $instance->get(); 174 | foreach ($data as $item) { 175 | $item->delete(); 176 | } 177 | } 178 | 179 | /** 180 | * @param string $sec 181 | * @param string $ptype 182 | * @param int $fieldIndex 183 | * @param string|null ...$fieldValues 184 | * @return array 185 | * @throws Throwable 186 | */ 187 | public function _removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, ?string ...$fieldValues): array 188 | { 189 | $removedRules = []; 190 | $data = $this->getCollection($ptype, $fieldIndex, $fieldValues); 191 | 192 | foreach ($data as $model) { 193 | $item = $model->hidden(['id', 'ptype'])->toArray(); 194 | $item = $this->filterRule($item); 195 | $removedRules[] = $item; 196 | } 197 | 198 | return $removedRules; 199 | } 200 | 201 | /** 202 | * Removes policy rules from the storage. 203 | * This is part of the Auto-Save feature. 204 | * 205 | * @param string $sec 206 | * @param string $ptype 207 | * @param string[][] $rules 208 | */ 209 | public function removePolicies(string $sec, string $ptype, array $rules): void 210 | { 211 | Db::transaction(function () use ($sec, $ptype, $rules) { 212 | foreach ($rules as $rule) { 213 | $this->removePolicy($sec, $ptype, $rule); 214 | } 215 | }); 216 | } 217 | 218 | /** 219 | * RemoveFilteredPolicy removes policy rules that match the filter from the storage. 220 | * This is part of the Auto-Save feature. 221 | * 222 | * @param string $sec 223 | * @param string $ptype 224 | * @param int $fieldIndex 225 | * @param string ...$fieldValues 226 | */ 227 | public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, string ...$fieldValues): void 228 | { 229 | $data = $this->getCollection($ptype, $fieldIndex, $fieldValues); 230 | foreach ($data as $item) { 231 | $item->delete(); 232 | } 233 | } 234 | 235 | /** 236 | * Updates a policy rule from storage. 237 | * This is part of the Auto-Save feature. 238 | * 239 | * @param string $sec 240 | * @param string $ptype 241 | * @param string[] $oldRule 242 | * @param string[] $newPolicy 243 | */ 244 | public function updatePolicy(string $sec, string $ptype, array $oldRule, array $newPolicy): void 245 | { 246 | $instance = $this->model->where('ptype', $ptype); 247 | foreach ($oldRule as $key => $value) { 248 | $instance->where('v' . $key, $value); 249 | } 250 | $instance = $instance->first(); 251 | 252 | $update = []; 253 | foreach ($newPolicy as $key => $value) { 254 | $update['v' . $key] = $value; 255 | } 256 | 257 | $instance->fill($update); 258 | $instance->save(); 259 | } 260 | 261 | /** 262 | * UpdatePolicies updates some policy rules to storage, like DB, redis. 263 | * 264 | * @param string $sec 265 | * @param string $ptype 266 | * @param string[][] $oldRules 267 | * @param string[][] $newRules 268 | * @return void 269 | */ 270 | public function updatePolicies(string $sec, string $ptype, array $oldRules, array $newRules): void 271 | { 272 | Db::transaction(function () use ($sec, $ptype, $oldRules, $newRules) { 273 | foreach ($oldRules as $i => $oldRule) { 274 | $this->updatePolicy($sec, $ptype, $oldRule, $newRules[$i]); 275 | } 276 | }); 277 | } 278 | 279 | /** 280 | * UpdateFilteredPolicies deletes old rules and adds new rules. 281 | * 282 | * @param string $sec 283 | * @param string $ptype 284 | * @param array $newPolicies 285 | * @param integer $fieldIndex 286 | * @param string ...$fieldValues 287 | * @return array 288 | */ 289 | public function updateFilteredPolicies(string $sec, string $ptype, array $newPolicies, int $fieldIndex, string ...$fieldValues): array 290 | { 291 | $oldRules = []; 292 | Db::transaction(function () use ($sec, $ptype, $fieldIndex, $fieldValues, $newPolicies, &$oldRules) { 293 | $oldRules = $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues); 294 | $this->addPolicies($sec, $ptype, $newPolicies); 295 | }); 296 | return $oldRules; 297 | } 298 | 299 | /** 300 | * Returns true if the loaded policy has been filtered. 301 | * 302 | * @return bool 303 | */ 304 | public function isFiltered(): bool 305 | { 306 | return $this->filtered; 307 | } 308 | 309 | /** 310 | * Sets filtered parameter. 311 | * 312 | * @param bool $filtered 313 | */ 314 | public function setFiltered(bool $filtered): void 315 | { 316 | $this->filtered = $filtered; 317 | } 318 | 319 | /** 320 | * Loads only policy rules that match the filter. 321 | * 322 | * @param Model $model 323 | * @param mixed $filter 324 | * 325 | * @throws InvalidFilterTypeException 326 | */ 327 | public function loadFilteredPolicy(Model $model, $filter): void 328 | { 329 | $instance = $this->model; 330 | if (is_string($filter)) { 331 | $instance->whereRaw($filter); 332 | } 333 | elseif ($filter instanceof Filter) { 334 | $where = []; 335 | foreach ($filter->p as $k => $v) { 336 | $where[$v] = $filter->g[$k]; 337 | } 338 | $instance->where($where); 339 | } 340 | elseif ($filter instanceof Closure) { 341 | $instance = $instance->where($filter); 342 | } 343 | else { 344 | throw new InvalidFilterTypeException('invalid filter type'); 345 | } 346 | $rows = $instance->get()->makeHidden(['created_at', 'updated_at', 'id'])->toArray(); 347 | if ($rows) { 348 | foreach ($rows as $row) { 349 | $row = array_filter($row, function ($value) { 350 | return !is_null($value) && $value !== ''; 351 | }); 352 | $line = implode( 353 | ', ', 354 | array_filter($row, function ($val) { 355 | return '' != $val && !is_null($val); 356 | }) 357 | ); 358 | $this->loadPolicyLine(trim($line), $model); 359 | } 360 | } 361 | $this->setFiltered(true); 362 | } 363 | 364 | /** 365 | * @param string $ptype 366 | * @param int $fieldIndex 367 | * @param array $fieldValues 368 | * 369 | * @return Builder[]|Collection 370 | */ 371 | protected function getCollection(string $ptype, int $fieldIndex, array $fieldValues): Collection|array 372 | { 373 | $where = [ 374 | 'ptype' => $ptype, 375 | ]; 376 | foreach (range(0, 5) as $value) { 377 | if ($fieldIndex <= $value && $value < $fieldIndex + count($fieldValues)) { 378 | if ('' != $fieldValues[$value - $fieldIndex]) { 379 | $where['v' . $value] = $fieldValues[$value - $fieldIndex]; 380 | } 381 | } 382 | } 383 | 384 | return $this->model->where($where)->get(); 385 | } 386 | } -------------------------------------------------------------------------------- /tests/Adapter.php: -------------------------------------------------------------------------------- 1 | assertTrue(Permission::driver('other')->addPolicy('writer', 'articles', 'edit')); 13 | $this->assertTrue(Permission::driver('other')->addPolicies([ 14 | ['writer', 'articles', 'list'], 15 | ['writer', 'articles', 'delete'], 16 | ])); 17 | 18 | $this->assertFalse(Permission::driver('other')->addPolicies([ 19 | ['writer', 'articles', 'list'], 20 | ['writer', 'articles', 'delete'], 21 | ])); 22 | 23 | $this->assertTrue(Permission::driver('other')->enforce('writer', 'articles', 'edit')); 24 | $this->assertTrue(Permission::driver('other')->enforce('writer', 'articles', 'delete')); 25 | $this->assertFalse(Permission::driver('other')->enforce('writer', 'articles', 'other')); 26 | 27 | $this->assertTrue(Permission::driver('other')->hasPolicy('writer', 'articles', 'edit')); 28 | $this->assertFalse(Permission::driver('other')->hasPolicy('writer', 'articles', 'other')); 29 | 30 | $this->assertTrue(Permission::driver('other')->removePolicy('writer', 'articles', 'edit')); 31 | $this->assertFalse(Permission::driver('other')->hasPolicy('writer', 'articles', 'edit')); 32 | $this->assertFalse(Permission::driver('other')->enforce('writer', 'articles', 'edit')); 33 | } 34 | 35 | public function testAddOtherRoleForUser() 36 | { 37 | $this->assertFalse(Permission::driver('other')->hasRoleForUser('eve', 'data2')); 38 | Permission::driver('other')->addRoleForUser('eve', 'data2'); 39 | $this->assertTrue(in_array('data2', Permission::driver('other')->getAllRoles())); 40 | $this->assertTrue(Permission::driver('other')->hasRoleForUser('eve', 'data2')); 41 | } 42 | 43 | public function testAddPermissionForUser() 44 | { 45 | $this->assertFalse(Permission::enforce('eve', 'data1', 'read')); 46 | Permission::addPermissionForUser('eve', 'data1', 'read'); 47 | $this->assertTrue(Permission::enforce('eve', 'data1', 'read')); 48 | } 49 | 50 | public function testAddPolicy() 51 | { 52 | $this->assertTrue(Permission::addPolicy('writer', 'articles', 'edit')); 53 | $this->assertTrue(Permission::addPolicies([ 54 | ['writer', 'articles', 'list'], 55 | ['writer', 'articles', 'delete'], 56 | ])); 57 | 58 | $this->assertFalse(Permission::addPolicies([ 59 | ['writer', 'articles', 'list'], 60 | ['writer', 'articles', 'delete'], 61 | ])); 62 | 63 | $this->assertTrue(Permission::enforce('writer', 'articles', 'edit')); 64 | $this->assertTrue(Permission::enforce('writer', 'articles', 'delete')); 65 | $this->assertFalse(Permission::enforce('writer', 'articles', 'other')); 66 | 67 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 68 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'other')); 69 | 70 | $this->assertTrue(Permission::removePolicy('writer', 'articles', 'edit')); 71 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 72 | $this->assertFalse(Permission::enforce('writer', 'articles', 'edit')); 73 | } 74 | 75 | public function testAddRoleForUser() 76 | { 77 | $this->assertFalse(Permission::hasRoleForUser('eve', 'data2')); 78 | Permission::addRoleForUser('eve', 'data2'); 79 | $this->assertTrue(in_array('data2', Permission::getAllRoles())); 80 | $this->assertTrue(Permission::hasRoleForUser('eve', 'data2')); 81 | } 82 | 83 | public function testOtherAddPermissionForUser() 84 | { 85 | $this->assertFalse(Permission::driver('other')->enforce('eve', 'data1', 'read')); 86 | Permission::driver('other')->addPermissionForUser('eve', 'data1', 'read'); 87 | $this->assertTrue(Permission::driver('other')->enforce('eve', 'data1', 'read')); 88 | } 89 | 90 | public function testUpdatePolicy() 91 | { 92 | Permission::addPolicy('writer', 'articles', 'edit'); 93 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 94 | 95 | $result = Permission::updatePolicies( 96 | [['writer', 'articles', 'edit']], 97 | [['writer', 'articles', 'update']] 98 | ); 99 | $this->assertTrue($result); 100 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 101 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'update')); 102 | } 103 | 104 | public function testRemovePolicies() 105 | { 106 | Permission::addPolicies([ 107 | ['writer', 'articles', 'list'], 108 | ['writer', 'articles', 'delete'] 109 | ]); 110 | 111 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'list')); 112 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'delete')); 113 | 114 | $result = Permission::removePolicies([ 115 | ['writer', 'articles', 'list'], 116 | ['writer', 'articles', 'delete'] 117 | ]); 118 | $this->assertTrue($result); 119 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'list')); 120 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'delete')); 121 | } 122 | 123 | public function testGetRolesForUser() 124 | { 125 | Permission::addRoleForUser('alice', 'admin'); 126 | Permission::addRoleForUser('alice', 'editor'); 127 | 128 | $roles = Permission::getRolesForUser('alice'); 129 | $this->assertContains('admin', $roles); 130 | $this->assertContains('editor', $roles); 131 | } 132 | 133 | public function testGetUsersForRole() 134 | { 135 | Permission::addRoleForUser('alice', 'admin'); 136 | Permission::addRoleForUser('bob', 'admin'); 137 | 138 | $users = Permission::getUsersForRole('admin'); 139 | $this->assertContains('alice', $users); 140 | $this->assertContains('bob', $users); 141 | } 142 | 143 | public function testDeleteRoleForUser() 144 | { 145 | Permission::addRoleForUser('alice', 'admin'); 146 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin')); 147 | 148 | $result = Permission::deleteRoleForUser('alice', 'admin'); 149 | $this->assertTrue($result); 150 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 151 | } 152 | 153 | public function testDeletePermissionForUser() 154 | { 155 | Permission::addPermissionForUser('alice', 'data1', 'read'); 156 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 157 | 158 | $result = Permission::deletePermissionForUser('alice', 'data1', 'read'); 159 | $this->assertTrue($result); 160 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 161 | } 162 | 163 | public function testGetPermissionsForUser() 164 | { 165 | Permission::addPermissionForUser('alice', 'data1', 'read'); 166 | Permission::addPermissionForUser('alice', 'data2', 'write'); 167 | 168 | $permissions = Permission::getPermissionsForUser('alice'); 169 | $this->assertContains(['alice', 'data1', 'read'], $permissions); 170 | $this->assertContains(['alice', 'data2', 'write'], $permissions); 171 | } 172 | 173 | public function testHasPermissionForUser() 174 | { 175 | Permission::addPermissionForUser('alice', 'data1', 'read'); 176 | $this->assertTrue(Permission::hasPermissionForUser('alice', 'data1', 'read')); 177 | $this->assertFalse(Permission::hasPermissionForUser('alice', 'data1', 'write')); 178 | } 179 | 180 | public function testGetImplicitRolesForUser() 181 | { 182 | Permission::addRoleForUser('alice', 'admin'); 183 | Permission::addRoleForUser('admin', 'super_admin'); 184 | 185 | $roles = Permission::getImplicitRolesForUser('alice'); 186 | $this->assertContains('admin', $roles); 187 | $this->assertContains('super_admin', $roles); 188 | } 189 | 190 | public function testGetImplicitPermissionsForUser() 191 | { 192 | Permission::addRoleForUser('alice', 'admin'); 193 | Permission::addPermissionForUser('admin', 'data1', 'read'); 194 | 195 | $permissions = Permission::getImplicitPermissionsForUser('alice'); 196 | $this->assertContains(['admin', 'data1', 'read'], $permissions); 197 | } 198 | 199 | public function testDeleteUser() 200 | { 201 | Permission::addRoleForUser('alice', 'admin'); 202 | Permission::addPermissionForUser('alice', 'data1', 'read'); 203 | 204 | $result = Permission::deleteUser('alice'); 205 | $this->assertTrue($result); 206 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 207 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 208 | } 209 | 210 | public function testDeleteRole() 211 | { 212 | Permission::addRoleForUser('alice', 'admin'); 213 | Permission::addRoleForUser('bob', 'admin'); 214 | 215 | $result = Permission::deleteRole('admin'); 216 | $this->assertTrue($result); 217 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 218 | $this->assertFalse(Permission::hasRoleForUser('bob', 'admin')); 219 | } 220 | 221 | public function testDriverManager() 222 | { 223 | $defaultDriver = Permission::getDefaultDriver(); 224 | $this->assertNotEmpty($defaultDriver); 225 | 226 | $allDrivers = Permission::getAllDriver(); 227 | $this->assertIsArray($allDrivers); 228 | } 229 | 230 | public function testEnforceWithInvalidData() 231 | { 232 | $this->assertFalse(Permission::enforce('', '', '')); 233 | $this->assertFalse(Permission::enforce('nonexistent', 'resource', 'action')); 234 | } 235 | 236 | public function testDuplicatePolicyHandling() 237 | { 238 | $result1 = Permission::addPolicy('writer', 'articles', 'edit'); 239 | $this->assertTrue($result1); 240 | 241 | $result2 = Permission::addPolicy('writer', 'articles', 'edit'); 242 | $this->assertFalse($result2); 243 | } 244 | 245 | public function testPolicyWithEmptyValues() 246 | { 247 | Permission::addPolicy('writer', 'articles', ''); 248 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', '')); 249 | 250 | $result = Permission::removePolicy('writer', 'articles', ''); 251 | $this->assertTrue($result); 252 | } 253 | 254 | public function testLargePolicySet() 255 | { 256 | $policies = []; 257 | for ($i = 0; $i < 100; $i++) { 258 | $policies[] = ['user' . $i, 'resource' . $i, 'action' . $i]; 259 | } 260 | 261 | $result = Permission::addPolicies($policies); 262 | $this->assertTrue($result); 263 | 264 | for ($i = 0; $i < 100; $i++) { 265 | $this->assertTrue(Permission::enforce('user' . $i, 'resource' . $i, 'action' . $i)); 266 | } 267 | } 268 | 269 | public function testRemoveFilteredPolicy() 270 | { 271 | Permission::addPolicies([ 272 | ['alice', 'data1', 'read'], 273 | ['alice', 'data2', 'read'], 274 | ['bob', 'data1', 'read'] 275 | ]); 276 | 277 | $result = Permission::removeFilteredPolicy(1, 'p', 0, 'alice'); 278 | $this->assertTrue($result); 279 | 280 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 281 | $this->assertFalse(Permission::enforce('alice', 'data2', 'read')); 282 | $this->assertTrue(Permission::enforce('bob', 'data1', 'read')); 283 | } 284 | 285 | public function testConfigAccess() 286 | { 287 | $config = Permission::getConfig('default'); 288 | $this->assertNotEmpty($config); 289 | 290 | $allConfig = Permission::getConfig(); 291 | $this->assertNotEmpty($allConfig); 292 | } 293 | 294 | public function testClear() 295 | { 296 | Permission::addPolicy('writer', 'articles', 'edit'); 297 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 298 | 299 | Permission::clear(); 300 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Adapter/DatabaseAdapter.php: -------------------------------------------------------------------------------- 1 | model = new RuleModel([], $driver); 56 | } 57 | 58 | /** 59 | * Filter the rule. 60 | * 61 | * @param array $rule 62 | * @return array 63 | */ 64 | public function filterRule(array $rule): array 65 | { 66 | $rule = array_values($rule); 67 | 68 | $i = count($rule) - 1; 69 | for (; $i >= 0; $i--) { 70 | if ($rule[$i] != '' && !is_null($rule[$i])) { 71 | break; 72 | } 73 | } 74 | 75 | return array_slice($rule, 0, $i + 1); 76 | } 77 | 78 | /** 79 | * savePolicyLine function. 80 | * 81 | * @param string $ptype 82 | * @param array $rule 83 | * 84 | * @return void 85 | */ 86 | public function savePolicyLine(string $ptype, array $rule): void 87 | { 88 | $col['ptype'] = $ptype; 89 | foreach ($rule as $key => $value) { 90 | $col['v' . strval($key) . ''] = $value; 91 | } 92 | $this->model->insert($col); 93 | } 94 | 95 | /** 96 | * loads all policy rules from the storage. 97 | * 98 | * @param Model $model 99 | * @throws DataNotFoundException 100 | * @throws DbException 101 | * @throws ModelNotFoundException 102 | */ 103 | public function loadPolicy(Model $model): void 104 | { 105 | $rows = $this->model->field(['ptype', 'v0', 'v1', 'v2', 'v3', 'v4', 'v5'])->select()->toArray(); 106 | foreach ($rows as $row) { 107 | // $line = implode(', ', array_filter(array_slice($row, 1), function ($val) { 108 | // return '' != $val && !is_null($val); 109 | // })); 110 | // $this->loadPolicyLine(trim($line), $model); 111 | $this->loadPolicyArray($this->filterRule($row), $model); 112 | } 113 | } 114 | 115 | /** 116 | * saves all policy rules to the storage. 117 | * 118 | * @param Model $model 119 | */ 120 | public function savePolicy(Model $model): void 121 | { 122 | foreach ($model['p'] as $ptype => $ast) { 123 | foreach ($ast->policy as $rule) { 124 | $this->savePolicyLine($ptype, $rule); 125 | } 126 | } 127 | 128 | foreach ($model['g'] as $ptype => $ast) { 129 | foreach ($ast->policy as $rule) { 130 | $this->savePolicyLine($ptype, $rule); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * adds a policy rule to the storage. 137 | * This is part of the Auto-Save feature. 138 | * 139 | * @param string $sec 140 | * @param string $ptype 141 | * @param array $rule 142 | */ 143 | public function addPolicy(string $sec, string $ptype, array $rule): void 144 | { 145 | $this->savePolicyLine($ptype, $rule); 146 | } 147 | 148 | /** 149 | * Adds a policy rules to the storage. 150 | * This is part of the Auto-Save feature. 151 | * 152 | * @param string $sec 153 | * @param string $ptype 154 | * @param string[][] $rules 155 | */ 156 | public function addPolicies(string $sec, string $ptype, array $rules): void 157 | { 158 | $cols = []; 159 | $i = 0; 160 | 161 | foreach ($rules as $rule) { 162 | $temp['ptype'] = $ptype; 163 | foreach ($rule as $key => $value) { 164 | $temp['v' . strval($key)] = $value; 165 | } 166 | $cols[$i++] = $temp; 167 | $temp = []; 168 | } 169 | $this->model->insertAll($cols); 170 | } 171 | 172 | /** 173 | * This is part of the Auto-Save feature. 174 | * 175 | * @param string $sec 176 | * @param string $ptype 177 | * @param array $rule 178 | * @throws DataNotFoundException 179 | * @throws DbException 180 | * @throws ModelNotFoundException 181 | */ 182 | public function removePolicy(string $sec, string $ptype, array $rule): void 183 | { 184 | $count = 0; 185 | 186 | $instance = $this->model->where('ptype', $ptype); 187 | 188 | foreach ($rule as $key => $value) { 189 | $instance->where('v' . strval($key), $value); 190 | } 191 | 192 | foreach ($instance->select() as $model) { 193 | if ($model->delete()) { 194 | ++$count; 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * Removes policy rules from the storage. 201 | * This is part of the Auto-Save feature. 202 | * 203 | * @param string $sec 204 | * @param string $ptype 205 | * @param string[][] $rules 206 | */ 207 | public function removePolicies(string $sec, string $ptype, array $rules): void 208 | { 209 | Db::transaction(function () use ($sec, $ptype, $rules) { 210 | foreach ($rules as $rule) { 211 | $this->removePolicy($sec, $ptype, $rule); 212 | } 213 | }); 214 | } 215 | 216 | /** 217 | * @param string $sec 218 | * @param string $ptype 219 | * @param int $fieldIndex 220 | * @param string|null ...$fieldValues 221 | * @return array 222 | * @throws DbException 223 | * @throws ModelNotFoundException 224 | * @throws DataNotFoundException 225 | */ 226 | public function _removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, ?string ...$fieldValues): array 227 | { 228 | $count = 0; 229 | $removedRules = []; 230 | 231 | $instance = $this->model->where('ptype', $ptype); 232 | foreach (range(0, 5) as $value) { 233 | if ($fieldIndex <= $value && $value < $fieldIndex + count($fieldValues)) { 234 | if ('' != $fieldValues[$value - $fieldIndex]) { 235 | $instance->where('v' . strval($value), $fieldValues[$value - $fieldIndex]); 236 | } 237 | } 238 | } 239 | 240 | foreach ($instance->select() as $model) { 241 | $item = $model->hidden(['id', 'ptype'])->toArray(); 242 | $item = $this->filterRule($item); 243 | $removedRules[] = $item; 244 | if ($model->delete()) { 245 | ++$count; 246 | } 247 | } 248 | 249 | return $removedRules; 250 | } 251 | 252 | /** 253 | * RemoveFilteredPolicy removes policy rules that match the filter from the storage. 254 | * This is part of the Auto-Save feature. 255 | * 256 | * @param string $sec 257 | * @param string $ptype 258 | * @param int $fieldIndex 259 | * @param string ...$fieldValues 260 | * @throws DataNotFoundException 261 | * @throws DbException 262 | * @throws ModelNotFoundException 263 | */ 264 | public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, string ...$fieldValues): void 265 | { 266 | $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues); 267 | } 268 | 269 | /** 270 | * Updates a policy rule from storage. 271 | * This is part of the Auto-Save feature. 272 | * 273 | * @param string $sec 274 | * @param string $ptype 275 | * @param string[] $oldRule 276 | * @param string[] $newPolicy 277 | * @throws DataNotFoundException 278 | * @throws DbException 279 | * @throws ModelNotFoundException 280 | */ 281 | public function updatePolicy(string $sec, string $ptype, array $oldRule, array $newPolicy): void 282 | { 283 | $instance = $this->model->where('ptype', $ptype); 284 | foreach ($oldRule as $key => $value) { 285 | $instance->where('v' . strval($key), $value); 286 | } 287 | $instance = $instance->find(); 288 | 289 | foreach ($newPolicy as $key => $value) { 290 | $column = 'v' . strval($key); 291 | $instance->$column = $value; 292 | } 293 | 294 | $instance->save(); 295 | } 296 | 297 | /** 298 | * UpdatePolicies updates some policy rules to storage, like db, redis. 299 | * 300 | * @param string $sec 301 | * @param string $ptype 302 | * @param string[][] $oldRules 303 | * @param string[][] $newRules 304 | * @return void 305 | */ 306 | public function updatePolicies(string $sec, string $ptype, array $oldRules, array $newRules): void 307 | { 308 | Db::transaction(function () use ($sec, $ptype, $oldRules, $newRules) { 309 | foreach ($oldRules as $i => $oldRule) { 310 | $this->updatePolicy($sec, $ptype, $oldRule, $newRules[$i]); 311 | } 312 | }); 313 | } 314 | 315 | /** 316 | * UpdateFilteredPolicies deletes old rules and adds new rules. 317 | * 318 | * @param string $sec 319 | * @param string $ptype 320 | * @param array $newPolicies 321 | * @param integer $fieldIndex 322 | * @param string ...$fieldValues 323 | * @return array 324 | */ 325 | public function updateFilteredPolicies(string $sec, string $ptype, array $newPolicies, int $fieldIndex, string ...$fieldValues): array 326 | { 327 | 328 | $oldRules = []; 329 | DB::transaction(function () use ($sec, $ptype, $fieldIndex, $fieldValues, $newPolicies, &$oldRules) { 330 | $oldRules = $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues); 331 | $this->addPolicies($sec, $ptype, $newPolicies); 332 | }); 333 | 334 | return $oldRules; 335 | } 336 | 337 | /** 338 | * Returns true if the loaded policy has been filtered. 339 | * 340 | * @return bool 341 | */ 342 | public function isFiltered(): bool 343 | { 344 | return $this->filtered; 345 | } 346 | 347 | /** 348 | * Sets filtered parameter. 349 | * 350 | * @param bool $filtered 351 | */ 352 | public function setFiltered(bool $filtered): void 353 | { 354 | $this->filtered = $filtered; 355 | } 356 | 357 | /** 358 | * Loads only policy rules that match the filter. 359 | * 360 | * @param Model $model 361 | * @param mixed $filter 362 | * @throws InvalidFilterTypeException 363 | * @throws DataNotFoundException 364 | * @throws DbException 365 | * @throws ModelNotFoundException 366 | */ 367 | public function loadFilteredPolicy(Model $model, $filter): void 368 | { 369 | $instance = $this->model; 370 | 371 | if (is_string($filter)) { 372 | $instance = $instance->whereRaw($filter); 373 | } elseif ($filter instanceof Filter) { 374 | foreach ($filter->p as $k => $v) { 375 | $instance = $instance->where($v, $filter->g[$k]); 376 | } 377 | } elseif ($filter instanceof \Closure) { 378 | $instance = $instance->where($filter); 379 | } else { 380 | throw new InvalidFilterTypeException('invalid filter type'); 381 | } 382 | $rows = $instance->select()->hidden(['id'])->toArray(); 383 | foreach ($rows as $row) { 384 | $row = array_filter($row, function ($value) { 385 | return !is_null($value) && $value !== ''; 386 | }); 387 | $line = implode(', ', array_filter($row, function ($val) { 388 | return '' != $val && !is_null($val); 389 | })); 390 | $this->loadPolicyLine(trim($line), $model); 391 | } 392 | $this->setFiltered(true); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /tests/PermissionTest.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'casbin' => [ 21 | 'webman-permission' => [ 22 | 'permission' => [ 23 | 'default' => 'default', 24 | 'default' => [ 25 | 'model' => [ 26 | 'config_type' => 'text', 27 | 'config_text' => ' 28 | [request_definition] 29 | r = sub, obj, act 30 | 31 | [policy_definition] 32 | p = sub, obj, act 33 | 34 | [role_definition] 35 | g = _, _ 36 | 37 | [policy_effect] 38 | e = some(where (p.eft == allow)) 39 | 40 | [matchers] 41 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 42 | ', 43 | ], 44 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 45 | ], 46 | 'other' => [ 47 | 'model' => [ 48 | 'config_type' => 'text', 49 | 'config_text' => ' 50 | [request_definition] 51 | r = sub, obj, act 52 | 53 | [policy_definition] 54 | p = sub, obj, act 55 | 56 | [role_definition] 57 | g = _, _ 58 | 59 | [policy_effect] 60 | e = some(where (p.eft == allow)) 61 | 62 | [matchers] 63 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 64 | ', 65 | ], 66 | 'adapter' => \Casbin\WebmanPermission\Adapter\DatabaseAdapter::class, 67 | 'adapter_config' => [ 68 | 'table' => 'other_casbin_rule' 69 | ], 70 | ], 71 | 'log' => [ 72 | 'enabled' => false, 73 | 'logger' => 'casbin', 74 | 'path' => '/tmp/casbin.log', 75 | ], 76 | ] 77 | ] 78 | ] 79 | ] 80 | ]; 81 | 82 | Permission::clear(); 83 | } 84 | 85 | public function testDriverMethod() 86 | { 87 | $enforcer = Permission::driver(); 88 | $this->assertInstanceOf(\Casbin\Enforcer::class, $enforcer); 89 | 90 | $otherEnforcer = Permission::driver('other'); 91 | $this->assertInstanceOf(\Casbin\Enforcer::class, $otherEnforcer); 92 | } 93 | 94 | public function testStaticMethodCall() 95 | { 96 | $result = Permission::addPolicy('writer', 'articles', 'edit'); 97 | $this->assertTrue($result); 98 | 99 | $result = Permission::enforce('writer', 'articles', 'edit'); 100 | $this->assertTrue($result); 101 | } 102 | 103 | public function testGetAllDriver() 104 | { 105 | Permission::driver(); 106 | Permission::driver('other'); 107 | 108 | $drivers = Permission::getAllDriver(); 109 | $this->assertIsArray($drivers); 110 | $this->assertGreaterThanOrEqual(2, count($drivers)); 111 | } 112 | 113 | public function testGetDefaultDriver() 114 | { 115 | $driver = Permission::getDefaultDriver(); 116 | $this->assertNotEmpty($driver); 117 | } 118 | 119 | public function testGetConfig() 120 | { 121 | $config = Permission::getConfig('default'); 122 | $this->assertNotEmpty($config); 123 | 124 | $allConfig = Permission::getConfig(); 125 | $this->assertNotEmpty($allConfig); 126 | } 127 | 128 | public function testClear() 129 | { 130 | Permission::addPolicy('writer', 'articles', 'edit'); 131 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 132 | 133 | Permission::clear(); 134 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 135 | } 136 | 137 | public function testAddFunction() 138 | { 139 | $result = Permission::addFunction('test_function', function($a, $b) { 140 | return $a + $b; 141 | }); 142 | 143 | $this->assertTrue($result); 144 | } 145 | 146 | public function testDomainBasedPermissions() 147 | { 148 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 149 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin', 'domain1')); 150 | 151 | $roles = Permission::getRolesForUserInDomain('alice', 'domain1'); 152 | $this->assertContains('admin', $roles); 153 | 154 | $users = Permission::getUsersForRoleInDomain('admin', 'domain1'); 155 | $this->assertContains('alice', $users); 156 | } 157 | 158 | public function testDomainBasedPermissionOperations() 159 | { 160 | Permission::addPermissionForUser('alice', 'data1', 'read', 'domain1'); 161 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read', 'domain1')); 162 | 163 | $permissions = Permission::getPermissionsForUserInDomain('alice', 'domain1'); 164 | $this->assertContains(['alice', 'data1', 'read'], $permissions); 165 | } 166 | 167 | public function testDomainRoleDeletion() 168 | { 169 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 170 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin', 'domain1')); 171 | 172 | $result = Permission::deleteRoleForUserInDomain('alice', 'admin', 'domain1'); 173 | $this->assertTrue($result); 174 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin', 'domain1')); 175 | } 176 | 177 | public function testDeleteRolesForUserInDomain() 178 | { 179 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 180 | Permission::addRoleForUserInDomain('alice', 'editor', 'domain1'); 181 | 182 | $result = Permission::deleteRolesForUserInDomain('alice', 'domain1'); 183 | $this->assertTrue($result); 184 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin', 'domain1')); 185 | $this->assertFalse(Permission::hasRoleForUser('alice', 'editor', 'domain1')); 186 | } 187 | 188 | public function testGetAllUsersByDomain() 189 | { 190 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 191 | Permission::addRoleForUserInDomain('bob', 'admin', 'domain1'); 192 | 193 | $users = Permission::getAllUsersByDomain('domain1'); 194 | $this->assertContains('alice', $users); 195 | $this->assertContains('bob', $users); 196 | } 197 | 198 | public function testDeleteAllUsersByDomain() 199 | { 200 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 201 | Permission::addRoleForUserInDomain('bob', 'admin', 'domain1'); 202 | 203 | $result = Permission::deleteAllUsersByDomain('domain1'); 204 | $this->assertTrue($result); 205 | $this->assertEmpty(Permission::getAllUsersByDomain('domain1')); 206 | } 207 | 208 | public function testDeleteDomains() 209 | { 210 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 211 | Permission::addRoleForUserInDomain('bob', 'admin', 'domain2'); 212 | 213 | $result = Permission::deleteDomains('domain1', 'domain2'); 214 | $this->assertTrue($result); 215 | $this->assertEmpty(Permission::getAllUsersByDomain('domain1')); 216 | $this->assertEmpty(Permission::getAllUsersByDomain('domain2')); 217 | } 218 | 219 | public function testGetImplicitUsersForRole() 220 | { 221 | Permission::addRoleForUser('alice', 'admin'); 222 | Permission::addRoleForUser('admin', 'super_admin'); 223 | 224 | $users = Permission::getImplicitUsersForRole('super_admin'); 225 | $this->assertContains('alice', $users); 226 | } 227 | 228 | public function testGetImplicitUsersForPermission() 229 | { 230 | Permission::addPermissionForUser('alice', 'data1', 'read'); 231 | Permission::addRoleForUser('bob', 'admin'); 232 | Permission::addPermissionForUser('admin', 'data1', 'read'); 233 | 234 | $users = Permission::getImplicitUsersForPermission('data1', 'read'); 235 | $this->assertContains('alice', $users); 236 | $this->assertContains('bob', $users); 237 | } 238 | 239 | public function testGetImplicitResourcesForUser() 240 | { 241 | Permission::addRoleForUser('alice', 'admin'); 242 | Permission::addPermissionForUser('admin', 'data1', 'read'); 243 | Permission::addPermissionForUser('admin', 'data2', 'write'); 244 | 245 | $resources = Permission::getImplicitResourcesForUser('alice'); 246 | $this->assertContains(['data1', 'read'], $resources); 247 | $this->assertContains(['data2', 'write'], $resources); 248 | } 249 | 250 | public function testBatchRoleOperations() 251 | { 252 | $result = Permission::addRolesForUser('alice', ['admin', 'editor']); 253 | $this->assertTrue($result); 254 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin')); 255 | $this->assertTrue(Permission::hasRoleForUser('alice', 'editor')); 256 | } 257 | 258 | public function testBatchPermissionOperations() 259 | { 260 | $result = Permission::addPermissionsForUser('alice', [ 261 | ['data1', 'read'], 262 | ['data2', 'write'] 263 | ]); 264 | $this->assertTrue($result); 265 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 266 | $this->assertTrue(Permission::enforce('alice', 'data2', 'write')); 267 | } 268 | 269 | public function testDeletePermissionsForUser() 270 | { 271 | Permission::addPermissionForUser('alice', 'data1', 'read'); 272 | Permission::addPermissionForUser('alice', 'data2', 'write'); 273 | 274 | $result = Permission::deletePermissionsForUser('alice'); 275 | $this->assertTrue($result); 276 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 277 | $this->assertFalse(Permission::enforce('alice', 'data2', 'write')); 278 | } 279 | 280 | public function testDeleteRolesForUser() 281 | { 282 | Permission::addRoleForUser('alice', 'admin'); 283 | Permission::addRoleForUser('alice', 'editor'); 284 | 285 | $result = Permission::deleteRolesForUser('alice'); 286 | $this->assertTrue($result); 287 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 288 | $this->assertFalse(Permission::hasRoleForUser('alice', 'editor')); 289 | } 290 | 291 | public function testGetAllRoles() 292 | { 293 | Permission::addRoleForUser('alice', 'admin'); 294 | Permission::addRoleForUser('bob', 'editor'); 295 | 296 | $roles = Permission::getAllRoles(); 297 | $this->assertContains('admin', $roles); 298 | $this->assertContains('editor', $roles); 299 | } 300 | 301 | public function testGetPolicy() 302 | { 303 | Permission::addPolicy('writer', 'articles', 'edit'); 304 | Permission::addPolicy('reader', 'articles', 'read'); 305 | 306 | $policy = Permission::getPolicy(); 307 | $this->assertContains(['writer', 'articles', 'edit'], $policy); 308 | $this->assertContains(['reader', 'articles', 'read'], $policy); 309 | } 310 | 311 | public function testPermissionWithSpecialCharacters() 312 | { 313 | Permission::addPolicy('user@domain.com', 'data#1', 'action:read'); 314 | $this->assertTrue(Permission::enforce('user@domain.com', 'data#1', 'action:read')); 315 | } 316 | 317 | public function testEmptyPolicyOperations() 318 | { 319 | $result = Permission::removePolicy('nonexistent', 'resource', 'action'); 320 | $this->assertTrue($result); 321 | 322 | $result = Permission::removePolicies([ 323 | ['nonexistent', 'resource', 'action'] 324 | ]); 325 | $this->assertTrue($result); 326 | } 327 | 328 | public function testConfigWithDefaults() 329 | { 330 | $config = Permission::getConfig('nonexistent', 'default_value'); 331 | $this->assertEquals('default_value', $config); 332 | } 333 | 334 | public function testDriverCaching() 335 | { 336 | $driver1 = Permission::driver(); 337 | $driver2 = Permission::driver(); 338 | 339 | $this->assertSame($driver1, $driver2); 340 | } 341 | 342 | public function testInvalidDriverAccess() 343 | { 344 | $this->expectException(CasbinException::class); 345 | Permission::driver('nonexistent_driver'); 346 | } 347 | } -------------------------------------------------------------------------------- /tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Permission::enforce('alice', 'data1', 'read')); 35 | $this->assertTrue(Permission::enforce('alice', 'data1', 'write')); 36 | $this->assertTrue(Permission::enforce('alice', 'data1', 'delete')); 37 | 38 | $this->assertTrue(Permission::enforce('bob', 'data1', 'read')); 39 | $this->assertFalse(Permission::enforce('bob', 'data1', 'write')); 40 | $this->assertTrue(Permission::enforce('bob', 'data2', 'read')); 41 | 42 | $this->assertFalse(Permission::enforce('charlie', 'data1', 'read')); 43 | $this->assertTrue(Permission::enforce('charlie', 'data2', 'read')); 44 | 45 | $adminRoles = Permission::getRolesForUser('alice'); 46 | $this->assertContains('admin', $adminRoles); 47 | 48 | $adminUsers = Permission::getUsersForRole('admin'); 49 | $this->assertContains('alice', $adminUsers); 50 | 51 | $alicePermissions = Permission::getPermissionsForUser('alice'); 52 | $this->assertContains(['admin', 'data1', 'read'], $alicePermissions); 53 | $this->assertContains(['admin', 'data1', 'write'], $alicePermissions); 54 | } 55 | 56 | public function testHierarchicalRBAC() 57 | { 58 | Permission::addRoleForUser('alice', 'user'); 59 | Permission::addRoleForUser('user', 'admin'); 60 | Permission::addRoleForUser('admin', 'super_admin'); 61 | 62 | Permission::addPermissionForUser('super_admin', '*', '*'); 63 | 64 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 65 | $this->assertTrue(Permission::enforce('alice', 'data2', 'write')); 66 | 67 | $implicitRoles = Permission::getImplicitRolesForUser('alice'); 68 | $this->assertContains('user', $implicitRoles); 69 | $this->assertContains('admin', $implicitRoles); 70 | $this->assertContains('super_admin', $implicitRoles); 71 | 72 | $implicitPermissions = Permission::getImplicitPermissionsForUser('alice'); 73 | $this->assertContains(['super_admin', '*', '*'], $implicitPermissions); 74 | } 75 | 76 | public function testDomainBasedRBAC() 77 | { 78 | Permission::addRoleForUserInDomain('alice', 'admin', 'domain1'); 79 | Permission::addRoleForUserInDomain('alice', 'user', 'domain2'); 80 | Permission::addRoleForUserInDomain('bob', 'admin', 'domain2'); 81 | 82 | Permission::addPermissionForUser('admin', 'data1', 'read', 'domain1'); 83 | Permission::addPermissionForUser('admin', 'data2', 'write', 'domain2'); 84 | 85 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read', 'domain1')); 86 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read', 'domain2')); 87 | 88 | $this->assertFalse(Permission::enforce('alice', 'data2', 'write', 'domain1')); 89 | $this->assertTrue(Permission::enforce('alice', 'data2', 'write', 'domain2')); 90 | 91 | $this->assertTrue(Permission::enforce('bob', 'data2', 'write', 'domain2')); 92 | $this->assertFalse(Permission::enforce('bob', 'data1', 'read', 'domain1')); 93 | 94 | $domain1Users = Permission::getAllUsersByDomain('domain1'); 95 | $this->assertContains('alice', $domain1Users); 96 | 97 | $domain2Users = Permission::getAllUsersByDomain('domain2'); 98 | $this->assertContains('alice', $domain2Users); 99 | $this->assertContains('bob', $domain2Users); 100 | } 101 | 102 | public function testResourceBasedAccessControl() 103 | { 104 | Permission::addPolicy('writer', 'article_1', 'edit'); 105 | Permission::addPolicy('writer', 'article_2', 'edit'); 106 | Permission::addPolicy('reader', 'article_1', 'read'); 107 | Permission::addPolicy('reader', 'article_2', 'read'); 108 | 109 | Permission::addRoleForUser('alice', 'writer'); 110 | Permission::addRoleForUser('bob', 'reader'); 111 | 112 | $this->assertTrue(Permission::enforce('alice', 'article_1', 'edit')); 113 | $this->assertTrue(Permission::enforce('alice', 'article_2', 'edit')); 114 | $this->assertTrue(Permission::enforce('alice', 'article_1', 'read')); 115 | 116 | $this->assertTrue(Permission::enforce('bob', 'article_1', 'read')); 117 | $this->assertTrue(Permission::enforce('bob', 'article_2', 'read')); 118 | $this->assertFalse(Permission::enforce('bob', 'article_1', 'edit')); 119 | } 120 | 121 | public function testDynamicPermissionAssignment() 122 | { 123 | Permission::addRoleForUser('alice', 'admin'); 124 | Permission::addRoleForUser('bob', 'user'); 125 | 126 | $this->assertFalse(Permission::enforce('alice', 'new_resource', 'read')); 127 | $this->assertFalse(Permission::enforce('bob', 'new_resource', 'read')); 128 | 129 | Permission::addPermissionForUser('admin', 'new_resource', 'read'); 130 | 131 | $this->assertTrue(Permission::enforce('alice', 'new_resource', 'read')); 132 | $this->assertFalse(Permission::enforce('bob', 'new_resource', 'read')); 133 | 134 | Permission::deletePermissionForUser('admin', 'new_resource', 'read'); 135 | 136 | $this->assertFalse(Permission::enforce('alice', 'new_resource', 'read')); 137 | $this->assertFalse(Permission::enforce('bob', 'new_resource', 'read')); 138 | } 139 | 140 | public function testBatchOperations() 141 | { 142 | $users = ['alice', 'bob', 'charlie', 'david']; 143 | $roles = ['admin', 'editor', 'writer', 'reader']; 144 | 145 | foreach ($users as $index => $user) { 146 | Permission::addRoleForUser($user, $roles[$index]); 147 | } 148 | 149 | $policies = [ 150 | ['admin', '*', '*'], 151 | ['editor', 'articles', 'edit'], 152 | ['writer', 'articles', 'write'], 153 | ['reader', 'articles', 'read'] 154 | ]; 155 | 156 | Permission::addPolicies($policies); 157 | 158 | $this->assertTrue(Permission::enforce('alice', 'articles', 'edit')); 159 | $this->assertTrue(Permission::enforce('bob', 'articles', 'edit')); 160 | $this->assertTrue(Permission::enforce('charlie', 'articles', 'write')); 161 | $this->assertTrue(Permission::enforce('david', 'articles', 'read')); 162 | 163 | Permission::removePolicies($policies); 164 | 165 | $this->assertFalse(Permission::enforce('alice', 'articles', 'edit')); 166 | $this->assertFalse(Permission::enforce('bob', 'articles', 'edit')); 167 | $this->assertFalse(Permission::enforce('charlie', 'articles', 'write')); 168 | $this->assertFalse(Permission::enforce('david', 'articles', 'read')); 169 | } 170 | 171 | public function testUserLifecycle() 172 | { 173 | Permission::addRoleForUser('alice', 'admin'); 174 | Permission::addRoleForUser('alice', 'editor'); 175 | Permission::addPermissionForUser('alice', 'data1', 'read'); 176 | Permission::addPermissionForUser('alice', 'data2', 'write'); 177 | 178 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin')); 179 | $this->assertTrue(Permission::hasRoleForUser('alice', 'editor')); 180 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 181 | $this->assertTrue(Permission::enforce('alice', 'data2', 'write')); 182 | 183 | $result = Permission::deleteUser('alice'); 184 | $this->assertTrue($result); 185 | 186 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 187 | $this->assertFalse(Permission::hasRoleForUser('alice', 'editor')); 188 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 189 | $this->assertFalse(Permission::enforce('alice', 'data2', 'write')); 190 | } 191 | 192 | public function testRoleLifecycle() 193 | { 194 | Permission::addRoleForUser('alice', 'admin'); 195 | Permission::addRoleForUser('bob', 'admin'); 196 | Permission::addRoleForUser('charlie', 'admin'); 197 | 198 | Permission::addPermissionForUser('admin', 'data1', 'read'); 199 | Permission::addPermissionForUser('admin', 'data2', 'write'); 200 | 201 | $this->assertTrue(Permission::hasRoleForUser('alice', 'admin')); 202 | $this->assertTrue(Permission::hasRoleForUser('bob', 'admin')); 203 | $this->assertTrue(Permission::hasRoleForUser('charlie', 'admin')); 204 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 205 | $this->assertTrue(Permission::enforce('bob', 'data2', 'write')); 206 | 207 | $result = Permission::deleteRole('admin'); 208 | $this->assertTrue($result); 209 | 210 | $this->assertFalse(Permission::hasRoleForUser('alice', 'admin')); 211 | $this->assertFalse(Permission::hasRoleForUser('bob', 'admin')); 212 | $this->assertFalse(Permission::hasRoleForUser('charlie', 'admin')); 213 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 214 | $this->assertFalse(Permission::enforce('bob', 'data2', 'write')); 215 | } 216 | 217 | public function testMultiDriverIntegration() 218 | { 219 | Permission::driver('default')->addRoleForUser('alice', 'admin'); 220 | Permission::driver('other')->addRoleForUser('bob', 'admin'); 221 | 222 | $this->assertTrue(Permission::driver('default')->hasRoleForUser('alice', 'admin')); 223 | $this->assertFalse(Permission::driver('default')->hasRoleForUser('bob', 'admin')); 224 | 225 | $this->assertFalse(Permission::driver('other')->hasRoleForUser('alice', 'admin')); 226 | $this->assertTrue(Permission::driver('other')->hasRoleForUser('bob', 'admin')); 227 | 228 | Permission::driver('default')->addPermissionForUser('admin', 'data1', 'read'); 229 | Permission::driver('other')->addPermissionForUser('admin', 'data2', 'read'); 230 | 231 | $this->assertTrue(Permission::driver('default')->enforce('alice', 'data1', 'read')); 232 | $this->assertFalse(Permission::driver('default')->enforce('alice', 'data2', 'read')); 233 | 234 | $this->assertFalse(Permission::driver('other')->enforce('bob', 'data1', 'read')); 235 | $this->assertTrue(Permission::driver('other')->enforce('bob', 'data2', 'read')); 236 | } 237 | 238 | public function testPolicyUpdateWorkflow() 239 | { 240 | Permission::addPolicy('writer', 'articles', 'edit'); 241 | Permission::addPolicy('writer', 'articles', 'delete'); 242 | 243 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'edit')); 244 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'delete')); 245 | 246 | $result = Permission::updatePolicies( 247 | [['writer', 'articles', 'edit'], ['writer', 'articles', 'delete']], 248 | [['writer', 'articles', 'update'], ['writer', 'articles', 'remove']] 249 | ); 250 | 251 | $this->assertTrue($result); 252 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'edit')); 253 | $this->assertFalse(Permission::hasPolicy('writer', 'articles', 'delete')); 254 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'update')); 255 | $this->assertTrue(Permission::hasPolicy('writer', 'articles', 'remove')); 256 | } 257 | 258 | public function testFilteredPolicyManagement() 259 | { 260 | Permission::addPolicies([ 261 | ['alice', 'data1', 'read'], 262 | ['alice', 'data2', 'read'], 263 | ['alice', 'data3', 'write'], 264 | ['bob', 'data1', 'read'], 265 | ['bob', 'data2', 'write'] 266 | ]); 267 | 268 | $result = Permission::removeFilteredPolicy(1, 'p', 0, 'alice'); 269 | $this->assertTrue($result); 270 | 271 | $this->assertFalse(Permission::enforce('alice', 'data1', 'read')); 272 | $this->assertFalse(Permission::enforce('alice', 'data2', 'read')); 273 | $this->assertFalse(Permission::enforce('alice', 'data3', 'write')); 274 | $this->assertTrue(Permission::enforce('bob', 'data1', 'read')); 275 | $this->assertTrue(Permission::enforce('bob', 'data2', 'write')); 276 | 277 | $result = Permission::removeFilteredPolicy(1, 'p', 2, 'read'); 278 | $this->assertTrue($result); 279 | 280 | $this->assertFalse(Permission::enforce('bob', 'data1', 'read')); 281 | $this->assertTrue(Permission::enforce('bob', 'data2', 'write')); 282 | } 283 | 284 | public function testPermissionInheritance() 285 | { 286 | Permission::addRoleForUser('alice', 'user'); 287 | Permission::addRoleForUser('user', 'admin'); 288 | Permission::addRoleForUser('admin', 'super_admin'); 289 | 290 | Permission::addPermissionForUser('user', 'data1', 'read'); 291 | Permission::addPermissionForUser('admin', 'data2', 'write'); 292 | Permission::addPermissionForUser('super_admin', 'data3', 'delete'); 293 | 294 | $this->assertTrue(Permission::enforce('alice', 'data1', 'read')); 295 | $this->assertTrue(Permission::enforce('alice', 'data2', 'write')); 296 | $this->assertTrue(Permission::enforce('alice', 'data3', 'delete')); 297 | 298 | $implicitUsers = Permission::getImplicitUsersForPermission('data1', 'read'); 299 | $this->assertContains('alice', $implicitUsers); 300 | 301 | $implicitUsers = Permission::getImplicitUsersForPermission('data2', 'write'); 302 | $this->assertContains('alice', $implicitUsers); 303 | 304 | $implicitUsers = Permission::getImplicitUsersForPermission('data3', 'delete'); 305 | $this->assertContains('alice', $implicitUsers); 306 | } 307 | 308 | public function testComplexDomainScenario() 309 | { 310 | $domains = ['sales', 'marketing', 'hr']; 311 | $users = ['alice', 'bob', 'charlie']; 312 | 313 | foreach ($domains as $domain) { 314 | foreach ($users as $user) { 315 | Permission::addRoleForUserInDomain($user, 'manager', $domain); 316 | Permission::addPermissionForUser('manager', 'reports', 'read', $domain); 317 | } 318 | } 319 | 320 | $this->assertTrue(Permission::enforce('alice', 'reports', 'read', 'sales')); 321 | $this->assertTrue(Permission::enforce('bob', 'reports', 'read', 'marketing')); 322 | $this->assertTrue(Permission::enforce('charlie', 'reports', 'read', 'hr')); 323 | 324 | $this->assertFalse(Permission::enforce('alice', 'reports', 'read', 'marketing')); 325 | $this->assertFalse(Permission::enforce('bob', 'reports', 'read', 'hr')); 326 | $this->assertFalse(Permission::enforce('charlie', 'reports', 'read', 'sales')); 327 | 328 | $result = Permission::deleteAllUsersByDomain('sales'); 329 | $this->assertTrue($result); 330 | 331 | $this->assertFalse(Permission::enforce('alice', 'reports', 'read', 'sales')); 332 | $this->assertTrue(Permission::enforce('bob', 'reports', 'read', 'marketing')); 333 | $this->assertTrue(Permission::enforce('charlie', 'reports', 'read', 'hr')); 334 | } 335 | 336 | protected function tearDown(): void 337 | { 338 | Permission::clear(); 339 | } 340 | } --------------------------------------------------------------------------------