├── .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 |
--------------------------------------------------------------------------------