]> BookStack Code Mirror - bookstack/blob - app/Services/PermissionService.php
Fixed model extending mis-use
[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         $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
483             $query->where('draft', '=', 0);
484             if (!$filterDrafts) {
485                 $query->orWhere(function($query) {
486                     $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
487                 });
488             }
489         });
490         $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
491         $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
492             ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
493
494         if (!$this->isAdmin()) {
495             $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
496                 ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
497                 ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
498                 ->where(function($query) {
499                     $query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
500                         $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
501                     });
502                 });
503             $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
504         }
505
506         $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
507         $this->clean();
508         return  $query;
509     }
510
511     /**
512      * Add restrictions for a generic entity
513      * @param string $entityType
514      * @param Builder|Entity $query
515      * @param string $action
516      * @return Builder
517      */
518     public function enforceEntityRestrictions($entityType, $query, $action = 'view')
519     {
520         if (strtolower($entityType) === 'page') {
521             // Prevent drafts being visible to others.
522             $query = $query->where(function ($query) {
523                 $query->where('draft', '=', false);
524                 if ($this->currentUser()) {
525                     $query->orWhere(function ($query) {
526                         $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
527                     });
528                 }
529             });
530         }
531
532         if ($this->isAdmin()) {
533             $this->clean();
534             return $query;
535         }
536
537         $this->currentAction = $action;
538         return $this->entityRestrictionQuery($query);
539     }
540
541     /**
542      * Filter items that have entities set as a polymorphic relation.
543      * @param $query
544      * @param string $tableName
545      * @param string $entityIdColumn
546      * @param string $entityTypeColumn
547      * @return mixed
548      */
549     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
550     {
551         if ($this->isAdmin()) {
552             $this->clean();
553             return $query;
554         }
555
556         $this->currentAction = 'view';
557         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
558
559         $q = $query->where(function ($query) use ($tableDetails) {
560             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
561                 $permissionQuery->select('id')->from('joint_permissions')
562                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
563                     ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
564                     ->where('action', '=', $this->currentAction)
565                     ->whereIn('role_id', $this->getRoles())
566                     ->where(function ($query) {
567                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
568                             $query->where('has_permission_own', '=', true)
569                                 ->where('created_by', '=', $this->currentUser()->id);
570                         });
571                     });
572             });
573         });
574         $this->clean();
575         return $q;
576     }
577
578     /**
579      * Filters pages that are a direct relation to another item.
580      * @param $query
581      * @param $tableName
582      * @param $entityIdColumn
583      * @return mixed
584      */
585     public function filterRelatedPages($query, $tableName, $entityIdColumn)
586     {
587         if ($this->isAdmin()) {
588             $this->clean();
589             return $query;
590         }
591
592         $this->currentAction = 'view';
593         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
594
595         $q = $query->where(function ($query) use ($tableDetails) {
596             $query->where(function ($query) use (&$tableDetails) {
597                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
598                     $permissionQuery->select('id')->from('joint_permissions')
599                         ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
600                         ->where('entity_type', '=', 'Bookstack\\Page')
601                         ->where('action', '=', $this->currentAction)
602                         ->whereIn('role_id', $this->getRoles())
603                         ->where(function ($query) {
604                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
605                                 $query->where('has_permission_own', '=', true)
606                                     ->where('created_by', '=', $this->currentUser()->id);
607                             });
608                         });
609                 });
610             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
611         });
612         $this->clean();
613         return $q;
614     }
615
616     /**
617      * Check if the current user is an admin.
618      * @return bool
619      */
620     private function isAdmin()
621     {
622         if ($this->isAdminUser === null) {
623             $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
624         }
625
626         return $this->isAdminUser;
627     }
628
629     /**
630      * Get the current user
631      * @return User
632      */
633     private function currentUser()
634     {
635         if ($this->currentUserModel === false) {
636             $this->currentUserModel = user();
637         }
638
639         return $this->currentUserModel;
640     }
641
642     /**
643      * Clean the cached user elements.
644      */
645     private function clean()
646     {
647         $this->currentUserModel = false;
648         $this->userRoles = false;
649         $this->isAdminUser = null;
650     }
651
652 }