]> BookStack Code Mirror - bookstack/blob - app/Services/PermissionService.php
Actually include the Queueable namespace...
[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\Connection;
12 use Illuminate\Database\Eloquent\Builder;
13 use Illuminate\Support\Collection;
14
15 class PermissionService
16 {
17
18     protected $currentAction;
19     protected $isAdminUser;
20     protected $userRoles = false;
21     protected $currentUserModel = false;
22
23     public $book;
24     public $chapter;
25     public $page;
26
27     protected $db;
28
29     protected $jointPermission;
30     protected $role;
31
32     protected $entityCache;
33
34     /**
35      * PermissionService constructor.
36      * @param JointPermission $jointPermission
37      * @param Connection $db
38      * @param Book $book
39      * @param Chapter $chapter
40      * @param Page $page
41      * @param Role $role
42      */
43     public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
44     {
45         $this->db = $db;
46         $this->jointPermission = $jointPermission;
47         $this->role = $role;
48         $this->book = $book;
49         $this->chapter = $chapter;
50         $this->page = $page;
51         // TODO - Update so admin still goes through filters
52     }
53
54     /**
55      * Prepare the local entity cache and ensure it's empty
56      */
57     protected function readyEntityCache()
58     {
59         $this->entityCache = [
60             'books' => collect(),
61             'chapters' => collect()
62         ];
63     }
64
65     /**
66      * Get a book via ID, Checks local cache
67      * @param $bookId
68      * @return Book
69      */
70     protected function getBook($bookId)
71     {
72         if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
73             return $this->entityCache['books']->get($bookId);
74         }
75
76         $book = $this->book->find($bookId);
77         if ($book === null) $book = false;
78         if (isset($this->entityCache['books'])) {
79             $this->entityCache['books']->put($bookId, $book);
80         }
81
82         return $book;
83     }
84
85     /**
86      * Get a chapter via ID, Checks local cache
87      * @param $chapterId
88      * @return Book
89      */
90     protected function getChapter($chapterId)
91     {
92         if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
93             return $this->entityCache['chapters']->get($chapterId);
94         }
95
96         $chapter = $this->chapter->find($chapterId);
97         if ($chapter === null) $chapter = false;
98         if (isset($this->entityCache['chapters'])) {
99             $this->entityCache['chapters']->put($chapterId, $chapter);
100         }
101
102         return $chapter;
103     }
104
105     /**
106      * Get the roles for the current user;
107      * @return array|bool
108      */
109     protected function getRoles()
110     {
111         if ($this->userRoles !== false) return $this->userRoles;
112
113         $roles = [];
114
115         if (auth()->guest()) {
116             $roles[] = $this->role->getSystemRole('public')->id;
117             return $roles;
118         }
119
120
121         foreach ($this->currentUser()->roles as $role) {
122             $roles[] = $role->id;
123         }
124         return $roles;
125     }
126
127     /**
128      * Re-generate all entity permission from scratch.
129      */
130     public function buildJointPermissions()
131     {
132         $this->jointPermission->truncate();
133         $this->readyEntityCache();
134
135         // Get all roles (Should be the most limited dimension)
136         $roles = $this->role->with('permissions')->get();
137
138         // Chunk through all books
139         $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
140             $this->createManyJointPermissions($books, $roles);
141         });
142
143         // Chunk through all chapters
144         $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
145             $this->createManyJointPermissions($chapters, $roles);
146         });
147
148         // Chunk through all pages
149         $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
150             $this->createManyJointPermissions($pages, $roles);
151         });
152     }
153
154     /**
155      * Rebuild the entity jointPermissions for a particular entity.
156      * @param Entity $entity
157      */
158     public function buildJointPermissionsForEntity(Entity $entity)
159     {
160         $roles = $this->role->get();
161         $entities = collect([$entity]);
162
163         if ($entity->isA('book')) {
164             $entities = $entities->merge($entity->chapters);
165             $entities = $entities->merge($entity->pages);
166         } elseif ($entity->isA('chapter')) {
167             $entities = $entities->merge($entity->pages);
168         }
169
170         $this->deleteManyJointPermissionsForEntities($entities);
171         $this->createManyJointPermissions($entities, $roles);
172     }
173
174     /**
175      * Rebuild the entity jointPermissions for a collection of entities.
176      * @param Collection $entities
177      */
178     public function buildJointPermissionsForEntities(Collection $entities)
179     {
180         $roles = $this->role->get();
181         $this->deleteManyJointPermissionsForEntities($entities);
182         $this->createManyJointPermissions($entities, $roles);
183     }
184
185     /**
186      * Build the entity jointPermissions for a particular role.
187      * @param Role $role
188      */
189     public function buildJointPermissionForRole(Role $role)
190     {
191         $roles = collect([$role]);
192
193         $this->deleteManyJointPermissionsForRoles($roles);
194
195         // Chunk through all books
196         $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
197             $this->createManyJointPermissions($books, $roles);
198         });
199
200         // Chunk through all chapters
201         $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
202             $this->createManyJointPermissions($books, $roles);
203         });
204
205         // Chunk through all pages
206         $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
207             $this->createManyJointPermissions($books, $roles);
208         });
209     }
210
211     /**
212      * Delete the entity jointPermissions attached to a particular role.
213      * @param Role $role
214      */
215     public function deleteJointPermissionsForRole(Role $role)
216     {
217         $this->deleteManyJointPermissionsForRoles([$role]);
218     }
219
220     /**
221      * Delete all of the entity jointPermissions for a list of entities.
222      * @param Role[] $roles
223      */
224     protected function deleteManyJointPermissionsForRoles($roles)
225     {
226         foreach ($roles as $role) {
227             $role->jointPermissions()->delete();
228         }
229     }
230
231     /**
232      * Delete the entity jointPermissions for a particular entity.
233      * @param Entity $entity
234      */
235     public function deleteJointPermissionsForEntity(Entity $entity)
236     {
237         $this->deleteManyJointPermissionsForEntities([$entity]);
238     }
239
240     /**
241      * Delete all of the entity jointPermissions for a list of entities.
242      * @param Entity[] $entities
243      */
244     protected function deleteManyJointPermissionsForEntities($entities)
245     {
246         if (count($entities) === 0) return;
247         $query = $this->jointPermission->newQuery();
248             foreach ($entities as $entity) {
249                 $query->orWhere(function($query) use ($entity) {
250                     $query->where('entity_id', '=', $entity->id)
251                         ->where('entity_type', '=', $entity->getMorphClass());
252                 });
253             }
254         $query->delete();
255     }
256
257     /**
258      * Create & Save entity jointPermissions for many entities and jointPermissions.
259      * @param Collection $entities
260      * @param Collection $roles
261      */
262     protected function createManyJointPermissions($entities, $roles)
263     {
264         $this->readyEntityCache();
265         $jointPermissions = [];
266         foreach ($entities as $entity) {
267             foreach ($roles as $role) {
268                 foreach ($this->getActions($entity) as $action) {
269                     $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
270                 }
271             }
272         }
273         $this->jointPermission->insert($jointPermissions);
274     }
275
276
277     /**
278      * Get the actions related to an entity.
279      * @param $entity
280      * @return array
281      */
282     protected function getActions($entity)
283     {
284         $baseActions = ['view', 'update', 'delete'];
285
286         if ($entity->isA('chapter')) {
287             $baseActions[] = 'page-create';
288         } else if ($entity->isA('book')) {
289             $baseActions[] = 'page-create';
290             $baseActions[] = 'chapter-create';
291         }
292
293          return $baseActions;
294     }
295
296     /**
297      * Create entity permission data for an entity and role
298      * for a particular action.
299      * @param Entity $entity
300      * @param Role $role
301      * @param $action
302      * @return array
303      */
304     protected function createJointPermissionData(Entity $entity, Role $role, $action)
305     {
306         $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
307         $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
308         $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
309         $explodedAction = explode('-', $action);
310         $restrictionAction = end($explodedAction);
311
312         if ($role->system_name === 'admin') {
313             return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
314         }
315
316         if ($entity->isA('book')) {
317
318             if (!$entity->restricted) {
319                 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
320             } else {
321                 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
322                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
323             }
324
325         } elseif ($entity->isA('chapter')) {
326
327             if (!$entity->restricted) {
328                 $book = $this->getBook($entity->book_id);
329                 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
330                 $hasPermissiveAccessToBook = !$book->restricted;
331                 return $this->createJointPermissionDataArray($entity, $role, $action,
332                     ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
333                     ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
334             } else {
335                 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
336                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
337             }
338
339         } elseif ($entity->isA('page')) {
340
341             if (!$entity->restricted) {
342                 $book = $this->getBook($entity->book_id);
343                 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
344                 $hasPermissiveAccessToBook = !$book->restricted;
345
346                 $chapter = $this->getChapter($entity->chapter_id);
347                 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
348                 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
349                 $acknowledgeChapter = ($chapter && $chapter->restricted);
350
351                 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
352                 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
353
354                 return $this->createJointPermissionDataArray($entity, $role, $action,
355                     ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
356                     ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
357                 );
358             } else {
359                 $hasAccess = $entity->hasRestriction($role->id, $action);
360                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
361             }
362
363         }
364     }
365
366     /**
367      * Create an array of data with the information of an entity jointPermissions.
368      * Used to build data for bulk insertion.
369      * @param Entity $entity
370      * @param Role $role
371      * @param $action
372      * @param $permissionAll
373      * @param $permissionOwn
374      * @return array
375      */
376     protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
377     {
378         $entityClass = get_class($entity);
379         return [
380             'role_id'            => $role->getRawAttribute('id'),
381             'entity_id'          => $entity->getRawAttribute('id'),
382             'entity_type'        => $entityClass,
383             'action'             => $action,
384             'has_permission'     => $permissionAll,
385             'has_permission_own' => $permissionOwn,
386             'created_by'         => $entity->getRawAttribute('created_by')
387         ];
388     }
389
390     /**
391      * Checks if an entity has a restriction set upon it.
392      * @param Ownable $ownable
393      * @param $permission
394      * @return bool
395      */
396     public function checkOwnableUserAccess(Ownable $ownable, $permission)
397     {
398         if ($this->isAdmin()) {
399             $this->clean();
400             return true;
401         }
402
403         $explodedPermission = explode('-', $permission);
404
405         $baseQuery = $ownable->where('id', '=', $ownable->id);
406         $action = end($explodedPermission);
407         $this->currentAction = $action;
408
409         $nonJointPermissions = ['restrictions', 'image', 'attachment'];
410
411         // Handle non entity specific jointPermissions
412         if (in_array($explodedPermission[0], $nonJointPermissions)) {
413             $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
414             $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
415             $this->currentAction = 'view';
416             $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
417             return ($allPermission || ($isOwner && $ownPermission));
418         }
419
420         // Handle abnormal create jointPermissions
421         if ($action === 'create') {
422             $this->currentAction = $permission;
423         }
424
425         $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
426         $this->clean();
427         return $q;
428     }
429
430     /**
431      * Check if an entity has restrictions set on itself or its
432      * parent tree.
433      * @param Entity $entity
434      * @param $action
435      * @return bool|mixed
436      */
437     public function checkIfRestrictionsSet(Entity $entity, $action)
438     {
439         $this->currentAction = $action;
440         if ($entity->isA('page')) {
441             return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
442         } elseif ($entity->isA('chapter')) {
443             return $entity->restricted || $entity->book->restricted;
444         } elseif ($entity->isA('book')) {
445             return $entity->restricted;
446         }
447     }
448
449     /**
450      * The general query filter to remove all entities
451      * that the current user does not have access to.
452      * @param $query
453      * @return mixed
454      */
455     protected function entityRestrictionQuery($query)
456     {
457         $q = $query->where(function ($parentQuery) {
458             $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
459                 $permissionQuery->whereIn('role_id', $this->getRoles())
460                     ->where('action', '=', $this->currentAction)
461                     ->where(function ($query) {
462                         $query->where('has_permission', '=', true)
463                             ->orWhere(function ($query) {
464                                 $query->where('has_permission_own', '=', true)
465                                     ->where('created_by', '=', $this->currentUser()->id);
466                             });
467                     });
468             });
469         });
470         $this->clean();
471         return $q;
472     }
473
474     /**
475      * Get the children of a book in an efficient single query, Filtered by the permission system.
476      * @param integer $book_id
477      * @param bool $filterDrafts
478      * @param bool $fetchPageContent
479      * @return \Illuminate\Database\Query\Builder
480      */
481     public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
482         $pageContentSelect = $fetchPageContent ? 'html' : "''";
483         $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
484             $query->where('draft', '=', 0);
485             if (!$filterDrafts) {
486                 $query->orWhere(function($query) {
487                     $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
488                 });
489             }
490         });
491         $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
492         $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
493             ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
494
495         if (!$this->isAdmin()) {
496             $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
497                 ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
498                 ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
499                 ->where(function($query) {
500                     $query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
501                         $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
502                     });
503                 });
504             $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
505         }
506
507         $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
508         $this->clean();
509         return  $query;
510     }
511
512     /**
513      * Add restrictions for a generic entity
514      * @param string $entityType
515      * @param Builder|Entity $query
516      * @param string $action
517      * @return mixed
518      */
519     public function enforceEntityRestrictions($entityType, $query, $action = 'view')
520     {
521         if (strtolower($entityType) === 'page') {
522             // Prevent drafts being visible to others.
523             $query = $query->where(function ($query) {
524                 $query->where('draft', '=', false);
525                 if ($this->currentUser()) {
526                     $query->orWhere(function ($query) {
527                         $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
528                     });
529                 }
530             });
531         }
532
533         if ($this->isAdmin()) {
534             $this->clean();
535             return $query;
536         }
537
538         $this->currentAction = $action;
539         return $this->entityRestrictionQuery($query);
540     }
541
542     /**
543      * Filter items that have entities set a a polymorphic relation.
544      * @param $query
545      * @param string $tableName
546      * @param string $entityIdColumn
547      * @param string $entityTypeColumn
548      * @return mixed
549      */
550     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
551     {
552         if ($this->isAdmin()) {
553             $this->clean();
554             return $query;
555         }
556
557         $this->currentAction = 'view';
558         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
559
560         $q = $query->where(function ($query) use ($tableDetails) {
561             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
562                 $permissionQuery->select('id')->from('joint_permissions')
563                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
564                     ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
565                     ->where('action', '=', $this->currentAction)
566                     ->whereIn('role_id', $this->getRoles())
567                     ->where(function ($query) {
568                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
569                             $query->where('has_permission_own', '=', true)
570                                 ->where('created_by', '=', $this->currentUser()->id);
571                         });
572                     });
573             });
574         });
575         $this->clean();
576         return $q;
577     }
578
579     /**
580      * Filters pages that are a direct relation to another item.
581      * @param $query
582      * @param $tableName
583      * @param $entityIdColumn
584      * @return mixed
585      */
586     public function filterRelatedPages($query, $tableName, $entityIdColumn)
587     {
588         if ($this->isAdmin()) {
589             $this->clean();
590             return $query;
591         }
592
593         $this->currentAction = 'view';
594         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
595
596         $q = $query->where(function ($query) use ($tableDetails) {
597             $query->where(function ($query) use (&$tableDetails) {
598                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
599                     $permissionQuery->select('id')->from('joint_permissions')
600                         ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
601                         ->where('entity_type', '=', 'Bookstack\\Page')
602                         ->where('action', '=', $this->currentAction)
603                         ->whereIn('role_id', $this->getRoles())
604                         ->where(function ($query) {
605                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
606                                 $query->where('has_permission_own', '=', true)
607                                     ->where('created_by', '=', $this->currentUser()->id);
608                             });
609                         });
610                 });
611             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
612         });
613         $this->clean();
614         return $q;
615     }
616
617     /**
618      * Check if the current user is an admin.
619      * @return bool
620      */
621     private function isAdmin()
622     {
623         if ($this->isAdminUser === null) {
624             $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
625         }
626
627         return $this->isAdminUser;
628     }
629
630     /**
631      * Get the current user
632      * @return User
633      */
634     private function currentUser()
635     {
636         if ($this->currentUserModel === false) {
637             $this->currentUserModel = user();
638         }
639
640         return $this->currentUserModel;
641     }
642
643     /**
644      * Clean the cached user elements.
645      */
646     private function clean()
647     {
648         $this->currentUserModel = false;
649         $this->userRoles = false;
650         $this->isAdminUser = null;
651     }
652
653 }