From 87f6ac976e83518bbe10a2ce57a853f9ae7008ef Mon Sep 17 00:00:00 2001 From: Dmitry Pupinin Date: Tue, 14 Jan 2020 12:10:32 +0700 Subject: [PATCH 1/4] Support several roles for user (many-to-many) --- config/config.php | 8 ++++++- src/ArrayAuthorization.php | 46 +++++++++++++++++++++++++----------- src/HRBACServiceProvider.php | 5 +++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/config/config.php b/config/config.php index 3807176..e62b357 100644 --- a/config/config.php +++ b/config/config.php @@ -1,5 +1,5 @@ App\Classes\Authorization\AuthorizationClass::class, + /** + * Name of user's class attribute that gives array of roles + * if you uses many-to-many relationship + */ + + 'userRolesAttribute' => 'own_roles', ]; diff --git a/src/ArrayAuthorization.php b/src/ArrayAuthorization.php index e74b3ca..faca3c8 100644 --- a/src/ArrayAuthorization.php +++ b/src/ArrayAuthorization.php @@ -3,6 +3,7 @@ namespace Dlnsk\HierarchicalRBAC; use Illuminate\Support\Str; +use Illuminate\Support\Arr; class ArrayAuthorization @@ -36,26 +37,15 @@ private function testUsingUserMethod($user, $initial_ability, $current_ability, * * @return boolean */ - public function checkPermission($user, $ability, $arguments) + public function checkAbility($user, $user_abilities, $ability, $arguments) { - if ($user->role === 'admin') { - return true; - } - - // У пользователя роль, которой нет в списке - $roles = $this->getRoles(); - if (!isset($roles[$user->role])) { - return null; - } - // Ищем разрешение для данной роли среди наследников текущего разрешения - $role = $roles[$user->role]; $permissions = $this->getPermissions(); $current = $ability; // Если для разрешения указана замена - элемент 'equal', то проверяется замена // (только при наличии оригинального разрешения в роли). // Callback оригинального не вызывается. - if (in_array($current, $role) and isset($permissions[$current]['equal'])) { + if (in_array($current, $user_abilities) and isset($permissions[$current]['equal'])) { $current = $permissions[$current]['equal']; } @@ -66,7 +56,7 @@ public function checkPermission($user, $ability, $arguments) throw new \Exception("Seems like permission '{$ability}' is in infinite loop"); } - if (in_array($current, $role)) { + if (in_array($current, $user_abilities)) { $suitable = $suitable || $this->testUsingUserMethod($user, $ability, $current, $arguments); } if (isset($permissions[$current]['next']) and !$suitable) { @@ -79,6 +69,34 @@ public function checkPermission($user, $ability, $arguments) } + public function checkPermission($user, $ability, $arguments) + { + $attribute = config('h-rbac.userRolesAttribute'); + $user_roles = Arr::wrap($user->role ?? null) ?: Arr::wrap($user->$attribute ?? null); + + if (in_array('admin', $user_roles)) { + return true; + } + + // У пользователя роли, которых нет в списке ролей приложения + $application_roles = array_keys($this->getRoles()); + $both_roles = array_intersect($application_roles, $user_roles); + if (!count($both_roles)) { + return null; + } + + $abilities = $this->getRoles(); + $user_abilities = []; + foreach ($both_roles as $role_name) { + $user_abilities = array_merge($user_abilities, $abilities[$role_name]); + } + $result = $this->checkAbility($user, $user_abilities, $ability, $arguments); + + return is_bool($result) ? $result : null; + } + + + /** * Return model of given class or exception if can't * diff --git a/src/HRBACServiceProvider.php b/src/HRBACServiceProvider.php index 762bdc5..2da7acb 100644 --- a/src/HRBACServiceProvider.php +++ b/src/HRBACServiceProvider.php @@ -55,7 +55,10 @@ public function register() $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { $bladeCompiler->directive('role', function ($roles) { - return "check() && in_array(auth()->user()->role, explode('|', $roles))): ?>"; + return 'user()->role ?? null) ?: Arr::wrap(auth()->user()->$__attribute ?? null); + if(auth()->check() && array_intersect($__user_roles, explode("|", '.$roles.'))): ?>'; }); $bladeCompiler->directive('endrole', function () { return ''; From 319b0231ec660d310dad45288ac34fa201ca6e99 Mon Sep 17 00:00:00 2001 From: Dmitry Pupinin Date: Wed, 15 Jan 2020 15:00:49 +0700 Subject: [PATCH 2/4] Some files moved and renamed --- .../AuthorizationClass.php | 3 +- composer.json | 3 +- database/migrations/.gitkeep | 0 ...users.php => add_role_field_to_users.stub} | 0 src/HRBACServiceProvider.php | 29 ++++++++++--------- 5 files changed, 20 insertions(+), 15 deletions(-) rename config/exampleClass.php => classes/AuthorizationClass.php (88%) delete mode 100644 database/migrations/.gitkeep rename database/migrations/{2016_03_14_000001_add_role_field_to_users.php => add_role_field_to_users.stub} (100%) diff --git a/config/exampleClass.php b/classes/AuthorizationClass.php similarity index 88% rename from config/exampleClass.php rename to classes/AuthorizationClass.php index 5a1884d..bb0b7fa 100644 --- a/config/exampleClass.php +++ b/classes/AuthorizationClass.php @@ -47,7 +47,8 @@ public function getRoles() { */ public function editOwnPost($user, $post) { - $post = $this->getModel(\App\Post::class, $post); // helper method for geting model + // This is a helper method for getting the model if $post is id + // $post = $this->getModel(\App\Post::class, $post); return $user->id === $post->user_id; } diff --git a/composer.json b/composer.json index 3647cec..99af9f1 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ }, "autoload": { "psr-4": { - "Dlnsk\\HierarchicalRBAC\\": "src" + "Dlnsk\\HierarchicalRBAC\\": "src", + "App\\Classes\\Authorization\\": "classes" } }, "extra": { diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/database/migrations/2016_03_14_000001_add_role_field_to_users.php b/database/migrations/add_role_field_to_users.stub similarity index 100% rename from database/migrations/2016_03_14_000001_add_role_field_to_users.php rename to database/migrations/add_role_field_to_users.stub diff --git a/src/HRBACServiceProvider.php b/src/HRBACServiceProvider.php index 2da7acb..ce40bad 100644 --- a/src/HRBACServiceProvider.php +++ b/src/HRBACServiceProvider.php @@ -1,4 +1,4 @@ -publishes([ - __DIR__.'/../database/migrations/' => base_path('database/migrations') - ], 'migrations'); - - // Publish your config - $this->publishes([ - __DIR__.'/../config/config.php' => config_path($this->packageName.'.php'), - __DIR__.'/../config/exampleClass.php' => app_path('Classes/Authorization/AuthorizationClass.php'), - ], 'config'); + if ($this->app->runningInConsole()) { - // + // Register your migration's publisher + $this->publishes([ + __DIR__ . '/../database/migrations/add_role_field_to_users.stub' + => database_path('migrations/' . date('Y_m_d_His', time()) . '_add_role_field_to_users.php'), + ], 'migrations'); + + // Publish your config + $this->publishes([ + __DIR__.'/../config/config.php' => config_path($this->packageName.'.php'), + __DIR__.'/../classes/AuthorizationClass.php' => app_path('Classes/Authorization/AuthorizationClass.php'), + ], 'config'); + + } } /** From 4caca07241a266674020938ae2ecccf39c3b93d8 Mon Sep 17 00:00:00 2001 From: Dmitry Pupinin Date: Wed, 15 Jan 2020 15:04:03 +0700 Subject: [PATCH 3/4] Covered by tests --- composer.json | 10 ++- phpunit.xml | 1 - tests/.gitkeep | 0 tests/TestCase.php | 31 ++++++++++ tests/UnitTest.php | 147 ++++++++++++++++++++++++++++++++++++++++++++ tests/ViewsTest.php | 98 +++++++++++++++++++++++++++++ 6 files changed, 284 insertions(+), 3 deletions(-) delete mode 100644 tests/.gitkeep create mode 100644 tests/TestCase.php create mode 100644 tests/UnitTest.php create mode 100644 tests/ViewsTest.php diff --git a/composer.json b/composer.json index 99af9f1..0a0d494 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,11 @@ } ], "require": { - "illuminate/support": "~5.7|>=6.0" + "laravel/framework": "~5.7|>=6.0" }, "require-dev": { - "phpunit/phpunit": "~4.5" + "phpunit/phpunit": "^8.0", + "orchestra/testbench": "^4.5" }, "autoload": { "psr-4": { @@ -22,6 +23,11 @@ "App\\Classes\\Authorization\\": "classes" } }, + "autoload-dev": { + "psr-4": { + "Dlnsk\\HierarchicalRBAC\\Tests\\": "tests" + } + }, "extra": { "laravel": { "providers": [ diff --git a/phpunit.xml b/phpunit.xml index e3213a2..2d346be 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="./vendor/autoload.php" > diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..9a63c20 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,31 @@ +user = (object)[]; + $this->post = (object)[]; + } + + /** @test */ + public function у_пользователя_нет_ничего_связанного_с_ролями() + { + $this->assertFalse(Gate::forUser($this->user)->allows('deletePost')); + } + + /** @test */ + public function у_пользователя_ошибочная_роль() + { + $this->user->role = 'dummy'; + + $this->assertFalse(Gate::forUser($this->user)->allows('deletePost')); + } + + /** @test */ + public function у_пользователя_правильная_роль_и_есть_право() + { + $this->user->role = 'manager'; + + $this->assertTrue(Gate::forUser($this->user)->allows('deletePost')); + } + + /** @test */ + public function у_пользователя_правильная_роль_но_права_нет() + { + $this->user->role = 'manager'; + + $this->assertFalse(Gate::forUser($this->user)->allows('dummy_permission')); + } + + /** @test */ + public function менеджер_редактирует_свой_пост() + { + $this->user->role = 'manager'; + $this->user->id = 1; + $this->post->user_id = 1; + + $this->assertTrue(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + /** @test */ + public function менеджер_редактирует_чужой_пост() + { + $this->user->role = 'manager'; + $this->user->id = 1; + $this->post->user_id = 2; + + $this->assertTrue(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + /** @test */ + public function пользователь_редактирует_свой_пост() + { + $this->user->role = 'user'; + $this->user->id = 1; + $this->post->user_id = 1; + + $this->assertTrue(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + /** @test */ + public function пользователь_редактирует_чужой_пост() + { + $this->user->role = 'user'; + $this->user->id = 1; + $this->post->user_id = 2; + + $this->assertFalse(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + +///////////////////////////////////////// + + + /** @test */ + public function у_пользователя_все_роли_ошибочные() + { + $this->user->own_roles = ['dummy1', 'dummy2']; + + $this->assertFalse(Gate::forUser($this->user)->allows('deletePost')); + } + + /** @test */ + public function у_пользователя_есть_правильная_роль_и_право() + { + $this->user->own_roles = ['dummy1', 'manager']; + + $this->assertTrue(Gate::forUser($this->user)->allows('deletePost')); + } + + /** @test */ + public function пользователь_с_пересекающимися_правами_редактирует_свой_пост() + { + $this->user->own_roles = ['user', 'manager']; + $this->user->id = 1; + $this->post->user_id = 1; + + $this->assertTrue(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + /** @test */ + public function пользователь_с_пересекающимися_правами_редактирует_чужой_пост() + { + $this->user->own_roles = ['user', 'manager']; + $this->user->id = 1; + $this->post->user_id = 2; + + $this->assertTrue(Gate::forUser($this->user)->allows('editPost', $this->post)); + } + + +/////////////////////////////////// + + + /** @test */ + public function изменение_имени_атрибута_возвращающего_несколько_ролей() + { + app()['config']->set('h-rbac.userRolesAttribute', 'changedName'); + $this->user->own_roles = ['manager']; + + $this->assertFalse(Gate::forUser($this->user)->allows('deletePost')); + + + $this->user->changedName = ['manager']; + $this->assertTrue(Gate::forUser($this->user)->allows('deletePost')); + } + +} diff --git a/tests/ViewsTest.php b/tests/ViewsTest.php new file mode 100644 index 0000000..5088f35 --- /dev/null +++ b/tests/ViewsTest.php @@ -0,0 +1,98 @@ +'.$generated); + } + + // If we caught an exception, we'll silently flush the output + // buffer so that no partially rendered views get thrown out + // to the client and confuse the user with junk. + catch (\Exception $e) + { + ob_get_clean(); throw $e; + } + + $content = ob_get_clean(); + + return $content; + } + + + /** @test */ + public function view_role() + { + $user = (object)[]; + $user->role = 'manager'; + \Auth::shouldReceive('check')->andReturn(true); + \Auth::shouldReceive('user')->andReturn($user); + + + $this->assertStringContainsString('general', $this->bladeCompile($this->template)); + $this->assertStringNotContainsString('has_dummy', $this->bladeCompile($this->template)); + $this->assertStringNotContainsString('has_user_role', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_manager_role', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_or_role_1', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_or_role_2', $this->bladeCompile($this->template)); + } + + /** @test */ + public function view_many_role() + { + $user = (object)[]; + $user->own_roles = ['manager']; + \Auth::shouldReceive('check')->andReturn(true); + \Auth::shouldReceive('user')->andReturn($user); + + + $this->assertStringContainsString('general', $this->bladeCompile($this->template)); + $this->assertStringNotContainsString('has_dummy', $this->bladeCompile($this->template)); + $this->assertStringNotContainsString('has_user_role', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_manager_role', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_or_role_1', $this->bladeCompile($this->template)); + $this->assertStringContainsString('has_or_role_2', $this->bladeCompile($this->template)); + } + +} From dc2d66a1df0cd03919ae14bfde737c912fea1d1f Mon Sep 17 00:00:00 2001 From: Dmitry Pupinin Date: Wed, 15 Jan 2020 21:32:22 +0700 Subject: [PATCH 4/4] Readme updated --- CHANGELOG.md | 10 +++++++++- README.md | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfec45..5b14518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,15 @@ All Notable changes to `h-rbac` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. -## [0.1] - 2016-03-14 +## [0.4.0] - 2020-01-15 +### Added +- Support for multiply roles (many-to-many relationship). Backward compatible. +- Covered by tests. +## [0.3.4] - 2019-10-24 +### Changed +- Since Laravel 5.7 helpers was changed + +## [0.1] - 2016-03-14 ### Added - Initial version diff --git a/README.md b/README.md index cdfeb5d..83f6759 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Add the service provider to `config/app.php`. We use auto-discovering feature si Publish some cool stuff: - config file (config/h-rbac.php) - - migration (add field `role` to `users` table) + - migration (adding field `role` to `users` table), only if you suppose to use one role per user - role/permission/callbacks configuration class (app/Classes/Authorization/AuthorizationClass.php) with @@ -42,6 +42,16 @@ with Add roles, permissions which you need and callbacks where it needs and have fun! +### Many To Many with roles + +If you want to use "Many To Many" relationship between users and roles add an accessor to `User` model. Name of attribute you can change in `config/h-rbac.php` if you need: + +``` php +public function getOwnRolesAttribute() { + return $this->roles()->pluck('name')->toArray(); +} +``` + ## Overview This module is wrapper for [authorization logic](https://laravel.com/docs/5.2/authorization#checking-abilities) and control access to resources of Laravel 5.1 and later. Except you shouldn't define abilities, they will define automatically.