From 291d3241aa6f9ea9cfa1ce66287ea5d0b483304f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 6 Mar 2019 20:56:20 +0800 Subject: [PATCH] first commit --- .gitignore | 6 + .travis.yml | 72 ++++ LICENSE | 201 +++++++++++ README.md | 327 ++++++++++++++++++ composer.json | 42 +++ config/lauthz-rbac-model.conf | 14 + config/lauthz.php | 60 ++++ .../2019_03_01_000000_create_rules_table.php | 35 ++ phpunit.xml | 36 ++ src/Adapters/DatabaseAdapter.php | 167 +++++++++ src/Commands/PolicyAdd.php | 48 +++ src/Commands/RoleAssign.php | 48 +++ src/Contracts/DatabaseAdapter.php | 9 + src/Contracts/Factory.php | 7 + src/EnforcerManager.php | 68 ++++ src/Exceptions/UnauthorizedException.php | 18 + src/Facades/Enforcer.php | 21 ++ src/LauthzServiceProvider.php | 51 +++ src/Logger.php | 79 +++++ src/Middlewares/EnforcerMiddleware.php | 43 +++ src/Middlewares/RequestMiddleware.php | 61 ++++ src/Models/Rule.php | 113 ++++++ src/Observers/RuleObserver.php | 18 + tests/Commands/PolicyAddTest.php | 24 ++ tests/Commands/RoleAssignTest.php | 23 ++ tests/DatabaseAdapterTest.php | 70 ++++ tests/EnforcerMiddlewareTest.php | 34 ++ tests/LoggerTest.php | 39 +++ tests/Models/User.php | 26 ++ tests/RequestMiddlewareTest.php | 78 +++++ tests/RuleCacheTest.php | 72 ++++ tests/TestCase.php | 94 +++++ 32 files changed, 2004 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/lauthz-rbac-model.conf create mode 100644 config/lauthz.php create mode 100644 database/migrations/2019_03_01_000000_create_rules_table.php create mode 100644 phpunit.xml create mode 100644 src/Adapters/DatabaseAdapter.php create mode 100644 src/Commands/PolicyAdd.php create mode 100644 src/Commands/RoleAssign.php create mode 100644 src/Contracts/DatabaseAdapter.php create mode 100644 src/Contracts/Factory.php create mode 100644 src/EnforcerManager.php create mode 100644 src/Exceptions/UnauthorizedException.php create mode 100644 src/Facades/Enforcer.php create mode 100644 src/LauthzServiceProvider.php create mode 100644 src/Logger.php create mode 100644 src/Middlewares/EnforcerMiddleware.php create mode 100644 src/Middlewares/RequestMiddleware.php create mode 100644 src/Models/Rule.php create mode 100644 src/Observers/RuleObserver.php create mode 100644 tests/Commands/PolicyAddTest.php create mode 100644 tests/Commands/RoleAssignTest.php create mode 100644 tests/DatabaseAdapterTest.php create mode 100644 tests/EnforcerMiddlewareTest.php create mode 100644 tests/LoggerTest.php create mode 100644 tests/Models/User.php create mode 100644 tests/RequestMiddlewareTest.php create mode 100644 tests/RuleCacheTest.php create mode 100644 tests/TestCase.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b77ffd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +vendor +.idea +.vscode +.phpunit* +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a21c86f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,72 @@ +language: php + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +services: + - mysql + +matrix: + fast_finish: true + include: + # Laravel 5.1 + - php: 5.6 + env: LARAVEL=5.1.* PHPUNIT=^5.7 + - php: 7.0 + env: LARAVEL=5.1.* PHPUNIT=^5.7 + - php: 7.1 + env: LARAVEL=5.1.* PHPUNIT=^5.7 + + # Laravel 5.5 + - php: 7.0 + env: LARAVEL=5.5.* PHPUNIT=~6.0 + - php: 7.1 + env: LARAVEL=5.5.* PHPUNIT=~6.0 + - php: 7.2 + env: LARAVEL=5.5.* PHPUNIT=~6.0 + - php: 7.3 + env: LARAVEL=5.5.* PHPUNIT=~6.0 + + # Laravel 5.6 + - php: 7.1 + env: LARAVEL=5.6.* PHPUNIT=~7.0 + - php: 7.2 + env: LARAVEL=5.6.* PHPUNIT=~7.0 + - php: 7.3 + env: LARAVEL=5.6.* PHPUNIT=~7.0 + + # Laravel 5.7 + - php: 7.1 + env: LARAVEL=5.7.* PHPUNIT=~7.5 + - php: 7.2 + env: LARAVEL=5.7.* PHPUNIT=~7.5 + - php: 7.3 + env: LARAVEL=5.7.* PHPUNIT=~7.5 + + # Laravel 5.8 + - php: 7.1 + env: LARAVEL=5.8.* PHPUNIT=~7.5 + - php: 7.2 + env: LARAVEL=5.8.* PHPUNIT=~8.0 + - php: 7.3 + env: LARAVEL=5.8.* PHPUNIT=~8.0 + +before_install: + - travis_retry composer self-update + - travis_retry composer require laravel/framework:$LARAVEL --no-update --no-interaction + - travis_retry composer require laravel/laravel:$LARAVEL phpunit/phpunit:$PHPUNIT --no-update --no-interaction --dev + - mysql -e 'create database if not exists lauthz;' + +install: + - travis_retry composer install --no-suggest --no-interaction + +script: + - vendor/bin/phpunit --version + - mkdir -p build/logs + - vendor/bin/phpunit + +after_script: + - travis_retry vendor/bin/php-coveralls -v diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29f81d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a191138 --- /dev/null +++ b/README.md @@ -0,0 +1,327 @@ +

+ Laravel Authorization +

+ +

+ Laravel-authz is an authorization library for the laravel framework. +

+ +

+ + Build Status + + + Coverage Status + + + Latest Stable Version + + + Total Downloads + + + License + +

+ +It's based on [Casbin](https://github.com/php-casbin/php-casbin), an authorization library that supports access control models like ACL, RBAC, ABAC. + +All you need to learn to use `Casbin` first. + +* [Installation](#installation) +* [Usage](#usage) + * [Quick start](#quick-start) + * [Using Enforcer Api](#using-enforcer-api) + * [Using a middleware](#using-a-middleware) + * [basic Enforcer Middleware](#basic-enforcer-middleware) + * [HTTP Request Middleware ( RESTful is also supported )](#http-request-middleware--restful-is-also-supported-) + * [Using artisan commands](#using-artisan-commands) + * [Cache](#using-cache) +* [Thinks](#thinks) +* [License](#license) + +## Installation + +Require this package in the `composer.json` of your Laravel project. This will download the package. + +``` +composer require casbin/laravel-authz +``` + +The `Lauthz\LauthzServiceProvider` is `auto-discovered` and registered by default, but if you want to register it yourself: + +Add the ServiceProvider in `config/app.php` + +```php +'providers' => [ + /* + * Package Service Providers... + */ + Lauthz\LauthzServiceProvider::class, +] +``` + +The Enforcer facade is also `auto-discovered`, but if you want to add it manually: + +Add the Facade in `config/app.php` + +```php +'aliases' => [ + // ... + 'Enforcer' => Lauthz\Facades\Enforcer::class, +] +``` + +To publish the config, run the vendor publish command: + +``` +php artisan vendor:publish +``` + +This will create a new model config file named `config/lauthz-rbac-model.conf` and a new lauthz config file named `config/lauthz.php`. + + +To migrate the migrations, run the migrate command: + +``` +php artisan migrate +``` + +This will create a new table named `rules` + + +## Usage + +### Quick start + +Once installed you can do stuff like this: + +```php + +use Enforcer; + +// adds permissions to a user +Enforcer::addPermissionForUser('eve', 'articles', 'read'); +// adds a role for a user. +Enforcer::addRoleForUser('eve', 'writer'); +// adds permissions to a rule +Enforcer::addPolicy('writer', 'articles','edit'); + +``` + +You can check if a user has a permission like this: + +```php +// to check if a user has permission +if (Enforcer::enforce("eve", "articles", "edit")) { + // permit eve to edit articles +} else { + // deny the request, show an error +} + +``` + +### Using Enforcer Api + +It provides a very rich api to facilitate various operations on the Policy: + +Gets all roles: + +```php +Enforcer::getAllRoles(); // ['writer', 'reader'] +``` + +Gets all the authorization rules in the policy.: + +```php +Enforcer::getPolicy(); +``` + +Gets the roles that a user has. + +```php +Enforcer::getRolesForUser('eve'); // ['writer'] +``` + +Gets the users that has a role. + +```php +Enforcer::getUsersForRole('writer'); // ['eve'] +``` + +Determines whether a user has a role. + +```php +Enforcer::hasRoleForUser('eve', 'writer'); // true or false +``` + +Adds a role for a user. + +```php +Enforcer::addRoleForUser('eve', 'writer'); +``` + +Adds a permission for a user or role. + +```php +// to user +Enforcer::addPermissionForUser('eve', 'articles', 'read'); +// to role +Enforcer::addPermissionForUser('writer', 'articles','edit'); +``` + +Deletes a role for a user. + +```php +Enforcer::deleteRoleForUser('eve', 'writer'); +``` + +Deletes all roles for a user. + +```php +Enforcer::deleteRolesForUser('eve'); +``` + +Deletes a role. + +```php +Enforcer::deleteRole('writer'); +``` + +Deletes a permission. + +```php +Enforcer::deletePermission('articles', 'read'); // returns false if the permission does not exist (aka not affected). +``` + +Deletes a permission for a user or role. + +```php +Enforcer::deletePermissionForUser('eve', 'articles', 'read'); +``` + +Deletes permissions for a user or role. + +```php +// to user +Enforcer::deletePermissionsForUser('eve'); +// to role +Enforcer::deletePermissionsForUser('writer'); +``` + +Gets permissions for a user or role. + +```php +Enforcer::getPermissionsForUser('eve'); // return array +``` + +Determines whether a user has a permission. + +```php +Enforcer::hasPermissionForUser('eve', 'articles', 'read'); // true or false +``` + +### Using a middleware + +This package comes with `EnforcerMiddleware`, `RequestMiddleware` middlewares. You can add them inside your `app/Http/Kernel.php` file. + +```php +protected $routeMiddleware = [ + // ... + // a basic Enforcer Middleware + 'enforcer' => \Lauthz\Middlewares\EnforcerMiddleware::class, + // an HTTP Request Middleware + 'http_request' => \Lauthz\Middlewares\RequestMiddleware::class, +]; +``` + +#### basic Enforcer Middleware + +Then you can protect your routes using middleware rules: + +```php +Route::group(['middleware' => ['enforcer:articles,read']], function () { + // pass +}); +``` + +#### HTTP Request Middleware ( RESTful is also supported ) + +If you need to authorize a Request,you need to define the model configuration first in `config/lauthz-rbac-model.conf`: + +```ini +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) +``` + +Then, using middleware rules: + +```php +Route::group(['middleware' => ['http_request']], function () { + Route::resource('photo', 'PhotoController'); +}); +``` + +### Using artisan commands + +You can create a policy from a console with artisan commands. + +To user: + +```bash +php artisan policy:add eve,articles,read +``` + +To Role: + +```bash +php artisan policy:add writer,articles,edit +``` + +Adds a role for a user: + +```bash +php artisan role:assign eve writer +``` + +### Using cache + +Authorization rules are cached to speed up performance. The default is off. + +Sets your own cache configs in Laravel's `config/lauthz.php`. + +```php +'cache' => [ + // changes whether Lauthz will cache the rules. + 'enabled' => false, + + // cache store + 'store' => 'default', + + // cache Key + 'key' => 'rules', + + // ttl \DateTimeInterface|\DateInterval|int|null + 'ttl' => 24 * 60, +], +``` + +## Thinks + +[Casbin](https://github.com/php-casbin/php-casbin) in Laravel. You can find the full documentation of Casbin [on the website](https://casbin.org/). + +## License + +This project is licensed under the [Apache 2.0 license](LICENSE). \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..19b2217 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "casbin/laravel-authz", + "keywords": ["laravel","casbin", "permission", "access-control", "authorization", "rbac", "acl", "abac", "authz"], + "description": "An authorization library that supports access control models like ACL, RBAC, ABAC in Laravel. ", + "authors": [ + { + "name": "TechLee", + "email": "techlee@qq.com" + } + ], + "license": "Apache-2.0", + "require": { + "laravel/framework": "~5.1", + "casbin/casbin": ">=0.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5.7|~6.0|~7.0|~8.0", + "php-coveralls/php-coveralls": "^2.1", + "mockery/mockery": "^1.0", + "laravel/laravel": "~5.1" + }, + "autoload": { + "psr-4": { + "Lauthz\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Lauthz\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Lauthz\\LauthzServiceProvider" + ], + "aliases": { + "Enforcer": "Lauthz\\Facades\\Enforcer" + } + } + } +} diff --git a/config/lauthz-rbac-model.conf b/config/lauthz-rbac-model.conf new file mode 100644 index 0000000..7320e08 --- /dev/null +++ b/config/lauthz-rbac-model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/config/lauthz.php b/config/lauthz.php new file mode 100644 index 0000000..b97bd7b --- /dev/null +++ b/config/lauthz.php @@ -0,0 +1,60 @@ + 'basic', + + 'basic' => [ + /* + * Casbin model setting. + */ + 'model' => [ + // Available Settings: "file", "text" + 'config_type' => 'file', + + 'config_file_path' => config_path('lauthz-rbac-model.conf'), + + 'config_text' => '', + ], + + /* + * Casbin adapter . + */ + 'adapter' => Lauthz\Adapters\DatabaseAdapter::class, + + /* + * Database setting. + */ + 'database' => [ + // Database connection for following tables. + 'connection' => '', + + // Rule table name. + 'rules_table' => 'rules', + ], + + 'log' => [ + // changes whether Lauthz will log messages to the Logger. + 'enabled' => false, + + // Casbin Logger + 'logger' => Lauthz\Logger::class, + ], + + 'cache' => [ + // changes whether Lauthz will cache the rules. + 'enabled' => false, + + // cache store + 'store' => 'default', + + // cache Key + 'key' => 'rules', + + // ttl \DateTimeInterface|\DateInterval|int|null + 'ttl' => 24 * 60, + ], + ], +]; diff --git a/database/migrations/2019_03_01_000000_create_rules_table.php b/database/migrations/2019_03_01_000000_create_rules_table.php new file mode 100644 index 0000000..e455235 --- /dev/null +++ b/database/migrations/2019_03_01_000000_create_rules_table.php @@ -0,0 +1,35 @@ +create(config('lauthz.basic.database.rules_table'), function (Blueprint $table) { + $table->increments('id'); + $table->string('ptype')->nullable(); + $table->string('v0')->nullable(); + $table->string('v1')->nullable(); + $table->string('v2')->nullable(); + $table->string('v3')->nullable(); + $table->string('v4')->nullable(); + $table->string('v5')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + $connection = config('lauthz.basic.database.connection') ?: config('database.default'); + Schema::connection($connection)->dropIfExists(config('lauthz.basic.database.rules_table')); + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d2b9165 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + ./tests/ + + + + + ./src + + + + + + + + + + + + + + + + + + diff --git a/src/Adapters/DatabaseAdapter.php b/src/Adapters/DatabaseAdapter.php new file mode 100644 index 0000000..e75c205 --- /dev/null +++ b/src/Adapters/DatabaseAdapter.php @@ -0,0 +1,167 @@ +eloquent = $eloquent; + } + + /** + * savePolicyLine function. + * + * @param string $ptype + * @param array $rule + * + * @return void + */ + public function savePolicyLine($ptype, array $rule) + { + $col['ptype'] = $ptype; + foreach ($rule as $key => $value) { + $col['v'.strval($key)] = $value; + } + + $this->eloquent->create($col); + } + + /** + * loads all policy rules from the storage. + * + * @param Model $model + * + * @return mixed + */ + public function loadPolicy($model) + { + $rows = $this->eloquent->getAllFromCache(); + + foreach ($rows as $row) { + $line = implode(', ', array_slice(array_values($row), 1)); + $this->loadPolicyLine(trim($line), $model); + } + } + + /** + * saves all policy rules to the storage. + * + * @param Model $model + * + * @return bool + */ + public function savePolicy($model) + { + foreach ($model->model['p'] as $ptype => $ast) { + foreach ($ast->policy as $rule) { + $this->savePolicyLine($ptype, $rule); + } + } + + foreach ($model->model['g'] as $ptype => $ast) { + foreach ($ast->policy as $rule) { + $this->savePolicyLine($ptype, $rule); + } + } + + return true; + } + + /** + * Adds a policy rule to the storage. + * This is part of the Auto-Save feature. + * + * @param string $sec + * @param string $ptype + * @param array $rule + * + * @return mixed + */ + public function addPolicy($sec, $ptype, $rule) + { + return $this->savePolicyLine($ptype, $rule); + } + + /** + * This is part of the Auto-Save feature. + * + * @param string $sec + * @param string $ptype + * @param array $rule + * + * @return mixed + */ + public function removePolicy($sec, $ptype, $rule) + { + $count = 0; + + $instance = $this->eloquent->where('ptype', $ptype); + + foreach ($rule as $key => $value) { + $instance->where('v'.strval($key), $value); + } + + foreach ($instance->get() as $model) { + if ($model->delete()) { + ++$count; + } + } + + return $count; + } + + /** + * RemoveFilteredPolicy removes policy rules that match the filter from the storage. + * This is part of the Auto-Save feature. + * + * @param string $sec + * @param string $ptype + * @param int $fieldIndex + * @param mixed ...$fieldValues + * + * @return mixed + */ + public function removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues) + { + $count = 0; + + $instance = $this->eloquent->where('ptype', $ptype); + foreach (range(0, 5) as $value) { + if ($fieldIndex <= $value && $value < $fieldIndex + count($fieldValues)) { + $instance->where('v'.strval($value), $fieldValues[$value - $fieldIndex]); + } + } + + foreach ($instance->get() as $model) { + if ($model->delete()) { + ++$count; + } + } + + return $count; + } +} diff --git a/src/Commands/PolicyAdd.php b/src/Commands/PolicyAdd.php new file mode 100644 index 0000000..992fb8d --- /dev/null +++ b/src/Commands/PolicyAdd.php @@ -0,0 +1,48 @@ +argument('policy')); + array_walk($params, function (&$value) { + $value = trim($value); + }); + $ret = Enforcer::addPolicy(...$params); + if ($ret) { + $this->info('Policy `'.implode(', ', $params).'` created'); + } else { + $this->error('Policy `'.implode(', ', $params).'` creation failed'); + } + + return $ret ? 0 : 1; + } +} diff --git a/src/Commands/RoleAssign.php b/src/Commands/RoleAssign.php new file mode 100644 index 0000000..cf87244 --- /dev/null +++ b/src/Commands/RoleAssign.php @@ -0,0 +1,48 @@ +argument('user'); + $role = $this->argument('role'); + + $ret = Enforcer::addRoleForUser($user, $role); + if ($ret) { + $this->info('Added `'.$role.'` role to `'.$user.'` successfully'); + } else { + $this->error('Added `'.$role.'` role to `'.$user.'` failed'); + } + + return $ret ? 0 : 1; + } +} diff --git a/src/Contracts/DatabaseAdapter.php b/src/Contracts/DatabaseAdapter.php new file mode 100644 index 0000000..61068d8 --- /dev/null +++ b/src/Contracts/DatabaseAdapter.php @@ -0,0 +1,9 @@ +app['config']['lauthz.default']; + } + + /** + * Create an instance of the Basic Enforcer driver. + * + * @param array $config + * + * @return \Casbin\Enforcer + */ + public function createBasicDriver() + { + $config = $this->getConfig('basic'); + + if ($logger = Arr::get($config, 'log.logger')) { + Log::setLogger(new $logger($this->app['log'])); + } + + $model = new Model(); + $configType = Arr::get($config, 'model.config_type'); + if ('file' == $configType) { + $model->loadModel(Arr::get($config, 'model.config_file_path', '')); + } elseif ('text' == $configType) { + $model->loadModelFromText(Arr::get($config, 'model.config_text', '')); + } + $adapter = Arr::get($config, 'adapter'); + if (!is_null($adapter)) { + $adapter = $this->app->make($adapter); + } + + return new Enforcer($model, $adapter, Arr::get($config, 'log.enabled', false)); + } + + /** + * Get the lauthz driver configuration. + * + * @param string $name + * + * @return array + */ + protected function getConfig($name) + { + return $this->app['config']["lauthz.{$name}"]; + } +} diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php new file mode 100644 index 0000000..fe96531 --- /dev/null +++ b/src/Exceptions/UnauthorizedException.php @@ -0,0 +1,18 @@ +app->runningInConsole()) { + $this->publishes([__DIR__.'/../database/migrations' => database_path('migrations')], 'laravel-lauthz-migrations'); + $this->publishes([__DIR__.'/../config/lauthz-rbac-model.conf' => config_path('lauthz-rbac-model.conf')], 'laravel-lauthz-config'); + $this->publishes([__DIR__.'/../config/lauthz.php' => config_path('lauthz.php')], 'laravel-lauthz-config'); + + $this->commands([ + Commands\PolicyAdd::class, + Commands\RoleAssign::class, + ]); + } + + $this->mergeConfigFrom(__DIR__.'/../config/lauthz.php', 'lauthz'); + + $this->bootObserver(); + } + + /** + * Boot Observer. + * + * @return void + */ + protected function bootObserver() + { + Rule::observe(new RuleObserver()); + } + + /** + * Register bindings in the container. + */ + public function register() + { + $this->app->singleton('enforcer', function ($app) { + return new EnforcerManager($app); + }); + } +} diff --git a/src/Logger.php b/src/Logger.php new file mode 100644 index 0000000..e7d4cce --- /dev/null +++ b/src/Logger.php @@ -0,0 +1,79 @@ +logger = $logger; + } + + /** + * controls whether print the message. + * + * @param bool $enable + */ + public function enableLog($enable) + { + $this->enable = $enable; + } + + /** + * returns if logger is enabled. + * + * @return bool + */ + public function isEnabled() + { + return $this->enable; + } + + /** + * formats using the default formats for its operands and logs the message. + * + * @param mixed ...$v + * + * @return mixed + */ + public function write(...$v) + { + if (!$this->enable) { + return; + } + $content = ''; + foreach ($v as $value) { + if (\is_array($value) || \is_object($value)) { + $value = json_encode($value); + } + $content .= $value; + } + $this->logger->info($content); + } + + /** + * formats according to a format specifier and logs the message. + * + * @param $format + * @param mixed ...$v + * + * @return mixed + */ + public function writef($format, ...$v) + { + if (!$this->enable) { + return; + } + $this->logger->info(sprintf($format, ...$v)); + } +} diff --git a/src/Middlewares/EnforcerMiddleware.php b/src/Middlewares/EnforcerMiddleware.php new file mode 100644 index 0000000..dde9ed9 --- /dev/null +++ b/src/Middlewares/EnforcerMiddleware.php @@ -0,0 +1,43 @@ +getAuthIdentifier(); + if (method_exists($user, 'getAuthzIdentifier')) { + $identifier = $user->getAuthzIdentifier(); + } + + if (!Enforcer::enforce($identifier, ...$args)) { + throw new UnauthorizedException(); + } + + return $next($request); + } +} diff --git a/src/Middlewares/RequestMiddleware.php b/src/Middlewares/RequestMiddleware.php new file mode 100644 index 0000000..8a612e2 --- /dev/null +++ b/src/Middlewares/RequestMiddleware.php @@ -0,0 +1,61 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param mixed ...$args + * + * @return mixed + */ + public function handle($request, Closure $next) + { + if (Auth::guest()) { + throw new UnauthorizedException(); + } + + $user = Auth::user(); + $identifier = $user->getAuthIdentifier(); + if (method_exists($user, 'getAuthzIdentifier')) { + $identifier = $user->getAuthzIdentifier(); + } + + if (!Enforcer::enforce($identifier, $request->getPathInfo(), $request->method())) { + throw new UnauthorizedException(); + } + + return $next($request); + } +} diff --git a/src/Models/Rule.php b/src/Models/Rule.php new file mode 100644 index 0000000..26e4181 --- /dev/null +++ b/src/Models/Rule.php @@ -0,0 +1,113 @@ +config('database.connection') ?: config('database.default'); + + $this->setConnection($connection); + $this->setTable($this->config('database.rules_table')); + + parent::__construct($attributes); + + $this->initCache(); + } + + /** + * Gets rules from caches. + * + * @return void + */ + public function getAllFromCache() + { + $get = function () { + return $this->get()->toArray(); + }; + if (!$this->config('cache.enabled', false)) { + return $get(); + } + + return $this->store->remember($this->config('cache.key'), $this->config('cache.ttl'), $get); + } + + /** + * Refresh Cache. + * + * @return void + */ + public function refreshCache() + { + if (!$this->config('cache.enabled', false)) { + return; + } + + $this->forgetCache(); + $this->getAllFromCache(); + } + + /** + * Forget Cache. + * + * @return void + */ + public function forgetCache() + { + $this->store->forget($this->config('cache.key')); + } + + /** + * Init cache. + * + * @return void + */ + protected function initCache() + { + $driver = config('lauthz.default'); + $store = $this->config('cache.store', 'default'); + $store = 'default' == $store ? null : $store; + $this->store = Cache::store($store); + } + + /** + * Gets config value by key. + * + * @param string $key + * @param string $default + * + * @return mixed + */ + protected function config($key = null, $default = null) + { + $driver = config('lauthz.default'); + + return config('lauthz.'.$driver.'.'.$key, $default); + } +} diff --git a/src/Observers/RuleObserver.php b/src/Observers/RuleObserver.php new file mode 100644 index 0000000..c55f006 --- /dev/null +++ b/src/Observers/RuleObserver.php @@ -0,0 +1,18 @@ +refreshCache(); + } + + public function deleted(Rule $rule) + { + $rule->refreshCache(); + } +} diff --git a/tests/Commands/PolicyAddTest.php b/tests/Commands/PolicyAddTest.php new file mode 100644 index 0000000..6a6669f --- /dev/null +++ b/tests/Commands/PolicyAddTest.php @@ -0,0 +1,24 @@ +assertFalse(Enforcer::enforce('eve', 'articles', 'read')); + $exitCode = Artisan::call('policy:add', ['policy' => 'eve, articles, read']); + $this->assertTrue(0 === $exitCode); + $this->assertTrue(Enforcer::enforce('eve', 'articles', 'read')); + + $exitCode = Artisan::call('policy:add', ['policy' => 'eve, articles, read']); + $this->assertFalse(0 === $exitCode); + } +} diff --git a/tests/Commands/RoleAssignTest.php b/tests/Commands/RoleAssignTest.php new file mode 100644 index 0000000..b53eb21 --- /dev/null +++ b/tests/Commands/RoleAssignTest.php @@ -0,0 +1,23 @@ +assertFalse(Enforcer::hasRoleForUser('eve', 'writer')); + $exitCode = Artisan::call('role:assign', ['user' => 'eve', 'role' => 'writer']); + $this->assertTrue(0 === $exitCode); + $exitCode = Artisan::call('role:assign', ['user' => 'eve', 'role' => 'writer']); + $this->assertFalse(0 === $exitCode); + $this->assertTrue(Enforcer::hasRoleForUser('eve', 'writer')); + } +} diff --git a/tests/DatabaseAdapterTest.php b/tests/DatabaseAdapterTest.php new file mode 100644 index 0000000..9406d35 --- /dev/null +++ b/tests/DatabaseAdapterTest.php @@ -0,0 +1,70 @@ +assertTrue(Enforcer::enforce('alice', 'data1', 'read')); + + $this->assertFalse(Enforcer::enforce('bob', 'data1', 'read')); + $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write')); + + $this->assertTrue(Enforcer::enforce('alice', 'data2', 'read')); + $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write')); + } + + public function testAddPolicy() + { + $this->assertFalse(Enforcer::enforce('eve', 'data3', 'read')); + Enforcer::addPermissionForUser('eve', 'data3', 'read'); + $this->assertTrue(Enforcer::enforce('eve', 'data3', 'read')); + } + + public function testSavePolicy() + { + $this->assertFalse(Enforcer::enforce('alice', 'data4', 'read')); + + $model = Enforcer::getModel(); + $model->clearPolicy(); + $model->addPolicy('p', 'p', ['alice', 'data4', 'read']); + + $adapter = Enforcer::getAdapter(); + $adapter->savePolicy($model); + $this->assertTrue(Enforcer::enforce('alice', 'data4', 'read')); + } + + public function testRemovePolicy() + { + $this->assertFalse(Enforcer::enforce('alice', 'data5', 'read')); + + Enforcer::addPermissionForUser('alice', 'data5', 'read'); + $this->assertTrue(Enforcer::enforce('alice', 'data5', 'read')); + + Enforcer::deletePermissionForUser('alice', 'data5', 'read'); + $this->assertFalse(Enforcer::enforce('alice', 'data5', 'read')); + } + + public function testRemoveFilteredPolicy() + { + $this->assertTrue(Enforcer::enforce('alice', 'data1', 'read')); + Enforcer::removeFilteredPolicy(1, 'data1'); + $this->assertFalse(Enforcer::enforce('alice', 'data1', 'read')); + $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write')); + $this->assertTrue(Enforcer::enforce('alice', 'data2', 'read')); + $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write')); + Enforcer::removeFilteredPolicy(1, 'data2', 'read'); + $this->assertTrue(Enforcer::enforce('bob', 'data2', 'write')); + $this->assertFalse(Enforcer::enforce('alice', 'data2', 'read')); + $this->assertTrue(Enforcer::enforce('alice', 'data2', 'write')); + Enforcer::removeFilteredPolicy(2, 'write'); + $this->assertFalse(Enforcer::enforce('bob', 'data2', 'write')); + $this->assertFalse(Enforcer::enforce('alice', 'data2', 'write')); + } +} diff --git a/tests/EnforcerMiddlewareTest.php b/tests/EnforcerMiddlewareTest.php new file mode 100644 index 0000000..e3692d9 --- /dev/null +++ b/tests/EnforcerMiddlewareTest.php @@ -0,0 +1,34 @@ +assertEquals($this->middleware('data1', 'read'), 'Unauthorized Exception'); + } + + public function testAfterLogin() + { + $this->login('alice'); + $this->assertEquals($this->middleware('data1', 'read'), 200); + $this->assertEquals($this->middleware('data2', 'read'), 200); + $this->assertEquals($this->middleware('data2', 'write'), 200); + + $this->login('bob'); + $this->assertEquals($this->middleware('data1', 'read'), 'Unauthorized Exception'); + $this->assertEquals($this->middleware('data2', 'write'), 200); + } + + protected function middleware(...$args) + { + return parent::runMiddleware(EnforcerMiddleware::class, new Request(), ...$args); + } +} diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php new file mode 100644 index 0000000..531f952 --- /dev/null +++ b/tests/LoggerTest.php @@ -0,0 +1,39 @@ +enableLog(false); + $this->assertFalse($logger->isEnabled()); + + $logger->enableLog(true); + $this->assertTrue($logger->isEnabled()); + + $monolog->shouldReceive('info')->once()->with('foo', []); + $logger->write('foo'); + + $monolog->shouldReceive('info')->once()->with('foo1foo2', []); + $logger->write('foo1', 'foo2'); + + $monolog->shouldReceive('info')->once()->with(json_encode(['foo1', 'foo2']), []); + $logger->write(['foo1', 'foo2']); + + $monolog->shouldReceive('info')->once()->with(sprintf('There are %u million cars in %s.', 2, 'Shanghai'), []); + $logger->writef('There are %u million cars in %s.', 2, 'Shanghai'); + } +} diff --git a/tests/Models/User.php b/tests/Models/User.php new file mode 100644 index 0000000..12454c6 --- /dev/null +++ b/tests/Models/User.php @@ -0,0 +1,26 @@ +name; + } +} diff --git a/tests/RequestMiddlewareTest.php b/tests/RequestMiddlewareTest.php new file mode 100644 index 0000000..b4e6088 --- /dev/null +++ b/tests/RequestMiddlewareTest.php @@ -0,0 +1,78 @@ +assertEquals($this->middleware('/foo', 'GET'), 'Unauthorized Exception'); + } + + public function testAfterLogin() + { + $this->login('alice'); + $this->assertEquals($this->middleware(Request::create('/foo', 'GET')), 200); + $this->assertEquals($this->middleware(Request::create('/foo/1', 'GET')), 200); + $this->assertEquals($this->middleware(Request::create('/foo', 'POST')), 200); + $this->assertEquals($this->middleware(Request::create('/foo/1', 'PUT')), 200); + $this->assertEquals($this->middleware(Request::create('/foo/1', 'DELETE')), 200); + + $this->assertEquals($this->middleware(Request::create('/foo/2', 'GET')), 200); + $this->assertEquals($this->middleware(Request::create('/foo/2', 'PUT')), 200); + $this->assertEquals($this->middleware(Request::create('/foo/2', 'DELETE')), 200); + + $this->assertEquals($this->middleware(Request::create('/foo1/123', 'GET')), 200); + $this->assertEquals($this->middleware(Request::create('/foo1/123', 'POST')), 200); + $this->assertEquals($this->middleware(Request::create('/foo1/123', 'PUT')), 'Unauthorized Exception'); + + $this->assertEquals($this->middleware(Request::create('/proxy', 'GET')), 'Unauthorized Exception'); + } + + protected function middleware($request) + { + return parent::runMiddleware(RequestMiddleware::class, $request); + } + + protected function initConfig() + { + parent::initConfig(); + $this->app['config']->set('lauthz.basic.model.config_type', 'text'); + $text = <<<'EOT' +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) +EOT; + $this->app['config']->set('lauthz.basic.model.config_text', $text); + } + + protected function initTable() + { + Rule::truncate(); + + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo', 'v2' => 'GET']); + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo/:id', 'v2' => 'GET']); + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo', 'v2' => 'POST']); + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo/:id', 'v2' => 'PUT']); + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo/:id', 'v2' => 'DELETE']); + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => '/foo1/*', 'v2' => '(GET)|(POST)']); + } +} diff --git a/tests/RuleCacheTest.php b/tests/RuleCacheTest.php new file mode 100644 index 0000000..701bc57 --- /dev/null +++ b/tests/RuleCacheTest.php @@ -0,0 +1,72 @@ +enableCache(); + + DB::connection()->enableQueryLog(); + + app(Rule::class)->forgetCache(); + + app(Rule::class)->getAllFromCache(); + $this->assertCount(1, DB::getQueryLog()); + + app(Rule::class)->getAllFromCache(); + $this->assertCount(1, DB::getQueryLog()); + + DB::flushQueryLog(); + app(Rule::class)->getAllFromCache(); + $this->assertCount(0, DB::getQueryLog()); + + $rule = Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => 'data1', 'v2' => 'read']); + app(Rule::class)->getAllFromCache(); + $this->assertCount(2, DB::getQueryLog()); + + $rule->delete(); + app(Rule::class)->getAllFromCache(); + app(Rule::class)->getAllFromCache(); + $this->assertCount(4, DB::getQueryLog()); + + DB::flushQueryLog(); + } + + public function testDisableCache() + { + $this->app['config']->set('lauthz.basic.cache.enabled', false); + + DB::connection()->enableQueryLog(); + app(Rule::class)->getAllFromCache(); + $this->assertCount(1, DB::getQueryLog()); + + $rule = Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => 'data1', 'v2' => 'read']); + app(Rule::class)->getAllFromCache(); + $this->assertCount(3, DB::getQueryLog()); + + $rule->delete(); + app(Rule::class)->getAllFromCache(); + app(Rule::class)->getAllFromCache(); + $this->assertCount(6, DB::getQueryLog()); + + DB::flushQueryLog(); + } + + protected function enableCache() + { + $this->app['config']->set('lauthz.basic.cache.enabled', true); + } + + protected function initTable() + { + Rule::truncate(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..3714659 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,94 @@ +app = require __DIR__.'/../vendor/laravel/laravel/bootstrap/app.php'; + + $this->app->booting(function () { + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('Enforcer', \Lauthz\Facades\Enforcer::class); + }); + + $this->app->make(Kernel::class)->bootstrap(); + + $this->app->register(\Lauthz\LauthzServiceProvider::class); + $this->initConfig(); + + $this->artisan('vendor:publish', ['--provider' => 'Lauthz\LauthzServiceProvider']); + $this->artisan('migrate', ['--force' => true]); + + if (method_exists($this, 'afterApplicationCreated')) { + $this->afterApplicationCreated(function () { + $this->initTable(); + }); + } else { + $this->initTable(); + } + + return $this->app; + } + + protected function initConfig() + { + $this->app['config']->set('database.default', 'mysql'); + $this->app['config']->set('database.connections.mysql.charset', 'utf8'); + $this->app['config']->set('database.connections.mysql.collation', 'utf8_unicode_ci'); + // $app['config']->set('lauthz.log.enabled', true); + } + + protected function initTable() + { + Rule::truncate(); + + Rule::create(['ptype' => 'p', 'v0' => 'alice', 'v1' => 'data1', 'v2' => 'read']); + Rule::create(['ptype' => 'p', 'v0' => 'bob', 'v1' => 'data2', 'v2' => 'write']); + + Rule::create(['ptype' => 'p', 'v0' => 'data2_admin', 'v1' => 'data2', 'v2' => 'read']); + Rule::create(['ptype' => 'p', 'v0' => 'data2_admin', 'v1' => 'data2', 'v2' => 'write']); + Rule::create(['ptype' => 'g', 'v0' => 'alice', 'v1' => 'data2_admin']); + } + + protected function runMiddleware($middleware, $request, ...$args) + { + $middleware = $this->app->make($middleware); + try { + return $middleware->handle($request, function () { + return (new Response())->setContent(''); + }, ...$args)->status(); + } catch (UnauthorizedException $e) { + return 'Unauthorized Exception'; + } + + return 'Exception'; + } + + protected function login($name) + { + Auth::login($this->user($name)); + } + + protected function user($name) + { + $user = new User(); + $user->name = $name; + + return $user; + } +}