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