├── .gitignore ├── composer.json ├── readme.md ├── readme_rus.md ├── src └── Rbac │ ├── Contracts │ ├── Assignable.php │ ├── RbacContext.php │ ├── RbacContextAccessor.php │ └── RbacManager.php │ ├── Facades │ └── Rbac.php │ ├── Item.php │ ├── ItemsRepository.php │ ├── Manager.php │ ├── Middleware │ └── RbacMiddleware.php │ ├── Permission.php │ ├── RbacServiceProvider.php │ ├── Role.php │ ├── Rule.php │ ├── Traits │ └── AllowedTrait.php │ └── install │ ├── Rbac │ ├── actions.php │ └── items.php │ └── config │ └── rbac.php └── tests ├── BladeTest.php ├── RbacTest.php ├── User.php ├── actions.php ├── bootstrap.php ├── items.php ├── test.blade.php └── test.compiled.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /tmp 3 | composer.lock -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-crowd/laravel-rbac", 3 | "description": "Laravel RBAC implementation.", 4 | "keywords": ["laravel", "RBAC", "ACL", "role-based access control"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Yuri Agapov", 9 | "email": "y.agapov@smart-crowd.ru" 10 | }, 11 | { 12 | "name": "Paul Klementev", 13 | "email": "klermonte@yandex.ru" 14 | } 15 | ], 16 | "require": { 17 | "illuminate/support": "5.1.*", 18 | "illuminate/container": "5.1.*" 19 | }, 20 | "require-dev": { 21 | "orchestra/testbench": "~3.0", 22 | "phpunit/phpunit": "~4.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "SmartCrowd\\Rbac\\": "src/Rbac/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel RBAC 2 | Laravel 5 RBAC implementation 3 | 4 | Package was inspired by RBAC module from Yii Framework 5 | 6 | ## Installation 7 | 1. Run 8 | ```bash 9 | composer require "smart-crowd/laravel-rbac":"dev-master" 10 | ``` 11 | 12 | 2. Add service provider and facade into `/config/app.php` file. 13 | ```php 14 | 'providers' => [ 15 | ... 16 | 17 | SmartCrowd\Rbac\RbacServiceProvider::class, 18 | ], 19 | ... 20 | 21 | 'aliases' => [ 22 | ... 23 | 24 | 'Rbac' => 'SmartCrowd\Rbac\Facades\Rbac' 25 | ] 26 | ``` 27 | 28 | 3. Publish package configs 29 | ```bash 30 | php artisan vendor:publish 31 | ``` 32 | 33 | 4. Implement `Assignable` contract in your user model. And use `AllowedTrait`. 34 | ```php 35 | use SmartCrowd\Rbac\Traits\AllowedTrait; 36 | use SmartCrowd\Rbac\Contracts\Assignable; 37 | 38 | class User extends Model implements Assignable 39 | { 40 | use AllowedTrait; 41 | 42 | /** 43 | * Should return array of permissions and roles names, 44 | * assigned to user. 45 | * 46 | * @return array Array of user assignments. 47 | */ 48 | public function getAssignments() 49 | { 50 | // your implementation here 51 | } 52 | ... 53 | } 54 | ``` 55 | 56 | ## Usage 57 | 1. Describe you permissions in `/Rbac/items.php` 58 | 59 | 2. Use inline in code 60 | ```php 61 | if (Auth::user()->allowed('article.delete', ['article' => $article])) { 62 | // user has access to 'somePermission.name' permission 63 | } 64 | ``` 65 | 66 | 3. Or in middleware 67 | ```php 68 | Route::delete('/articles/{article}', [ 69 | 'middleware' => 'rbac:article.delete', 70 | 'uses' => 'ArticlesController@delete' 71 | ]); 72 | ``` 73 | Of course, don't forget to register middleware in `/Http/Kernel.php` file 74 | ```php 75 | protected $routeMiddleware = [ 76 | ... 77 | 'rbac' => 'SmartCrowd\Rbac\Middleware\RbacMiddleware', 78 | ]; 79 | ``` 80 | To use route parameters in business rules as models instead just ids, you should bind it in `RouteServicePrivider.php`: 81 | ```php 82 | public function boot(Router $router) 83 | { 84 | //... 85 | $router->model('article', '\App\Article'); 86 | 87 | parent::boot($router); 88 | } 89 | ``` 90 | 91 | There are 3 ways to bind permission name to action name: 92 | - middleware paramenter 93 | - bind they directelly in `/Rbac/actions.php` file 94 | - name permission like action, for example `article.edit` for `ArticleController@edit` action 95 | 96 | 4. Or in your views 97 | ```php 98 | @allowed('article.edit', ['article' => $article]) 99 | edit 100 | @else 101 | You can not edit this article 102 | @endallowed 103 | ``` 104 | If `rbac.shortDirectives` option are enabled, you can use shorter forms of directives, like this: 105 | ```php 106 | @allowedArticleEdit(['article' => $article]) 107 | {{ $some }} 108 | @endallowed 109 | 110 | @allowedIndex 111 | {{ $some }} 112 | @endallowed 113 | ``` 114 | 115 | ### Context Roles 116 | In some cases, you may want to have dynamically assigned roles. For example, the role `groupModerator` is dynamic, because depending on the current group, the current user may have this role, or may not have. In our terminology, this role are "Context Role", and current group is "Role Context". The context decides which additional context roles will be assigned to the current user. In our case, `Group` model should implement `RbacContext` interface, and method `getAssignments($user)`. 117 | 118 | When checking is enough to send context model among other parameters: 119 | ```php 120 | @allowed('group.post.delete', ['post' => $post, 'group' => $group]) // or $post->group 121 | post delete button 122 | @endallowed 123 | ``` 124 | 125 | But for automatic route check in middleware we usually send only post without group: 126 | ```php 127 | Route::delete('/post/{post}', [ 128 | 'middleware' => 'rbac:group.post.delete', 129 | 'uses' => 'PostController@delete' 130 | ]); 131 | ``` 132 | For this case you can implement `RbacContextAccesor` intarface by `Post` model. `getContext()` method should return `Group` model. Then you just have to send only the post, and context roles will be applied in middleware to: 133 | ```php 134 | @allowed('group.post.delete', ['post' => $post]) 135 | post delete button 136 | @endallowed 137 | ``` 138 | You can not do that, if you send context with subject: 139 | ```php 140 | Route::delete('/group/{group}/post/{post}', [ 141 | 'middleware' => 'rbac:group.post.delete', 142 | 'uses' => 'PostController@delete' 143 | ]); 144 | ``` 145 | -------------------------------------------------------------------------------- /readme_rus.md: -------------------------------------------------------------------------------- 1 | # Laravel RBAC 2 | Реализация RBAC для Laravel 5 3 | 4 | На создание этого пакета вдохновила реализация RBAC во фреймворке Yii. 5 | 6 | ## Установка 7 | 1. Запустите 8 | ```bash 9 | composer require "smart-crowd/laravel-rbac":"dev-master" 10 | ``` 11 | 12 | 2. Добавьте провайдер и алиас в файл `/config/app.php`. 13 | ```php 14 | 'providers' => [ 15 | ... 16 | 17 | SmartCrowd\Rbac\RbacServiceProvider::class, 18 | ], 19 | ... 20 | 21 | 'aliases' => [ 22 | ... 23 | 24 | 'Rbac' => 'SmartCrowd\Rbac\Facades\Rbac' 25 | ] 26 | ``` 27 | 28 | 3. Опубликуйте конфиги пакета 29 | ```bash 30 | php artisan vendor:publish 31 | ``` 32 | 33 | 4. Реализуйте интерфейс `Assignable` вашей моделью пользователя. Также используйте трейт `AllowedTrait`. 34 | ```php 35 | use SmartCrowd\Rbac\Traits\AllowedTrait; 36 | use SmartCrowd\Rbac\Contracts\Assignable; 37 | 38 | class User extends Model implements Assignable 39 | { 40 | use AllowedTrait; 41 | 42 | /** 43 | * Должен вернуть массив прав и ролей, назначенных пользователю 44 | * 45 | * @return array Массив назначенных прав и ролей 46 | */ 47 | public function getAssignments() 48 | { 49 | // ваша реализация 50 | } 51 | ... 52 | } 53 | ``` 54 | 55 | ## Использование 56 | 1. Опишите ваши права в файле `/Rbac/items.php` 57 | 58 | 2. Использование в коде (например в конроллере) 59 | ```php 60 | if (Auth::user()->allowed('article.delete', ['article' => $article])) { 61 | // пользователь иммет доступ к действию 'somePermission.name' 62 | } 63 | ``` 64 | 65 | 3. Также вы можете использовать мидлвер 66 | ```php 67 | Route::delete('/articles/{article}', [ 68 | 'middleware' => 'rbac:article.delete', 69 | 'uses' => 'ArticlesController@delete' 70 | ]); 71 | ``` 72 | Конечно не забудьте зарегистрировать этот мидлвер в файле `/Http/Kernel.php` 73 | ```php 74 | protected $routeMiddleware = [ 75 | ... 76 | 'rbac' => 'SmartCrowd\Rbac\Middleware\RbacMiddleware', 77 | ]; 78 | ``` 79 | Если вы хотите использовать в бизнес правилах модели, вместо их идентификаторов, вам необходимо 80 | привязать модель к роуту в провайдере `RouteServicePrivider.php`: 81 | ```php 82 | public function boot(Router $router) 83 | { 84 | //... 85 | $router->model('article', '\App\Article'); 86 | 87 | parent::boot($router); 88 | } 89 | ``` 90 | 91 | Есть 3 способа назничть проверяемые права роуту: 92 | - параметр мидлвера, как в примере выше 93 | - связать роут с проверяемымы правами напрямую в файле `/Rbac/actions.php` 94 | - назвать право или роль как экшн, напрмер `article.edit` автоматически будет проверяться для действия `ArticleController@edit`. 95 | Конечно если для этого роута был назначен наш мидлвер 96 | 97 | 4. Использование в blade шаблонах: 98 | ```php 99 | @allowed('article.edit', ['article' => $article]) 100 | edit 101 | @else 102 | Вы не можете редактировать эту статью 103 | @endallowed 104 | ``` 105 | Если включена опция `rbac.shortDirectives`, вы можете более короткие формы директивы проверки прав, напрмер так: 106 | ```php 107 | @allowedArticleEdit(['article' => $article]) 108 | {{ $some }} 109 | @endallowed 110 | 111 | @allowedIndex 112 | {{ $some }} 113 | @endallowed 114 | ``` 115 | 116 | ### Контекстные роли 117 | В некоторых случаях, у вас может возникнуть необходимость в динамически назначаемых ролях. 118 | Напрмер, роль `groupModerator` динамически назначаемая, потому что, в зависимости от текущей группы, 119 | авторизованный пользователь может иметь эту роль, а может и не иметь. 120 | В нашем случае роль `groupModerator` является контекстной, а текущая группа - контекстом этой роли. 121 | Контекст определяет какие дополнительные (контекстные) роли и права будут назначены 122 | текущему авторизованному пользователю. 123 | Возвращаясь к нашему примеру: модель `Group` должна реализовать интерфейс `RbacContext`, который требует наличия 124 | метода `getAssignments($user)`. 125 | 126 | При проверке достаточно передать модель-контекст наряду с остальными параметрами 127 | ```php 128 | @allowed('group.post.delete', ['post' => $post, 'group' => $group]) // или $post->group 129 | кнопка удаления поста 130 | @endallowed 131 | ``` 132 | 133 | В роутах этот пример может выглядеть так (конечно и `group` и `post` должны быть привязаны в `RouteServicePrivider.php`): 134 | ```php 135 | Route::delete('/group/{group}/post/{post}', [ 136 | 'middleware' => 'rbac:group.post.delete', 137 | 'uses' => 'PostController@delete' 138 | ]); 139 | ``` 140 | 141 | Но мы не всегда можем передать контекст в роуте вместе с основной моделью: 142 | ```php 143 | Route::delete('/post/{post}', [ 144 | 'middleware' => 'rbac:group.post.delete', 145 | 'uses' => 'PostController@delete' 146 | ]); 147 | ``` 148 | В этом случае вы можете реализовать интерфейс `RbacContextAccesor` вашей моделью. В нашем примере это `Post`. 149 | Метод `RbacContextAccesor::getContext()` должен вернуть экземпляр модели-контекста. Для нашего примера это группа, в 150 | которой был опубликован этот пост. Тогда и при проверке в шаблоне нет необходимости передавать модель-контекст: 151 | ```php 152 | @allowed('group.post.delete', ['post' => $post]) 153 | кнопка удаления поста 154 | @endallowed 155 | ``` 156 | 157 | -------------------------------------------------------------------------------- /src/Rbac/Contracts/Assignable.php: -------------------------------------------------------------------------------- 1 | $value) { 33 | if (property_exists($this, $key)) { 34 | $this->{$key} = $value; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Rbac/ItemsRepository.php: -------------------------------------------------------------------------------- 1 | item 11 | 12 | /** 13 | * @var array $children Items tree 14 | */ 15 | private $children = []; // itemName, childName => child 16 | 17 | /** 18 | * @var array $action Permissions to Http actions assigns 19 | */ 20 | protected $actions = []; // actionName => itemName[] 21 | 22 | /** 23 | * @var array $controllers Permissions prefix to controllers assigns 24 | */ 25 | protected $controllers = []; // controllerName => prefix 26 | 27 | /** 28 | * Add a item node to items list 29 | * 30 | * @param int $type 31 | * @param $name 32 | * @param array $children Names of child nodes 33 | * @param \Closure $rule 34 | * @param string $title 35 | * @throws \Exception 36 | */ 37 | public function addItem($type, $name, $children = [], $rule = null, $title = '') 38 | { 39 | $class = $type == Item::TYPE_PERMISSION ? '\\SmartCrowd\\Rbac\\Permission' : '\\SmartCrowd\\Rbac\\Role'; 40 | $this->items[$name] = new $class([ 41 | 'type' => $type, 42 | 'name' => $name, 43 | 'rule' => $rule, 44 | 'title' => $title, 45 | ]); 46 | 47 | foreach ($children as $childName) { 48 | if (isset($this->items[$childName])) { 49 | $this->addChild($this->items[$name], $this->items[$childName]); 50 | } elseif (strpos($childName, '*') !== false) { 51 | $found = $this->search($childName); 52 | foreach ($found as $foundItem) { 53 | $this->addChild($this->items[$name], $this->items[$foundItem->name]); 54 | } 55 | } else { 56 | throw new \Exception("Can't add unknown permission '{$childName}' as child of '{$name}'"); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Searches items according given wildcard pattern 63 | * 64 | * @param string $pattern 65 | * @return array Founded items 66 | */ 67 | public function search($pattern) 68 | { 69 | $pattern = '/^' . str_replace(['.', '*'], ['\\.', '.*'], $pattern) . '$/i'; 70 | 71 | $found = []; 72 | foreach ($this->items as $item) { 73 | if (preg_match($pattern, $item->name)) { 74 | $found[] = $item; 75 | } 76 | } 77 | 78 | return $found; 79 | } 80 | 81 | /** 82 | * @param array $actions 83 | * @param array $permissions 84 | */ 85 | public function action($actions, $permissions) 86 | { 87 | foreach ($actions as $action) { 88 | $currentPermissions = isset($this->actions[$action]) ? $this->actions[$action] : []; 89 | $this->actions[$action] = array_merge($currentPermissions, $permissions); 90 | } 91 | } 92 | 93 | /** 94 | * @param string $controllerName 95 | * @param string $prefix 96 | */ 97 | public function controller($controllerName, $prefix) 98 | { 99 | $this->controllers[$controllerName] = $prefix; 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function getChildren() 106 | { 107 | return $this->children; 108 | } 109 | 110 | /** 111 | * @return array 112 | */ 113 | public function getActions() 114 | { 115 | return $this->actions; 116 | } 117 | 118 | /** 119 | * @return array 120 | */ 121 | public function getControllers() 122 | { 123 | return $this->controllers; 124 | } 125 | 126 | /** 127 | * @param $key 128 | * @return bool 129 | */ 130 | public function has($key) 131 | { 132 | return $this->offsetExists($key); 133 | } 134 | 135 | /** 136 | * {inheritdoc} 137 | */ 138 | public function offsetExists($offset) 139 | { 140 | return isset($this->items[$offset]); 141 | } 142 | 143 | /** 144 | * {inheritdoc} 145 | */ 146 | public function offsetGet($offset) 147 | { 148 | return $this->items[$offset]; 149 | } 150 | 151 | /** 152 | * {inheritdoc} 153 | */ 154 | public function offsetSet($offset, $value) 155 | { 156 | $this->items[$offset] = $value; 157 | } 158 | 159 | /** 160 | * {inheritdoc} 161 | */ 162 | public function offsetUnset($offset) 163 | { 164 | unset($this->items[$offset]); 165 | } 166 | 167 | /** 168 | * {inheritdoc} 169 | */ 170 | public function getIterator() 171 | { 172 | return new \ArrayIterator($this->items); 173 | } 174 | 175 | 176 | /** 177 | * Make a new tree relation 178 | * 179 | * @param Item $parent 180 | * @param Item $child 181 | * @return bool 182 | * @throws \Exception 183 | */ 184 | protected function addChild($parent, $child) 185 | { 186 | if (!isset($this->items[$parent->name], $this->items[$child->name])) { 187 | throw new \Exception("Either '{$parent->name}' or '{$child->name}' does not exist."); 188 | } 189 | 190 | if ($parent->name == $child->name) { 191 | throw new \Exception("Cannot add '{$parent->name} ' as a child of itself."); 192 | } 193 | 194 | if ($parent instanceof Permission && $child instanceof Role) { 195 | throw new \Exception("Cannot add a role as a child of a permission."); 196 | } 197 | 198 | if ($this->detectLoop($parent, $child)) { 199 | throw new \Exception("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected."); 200 | } 201 | 202 | if (isset($this->children[$parent->name][$child->name])) { 203 | throw new \Exception("The item '{$parent->name}' already has a child '{$child->name}'."); 204 | } 205 | 206 | $this->children[$parent->name][$child->name] = $this->items[$child->name]; 207 | 208 | return true; 209 | } 210 | 211 | /** 212 | * Checks whether there is a loop in the authorization item hierarchy. 213 | * 214 | * @param Item $parent parent item 215 | * @param Item $child the child item that is to be added to the hierarchy 216 | * @return boolean whether a loop exists 217 | */ 218 | protected function detectLoop($parent, $child) 219 | { 220 | if ($child->name === $parent->name) { 221 | return true; 222 | } 223 | 224 | if (!isset($this->children[$child->name], $this->items[$parent->name])) { 225 | return false; 226 | } 227 | 228 | foreach ($this->children[$child->name] as $grandchild) { 229 | /* @var $grandchild Item */ 230 | if ($this->detectLoop($parent, $grandchild)) { 231 | return true; 232 | } 233 | } 234 | return false; 235 | } 236 | } -------------------------------------------------------------------------------- /src/Rbac/Manager.php: -------------------------------------------------------------------------------- 1 | items = new ItemsRepository; 20 | } 21 | 22 | /** 23 | * @param Assignable|null $user 24 | * @param string $itemName 25 | * @param array $params 26 | * @return boolean 27 | */ 28 | public function checkAccess($user, $itemName, $params = []) 29 | { 30 | if (empty($user)) { 31 | return false; 32 | } 33 | 34 | $assignments = $user->getAssignments(); 35 | $contextAssignments = $this->resolveContextAssignments($user, $params); 36 | $assignments = array_merge($assignments, $contextAssignments); 37 | return $this->checkAccessRecursive($user, $itemName, $params, $assignments); 38 | } 39 | 40 | /** 41 | * @param string $itemName 42 | * @return bool 43 | */ 44 | public function has($itemName) 45 | { 46 | return isset($this->items[$itemName]); 47 | } 48 | 49 | /** 50 | * @param array|string $actions 51 | * @param array|string $permissions 52 | */ 53 | public function action($actions, $permissions) 54 | { 55 | if (!is_array($actions)) { 56 | $actions = [$actions]; 57 | } 58 | 59 | if (!is_array($permissions)) { 60 | $permissions = [$permissions]; 61 | } 62 | 63 | $this->items->action($actions, $permissions); 64 | } 65 | 66 | /** 67 | * @param string $controllerName 68 | * @param string $prefix 69 | */ 70 | public function controller($controllerName, $prefix) 71 | { 72 | $this->items->controller($controllerName, $prefix); 73 | } 74 | 75 | /** 76 | * @param string $name 77 | * @param array $children 78 | * @param \Closure $rule 79 | * @param string $title 80 | * @throws \Exception 81 | */ 82 | public function permission($name, $children = [], $rule = null, $title = '') 83 | { 84 | $this->items->addItem(Item::TYPE_PERMISSION, $name, $children, $rule, $title); 85 | } 86 | 87 | /** 88 | * @param string $name 89 | * @param array $children 90 | * @param string $title 91 | * @throws \Exception 92 | */ 93 | public function role($name, $children, $title = '') 94 | { 95 | $this->items->addItem(Item::TYPE_ROLE, $name, $children, null, $title); 96 | } 97 | 98 | /** 99 | * @param string $itemName 100 | * @param string $controller 101 | * @param string $foreignKey 102 | */ 103 | public function resource($itemName, $controller = null, $foreignKey = null) 104 | { 105 | $actions = [ 106 | 'index', 107 | 'create', 108 | 'store', 109 | 'show', 110 | 'edit', 111 | 'update', 112 | 'destroy' 113 | ]; 114 | 115 | $tasks = [ 116 | 'public' => [ 117 | 'index', 118 | 'show' 119 | ], 120 | 'manage' => [ 121 | 'update', 122 | 'edit', 123 | 'destroy' 124 | ] 125 | ]; 126 | 127 | foreach ($actions as $action) { 128 | $this->permission($itemName . '.' . $action); 129 | } 130 | 131 | foreach ($tasks as $taskName => $actions) { 132 | $this->permission($itemName . '.' . $taskName, array_map(function ($value) use ($itemName) { 133 | return $itemName . '.' . $value; 134 | }, $actions)); 135 | } 136 | 137 | if (!empty($foreignKey)) { 138 | $this->permission($itemName . '.manage.own', [$itemName . '.manage'], function ($params) use ($foreignKey, $itemName) { 139 | return $params[$itemName]->{$foreignKey} == $this->user->id; 140 | }); 141 | } 142 | 143 | if (!empty($controller)) { 144 | foreach ($actions as $action) { 145 | $this->action($controller . '@' . $action, $itemName . '.' . $action); 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * @return ItemsRepository 152 | */ 153 | public function getRepository() 154 | { 155 | return $this->items; 156 | } 157 | 158 | /** 159 | * @var ItemsRepository $repository 160 | */ 161 | public function setRepository(ItemsRepository $repository) 162 | { 163 | $this->items = $repository; 164 | } 165 | 166 | /** 167 | * Performs access check for the specified user. 168 | * This method is internally called by [[checkAccess()]]. 169 | * 170 | * @param Assignable $user the user. 171 | * @param string $itemName the name of the operation that need access check. 172 | * @param array $params name-value pairs that would be passed to rules associated. 173 | * with the permissions and roles assigned to the user. 174 | * @param array $assignments the list of permissions and roles, assigned to the specified user. 175 | * @return boolean whether the operations can be performed by the user. 176 | */ 177 | protected function checkAccessRecursive(Assignable $user, $itemName, $params, $assignments) 178 | { 179 | if (!$this->items->has($itemName)) { 180 | return false; 181 | } 182 | 183 | /* @var $item Item */ 184 | $item = $this->items[$itemName]; 185 | 186 | if (!$this->executeRule($user, $item, $params)) { 187 | return false; 188 | } 189 | 190 | if (in_array($itemName, $assignments)) { 191 | return true; 192 | } 193 | 194 | foreach ($this->items->getChildren() as $parentName => $children) { 195 | if (isset($children[$itemName]) && $this->checkAccessRecursive($user, $parentName, $params, $assignments)) { 196 | return true; 197 | } 198 | } 199 | return false; 200 | } 201 | 202 | /** 203 | * Executes the rule associated with the specified auth item. 204 | * 205 | * If the item does not specify a rule, this method will return true. Otherwise, it will 206 | * return the value of rule execution. 207 | * 208 | * @param Assignable $user the user. 209 | * @param Item $item the auth item that needs to execute its rule 210 | * @param array $params parameters passed to [[ManagerInterface::checkAccess()]] and will be passed to the rule 211 | * @return boolean the return value of rule execution. If the auth item does not specify a rule, true will be returned. 212 | */ 213 | protected function executeRule($user, $item, $params) 214 | { 215 | if ($item->rule instanceof \Closure) { 216 | return (new Rule($item->rule)) 217 | ->setUser($user) 218 | ->setItem($item) 219 | ->execute($params); 220 | } 221 | return true; 222 | } 223 | 224 | /** 225 | * Extracts assignments from business rules parameters, 226 | * if they are RBAC context, or context accessor. 227 | * 228 | * @param Assignable $user 229 | * @param array $params Business rules parameters. 230 | * @return array Array of new context assignments for current checked user. 231 | */ 232 | protected function resolveContextAssignments($user, $params) 233 | { 234 | $assignments = []; 235 | foreach ($params as $parameter) { 236 | if ($parameter instanceof RbacContext) { 237 | $assignments = array_merge($assignments, $parameter->getAssignments($user)); 238 | } 239 | if ($parameter instanceof RbacContextAccessor) { 240 | $assignments = array_merge( 241 | $assignments, 242 | $this->resolveContextAssignments($user, [$parameter->getContext()]) 243 | ); 244 | } 245 | } 246 | 247 | return $assignments; 248 | } 249 | 250 | /** 251 | * @return Item array 252 | */ 253 | public function getActions() 254 | { 255 | return $this->items->getActions(); 256 | } 257 | 258 | /** 259 | * @return Item array 260 | */ 261 | public function getControllers() 262 | { 263 | return $this->items->getControllers(); 264 | } 265 | } -------------------------------------------------------------------------------- /src/Rbac/Middleware/RbacMiddleware.php: -------------------------------------------------------------------------------- 1 | manager = $rbacManager; 20 | } 21 | 22 | /** 23 | * Run the request filter. 24 | * 25 | * @param \Illuminate\Http\Request $request 26 | * @param \Closure $next 27 | * @param array $permissions 28 | * @return mixed 29 | */ 30 | public function handle($request, \Closure $next, $permissions = []) 31 | { 32 | $route = $request->route(); 33 | 34 | if (empty($permissions)) { 35 | $permissions = $this->resolvePermission($route); 36 | } 37 | 38 | if (!is_array($permissions)) { 39 | $permissions = [$permissions]; 40 | } 41 | foreach ($permissions as $permission){ 42 | if (!Auth::check() || !$this->manager->checkAccess(Auth::user(), $permission, $route->parameters())) { 43 | throw new AccessDeniedHttpException; 44 | } 45 | } 46 | 47 | return $next($request); 48 | } 49 | 50 | private function resolvePermission($route) 51 | { 52 | $rbacActions = $this->manager->getActions(); 53 | $rbacControllers = $this->manager->getControllers(); 54 | 55 | $action = $route->getAction(); 56 | 57 | $actionNameSlash = str_replace($action['namespace'], '', $action['uses']); 58 | $actionName = ltrim($actionNameSlash, '\\'); 59 | $actionParts = explode('@', $actionName); 60 | 61 | if (isset($rbacActions[$actionName])) { 62 | $permissionName = $rbacActions[$actionName]; 63 | } elseif (isset($rbacControllers[$actionParts[0]])) { 64 | $permissionName = $rbacControllers[$actionParts[0]] . '.' . $actionParts[1]; 65 | } else { 66 | $permissionName = $this->dotStyle($actionName); 67 | } 68 | 69 | return $permissionName; 70 | } 71 | 72 | private function dotStyle($action) 73 | { 74 | return str_replace(['@', '\\'], '.', str_replace('controller', '', strtolower($action))); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/Rbac/Permission.php: -------------------------------------------------------------------------------- 1 | app->singleton('SmartCrowd\Rbac\Contracts\RbacManager', 'SmartCrowd\Rbac\Manager'); 22 | 23 | if (File::exists(Config::get('rbac.itemsPath'))) { 24 | File::requireOnce(Config::get('rbac.itemsPath')); 25 | } 26 | 27 | if (File::exists(Config::get('rbac.actionsPath'))) { 28 | File::requireOnce(Config::get('rbac.actionsPath')); 29 | } 30 | } 31 | 32 | /** 33 | * Perform post-registration booting of services. 34 | * 35 | * @return void 36 | */ 37 | public function boot() 38 | { 39 | $this->publishes([ 40 | __DIR__ . '/install/config/rbac.php' => config_path('rbac.php'), 41 | __DIR__ . '/install/Rbac' => app_path('Rbac'), 42 | ]); 43 | 44 | $this->registerDirectives(); 45 | } 46 | 47 | private function registerDirectives() 48 | { 49 | Blade::directive('allowed', function ($expression) { 50 | 51 | if (Str::startsWith($expression, '(')) { 52 | $expression = substr($expression, 1, -1); 53 | } 54 | 55 | return ""; 56 | }); 57 | 58 | if (Config::get('rbac.shortDirectives')) { 59 | 60 | foreach (Rbac::getRepository() as $name => $item) { 61 | 62 | $directiveName = $item->type == Item::TYPE_PERMISSION ? 'allowed' : 'is'; 63 | $directiveName .= Str::studly(str_replace('.', ' ', $name)); 64 | 65 | Blade::directive($directiveName, function($expression) use ($name) { 66 | 67 | $expression = trim($expression, '()'); 68 | if (!empty($expression)) { 69 | $expression = ', ' . $expression; 70 | } 71 | 72 | return ""; 73 | }); 74 | } 75 | } 76 | 77 | Blade::directive('endallowed', function($expression) { 78 | return ""; 79 | }); 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/Rbac/Role.php: -------------------------------------------------------------------------------- 1 | closure = $closure->bindTo($this, $this); 26 | } 27 | 28 | /** 29 | * @param $user 30 | * @return $this 31 | */ 32 | public function setUser($user) 33 | { 34 | $this->user = $user; 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param Item $item 40 | * @return $this 41 | */ 42 | public function setItem($item) 43 | { 44 | $this->item = $item; 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param array $params 50 | * @return boolean 51 | */ 52 | public function execute($params) 53 | { 54 | $closure = $this->closure; 55 | return (boolean) $closure($params); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/Rbac/Traits/AllowedTrait.php: -------------------------------------------------------------------------------- 1 | ' for each controller 21 | * action, for example 'authorisation.postlogin' or 'authorisation.getlogout' 22 | * 23 | * 24 | * If you not bind permission directly or through prefix Rbac will try to find 25 | * needed permission by itself. For example, for action UsersController@show, 'users.show' will be searched 26 | */ -------------------------------------------------------------------------------- /src/Rbac/install/Rbac/items.php: -------------------------------------------------------------------------------- 1 | user->id == $params['user']->id; 17 | * }); 18 | * 19 | * 20 | * Rbac::role('user', [ 21 | * 'users.view', 22 | * 'users.update.self' 23 | * ]); 24 | * 25 | * Rbac::role('admin', [ 26 | * 'user', 27 | * 'users.update' 28 | * ]); 29 | * 30 | * 31 | * Rbac::resource('photo', 'PhotoController', 'owner_id'); 32 | * 33 | * Is equivalent for: 34 | * 35 | * Rbac::permission('photo.index'); 36 | * Rbac::permission('photo.create'); 37 | * Rbac::permission('photo.store'); 38 | * Rbac::permission('photo.show'); 39 | * Rbac::permission('photo.edit'); 40 | * Rbac::permission('photo.update'); 41 | * Rbac::permission('photo.destroy'); 42 | * 43 | * Rbac::permission('photo.public', [ 44 | * 'photo.index', 45 | * 'photo.show' 46 | * ]); 47 | * 48 | * Rbac::permission('photo.manage', [ 49 | * 'photo.update', 50 | * 'photo.edit', 51 | * 'photo.destroy' 52 | * ]); 53 | * 54 | * Rbac::permission('photo.manage.own', ['photo.manage'], function ($params) 55 | * { 56 | * return $params['photo']->owner_id == $this->user->id; 57 | * }); 58 | * 59 | * Rbac::action('PhotoController@index', 'photo.index'); 60 | * Rbac::action('PhotoController@create', 'photo.create'); 61 | * Rbac::action('PhotoController@store', 'photo.store'); 62 | * Rbac::action('PhotoController@show', 'photo.show'); 63 | * Rbac::action('PhotoController@edit', 'photo.edit'); 64 | * Rbac::action('PhotoController@update', 'photo.update'); 65 | * Rbac::action('PhotoController@destroy', 'photo.destroy'); 66 | */ 67 | -------------------------------------------------------------------------------- /src/Rbac/install/config/rbac.php: -------------------------------------------------------------------------------- 1 | app_path('Rbac/items.php'), 5 | 'actionsPath' => app_path('Rbac/actions.php'), 6 | 'shortDirectives' => false 7 | ]; -------------------------------------------------------------------------------- /tests/BladeTest.php: -------------------------------------------------------------------------------- 1 | 'SmartCrowd\Rbac\Facades\Rbac' 26 | ]; 27 | } 28 | 29 | protected function getEnvironmentSetUp($app) 30 | { 31 | $app['config']->set('rbac.itemsPath', __DIR__ . '/items.php'); 32 | $app['config']->set('rbac.actionsPath', __DIR__ . '/actions.php'); 33 | $app['config']->set('rbac.shortDirectives', true); 34 | } 35 | 36 | public function testDirectives() 37 | { 38 | Blade::compile('test.blade.php'); 39 | $this->assertEquals( 40 | file_get_contents(Blade::getCompiledPath('test.blade.php')), 41 | file_get_contents('test.compiled.php') 42 | ); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /tests/RbacTest.php: -------------------------------------------------------------------------------- 1 | 'SmartCrowd\Rbac\Facades\Rbac' 18 | ]; 19 | } 20 | 21 | protected function getEnvironmentSetUp($app) 22 | { 23 | $app['config']->set('rbac.itemsPath', __DIR__ . '/items.php'); 24 | $app['config']->set('rbac.actionsPath', __DIR__ . '/actions.php'); 25 | } 26 | 27 | public function rules() 28 | { 29 | $admin = new User(1, ['admin']); 30 | $user = new User(2, ['user']); 31 | $entity1 = (object) ['author_id' => 2]; 32 | $entity2 = (object) ['author_id' => 3]; 33 | 34 | $ret = []; 35 | foreach (['news', 'article'] as $name) { 36 | $ret = array_merge($ret, [ 37 | [$admin, $entity1, $name . '.destroy', true], 38 | [$admin, $entity1, $name . '.update', true], 39 | [$admin, $entity2, $name . '.destroy', true], 40 | [$admin, $entity2, $name . '.update', true], 41 | [$user, $entity1, $name . '.destroy', true], 42 | [$user, $entity1, $name . '.update', true], 43 | [$user, $entity2, $name . '.destroy', false], 44 | [$user, $entity2, $name . '.update', false], 45 | ]); 46 | } 47 | 48 | return $ret; 49 | } 50 | 51 | /** 52 | * @dataProvider rules 53 | */ 54 | public function testRbac($subject, $object, $action, $result) 55 | { 56 | $params = []; 57 | foreach (['news', 'article'] as $name) { 58 | $params[$name] = $object; 59 | } 60 | $this->assertEquals($result, $subject->allowed($action, $params)); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /tests/User.php: -------------------------------------------------------------------------------- 1 | id = $id; 18 | $this->roles = $roles; 19 | } 20 | 21 | public function getAssignments() 22 | { 23 | return $this->roles; 24 | } 25 | } -------------------------------------------------------------------------------- /tests/actions.php: -------------------------------------------------------------------------------- 1 | ' for each controller 21 | * action, for example 'authorisation.postlogin' or 'authorisation.getlogout' 22 | * 23 | * 24 | * If you not bind permission directly or through prefix Rbac will try to find 25 | * needed permission by itself. For example, for action UsersController@show, 'users.show' will be searched 26 | */ -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('SmartCrowd\\Rbac\\', __DIR__); -------------------------------------------------------------------------------- /tests/items.php: -------------------------------------------------------------------------------- 1 | user->id == $params['news']->author_id; 10 | }); 11 | 12 | Rbac::resource('article', 'ArticlesController', 'author_id'); 13 | 14 | Rbac::role('admin', [ 15 | 'news.manage', 16 | 'article.manage' // from resource 17 | ]); 18 | Rbac::role('user', [ 19 | 'news.manage.own', 20 | 'article.manage.own', // from resource 21 | ]); -------------------------------------------------------------------------------- /tests/test.blade.php: -------------------------------------------------------------------------------- 1 | @allowed('article.edit', ['article' => ['id' => 1]]) 2 | @else 3 | @endallowed 4 | 5 | @allowedArticleEdit(['article' => ['id' => 1]]) 6 | @endallowed 7 | 8 | @allowedArticlePublic 9 | @endallowed 10 | 11 | @allowedWrongPermissionName 12 | @endallowed -------------------------------------------------------------------------------- /tests/test.compiled.php: -------------------------------------------------------------------------------- 1 | ['id' => 1]])): ?> 2 | 3 | 4 | 5 | ['id' => 1]])): ?> 6 | 7 | 8 | 9 | 10 | 11 | @allowedWrongPermissionName 12 | --------------------------------------------------------------------------------