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