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