]> BookStack Code Mirror - bookstack/blob - app/Services/RestrictionService.php
0050401bf75586d8559229ec15645c77b7aadfb0
[bookstack] / app / Services / RestrictionService.php
1 <?php namespace BookStack\Services;
2
3 use BookStack\Book;
4 use BookStack\Chapter;
5 use BookStack\Entity;
6 use BookStack\EntityPermission;
7 use BookStack\Page;
8 use BookStack\Role;
9 use Illuminate\Database\Eloquent\Collection;
10
11 class RestrictionService
12 {
13
14     protected $userRoles;
15     protected $isAdmin;
16     protected $currentAction;
17     protected $currentUser;
18
19     public $book;
20     public $chapter;
21     public $page;
22
23     protected $entityPermission;
24     protected $role;
25
26     /**
27      * The actions that have permissions attached throughout the application.
28      * @var array
29      */
30     protected $actions = ['view', 'create', 'update', 'delete'];
31
32     /**
33      * RestrictionService constructor.
34      * @param EntityPermission $entityPermission
35      * @param Book $book
36      * @param Chapter $chapter
37      * @param Page $page
38      * @param Role $role
39      */
40     public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role)
41     {
42         $this->currentUser = auth()->user();
43         $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
44         $this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
45
46         $this->entityPermission = $entityPermission;
47         $this->role = $role;
48         $this->book = $book;
49         $this->chapter = $chapter;
50         $this->page = $page;
51     }
52
53     /**
54      * Re-generate all entity permission from scratch.
55      */
56     public function buildEntityPermissions()
57     {
58         $this->entityPermission->truncate();
59
60         // Get all roles (Should be the most limited dimension)
61         $roles = $this->role->load('permissions')->all();
62
63         // Chunk through all books
64         $this->book->chunk(500, function ($books) use ($roles) {
65             $this->createManyEntityPermissions($books, $roles);
66         });
67
68         // Chunk through all chapters
69         $this->chapter->with('book')->chunk(500, function ($books) use ($roles) {
70             $this->createManyEntityPermissions($books, $roles);
71         });
72
73         // Chunk through all pages
74         $this->page->with('book', 'chapter')->chunk(500, function ($books) use ($roles) {
75             $this->createManyEntityPermissions($books, $roles);
76         });
77     }
78
79     /**
80      * Create the entity permissions for a particular entity.
81      * @param Entity $entity
82      */
83     public function buildEntityPermissionsForEntity(Entity $entity)
84     {
85         $roles = $this->role->load('permissions')->all();
86         $entities = collect([$entity]);
87
88         if ($entity->isA('book')) {
89             $entities = $entities->merge($entity->chapters);
90             $entities = $entities->merge($entity->pages);
91         } elseif ($entity->isA('chapter')) {
92             $entities = $entities->merge($entity->pages);
93         }
94
95         $this->deleteManyEntityPermissionsForEntities($entities);
96         $this->createManyEntityPermissions($entities, $roles);
97     }
98
99     /**
100      * Build the entity permissions for a particular role.
101      * @param Role $role
102      */
103     public function buildEntityPermissionForRole(Role $role)
104     {
105         $roles = collect([$role]);
106
107         $this->deleteManyEntityPermissionsForRoles($roles);
108
109         // Chunk through all books
110         $this->book->chunk(500, function ($books) use ($roles) {
111             $this->createManyEntityPermissions($books, $roles);
112         });
113
114         // Chunk through all chapters
115         $this->chapter->with('book')->chunk(500, function ($books) use ($roles) {
116             $this->createManyEntityPermissions($books, $roles);
117         });
118
119         // Chunk through all pages
120         $this->page->with('book', 'chapter')->chunk(500, function ($books) use ($roles) {
121             $this->createManyEntityPermissions($books, $roles);
122         });
123     }
124
125     /**
126      * Delete the entity permissions attached to a particular role.
127      * @param Role $role
128      */
129     public function deleteEntityPermissionsForRole(Role $role)
130     {
131         $this->deleteManyEntityPermissionsForRoles([$role]);
132     }
133
134     /**
135      * Delete all of the entity permissions for a list of entities.
136      * @param Role[] $roles
137      */
138     protected function deleteManyEntityPermissionsForRoles($roles)
139     {
140         foreach ($roles as $role) {
141             $role->entityPermissions()->delete();
142         }
143     }
144
145     /**
146      * Delete the entity permissions for a particular entity.
147      * @param Entity $entity
148      */
149     public function deleteEntityPermissionsForEntity(Entity $entity)
150     {
151         $this->deleteManyEntityPermissionsForEntities([$entity]);
152     }
153
154     /**
155      * Delete all of the entity permissions for a list of entities.
156      * @param Entity[] $entities
157      */
158     protected function deleteManyEntityPermissionsForEntities($entities)
159     {
160         foreach ($entities as $entity) {
161             $entity->permissions()->delete();
162         }
163     }
164
165     /**
166      * Create & Save entity permissions for many entities and permissions.
167      * @param Collection $entities
168      * @param Collection $roles
169      */
170     protected function createManyEntityPermissions($entities, $roles)
171     {
172         $entityPermissions = [];
173         foreach ($entities as $entity) {
174             foreach ($roles as $role) {
175                 foreach ($this->actions as $action) {
176                     $entityPermissions[] = $this->createEntityPermissionData($entity, $role, $action);
177                 }
178             }
179         }
180         \Log::info(collect($entityPermissions)->where('entity_id', 1)->where('entity_type', 'BookStack\\Page')->where('role_id', 2)->all());
181         $this->entityPermission->insert($entityPermissions);
182     }
183
184     /**
185      * Create entity permission data for an entity and role
186      * for a particular action.
187      * @param Entity $entity
188      * @param Role $role
189      * @param $action
190      * @return array
191      */
192     protected function createEntityPermissionData(Entity $entity, Role $role, $action)
193     {
194         $permissionPrefix = $entity->getType() . '-' . $action;
195         $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
196         $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
197
198         if ($entity->isA('book')) {
199
200             if (!$entity->restricted) {
201                 return $this->createEntityPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
202             } else {
203                 $hasAccess = $entity->hasActiveRestriction($role->id, $action);
204                 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
205             }
206
207         } elseif ($entity->isA('chapter')) {
208
209             if (!$entity->restricted) {
210                 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $action);
211                 $hasPermissiveAccessToBook = !$entity->book->restricted;
212                 return $this->createEntityPermissionDataArray($entity, $role, $action,
213                     ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
214                     ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
215             } else {
216                 $hasAccess = $entity->hasActiveRestriction($role->id, $action);
217                 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
218             }
219
220         } elseif ($entity->isA('page')) {
221
222             if (!$entity->restricted) {
223                 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $action);
224                 $hasPermissiveAccessToBook = !$entity->book->restricted;
225                 $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $action);
226                 $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
227                 $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
228
229                 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
230                 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
231
232                 return $this->createEntityPermissionDataArray($entity, $role, $action,
233                     ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
234                     ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
235                 );
236             } else {
237                 $hasAccess = $entity->hasRestriction($role->id, $action);
238                 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
239             }
240
241         }
242     }
243
244     /**
245      * Create an array of data with the information of an entity permissions.
246      * Used to build data for bulk insertion.
247      * @param Entity $entity
248      * @param Role $role
249      * @param $action
250      * @param $permissionAll
251      * @param $permissionOwn
252      * @return array
253      */
254     protected function createEntityPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
255     {
256         $entityClass = get_class($entity);
257         return [
258             'role_id'            => $role->id,
259             'entity_id'          => $entity->id,
260             'entity_type'        => $entityClass,
261             'action'             => $action,
262             'has_permission'     => $permissionAll,
263             'has_permission_own' => $permissionOwn,
264             'created_by'         => $entity->created_by
265         ];
266     }
267
268     /**
269      * Checks if an entity has a restriction set upon it.
270      * @param Entity $entity
271      * @param $permission
272      * @return bool
273      */
274     public function checkEntityUserAccess(Entity $entity, $permission)
275     {
276         if ($this->isAdmin) return true;
277         $explodedPermission = explode('-', $permission);
278
279         $baseQuery = $entity->where('id', '=', $entity->id);
280
281         $nonEntityPermissions = ['restrictions'];
282
283         // Handle non entity specific permissions
284         if (in_array($explodedPermission[0], $nonEntityPermissions)) {
285             $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
286             $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
287             $this->currentAction = 'view';
288             $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
289             return ($allPermission || ($isOwner && $ownPermission));
290         }
291
292         $action = end($explodedPermission);
293         $this->currentAction = $action;
294         return $this->entityRestrictionQuery($baseQuery)->count() > 0;
295     }
296
297     /**
298      * Check if an entity has restrictions set on itself or its
299      * parent tree.
300      * @param Entity $entity
301      * @param $action
302      * @return bool|mixed
303      */
304     public function checkIfRestrictionsSet(Entity $entity, $action)
305     {
306         $this->currentAction = $action;
307         if ($entity->isA('page')) {
308             return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
309         } elseif ($entity->isA('chapter')) {
310             return $entity->restricted || $entity->book->restricted;
311         } elseif ($entity->isA('book')) {
312             return $entity->restricted;
313         }
314     }
315
316     /**
317      * The general query filter to remove all entities
318      * that the current user does not have access to.
319      * @param $query
320      * @return mixed
321      */
322     protected function entityRestrictionQuery($query)
323     {
324         return $query->where(function ($parentQuery) {
325             $parentQuery->whereHas('permissions', function ($permissionQuery) {
326                 $permissionQuery->whereIn('role_id', $this->userRoles)
327                     ->where('action', '=', $this->currentAction)
328                     ->where(function ($query) {
329                         $query->where('has_permission', '=', true)
330                             ->orWhere(function ($query) {
331                                 $query->where('has_permission_own', '=', true)
332                                     ->where('created_by', '=', $this->currentUser->id);
333                             });
334                     });
335             });
336         });
337     }
338
339     /**
340      * Add restrictions for a page query
341      * @param $query
342      * @param string $action
343      * @return mixed
344      */
345     public function enforcePageRestrictions($query, $action = 'view')
346     {
347         // Prevent drafts being visible to others.
348         $query = $query->where(function ($query) {
349             $query->where('draft', '=', false);
350             if ($this->currentUser) {
351                 $query->orWhere(function ($query) {
352                     $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
353                 });
354             }
355         });
356
357         if ($this->isAdmin) return $query;
358         $this->currentAction = $action;
359         return $this->entityRestrictionQuery($query);
360     }
361
362     /**
363      * Add on permission restrictions to a chapter query.
364      * @param $query
365      * @param string $action
366      * @return mixed
367      */
368     public function enforceChapterRestrictions($query, $action = 'view')
369     {
370         if ($this->isAdmin) return $query;
371         $this->currentAction = $action;
372         return $this->entityRestrictionQuery($query);
373     }
374
375     /**
376      * Add restrictions to a book query.
377      * @param $query
378      * @param string $action
379      * @return mixed
380      */
381     public function enforceBookRestrictions($query, $action = 'view')
382     {
383         if ($this->isAdmin) return $query;
384         $this->currentAction = $action;
385         return $this->entityRestrictionQuery($query);
386     }
387
388     /**
389      * Filter items that have entities set a a polymorphic relation.
390      * @param $query
391      * @param string $tableName
392      * @param string $entityIdColumn
393      * @param string $entityTypeColumn
394      * @return mixed
395      */
396     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
397     {
398         if ($this->isAdmin) return $query;
399         $this->currentAction = 'view';
400         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
401
402         return $query->where(function ($query) use ($tableDetails) {
403             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
404                 $permissionQuery->select('id')->from('entity_permissions')
405                     ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
406                     ->whereRaw('entity_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
407                     ->where('action', '=', $this->currentAction)
408                     ->whereIn('role_id', $this->userRoles)
409                     ->where(function ($query) {
410                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
411                             $query->where('has_permission_own', '=', true)
412                                 ->where('created_by', '=', $this->currentUser->id);
413                         });
414                     });
415             });
416         });
417
418     }
419
420     /**
421      * Filters pages that are a direct relation to another item.
422      * @param $query
423      * @param $tableName
424      * @param $entityIdColumn
425      * @return mixed
426      */
427     public function filterRelatedPages($query, $tableName, $entityIdColumn)
428     {
429         if ($this->isAdmin) return $query;
430         $this->currentAction = 'view';
431         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
432
433         return $query->where(function ($query) use ($tableDetails) {
434             $query->where(function ($query) use (&$tableDetails) {
435                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
436                     $permissionQuery->select('id')->from('entity_permissions')
437                         ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
438                         ->where('entity_type', '=', 'Bookstack\\Page')
439                         ->where('action', '=', $this->currentAction)
440                         ->whereIn('role_id', $this->userRoles)
441                         ->where(function ($query) {
442                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
443                                 $query->where('has_permission_own', '=', true)
444                                     ->where('created_by', '=', $this->currentUser ? $this->currentUser->id : 0);
445                             });
446                         });
447                 });
448             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
449         });
450     }
451
452 }