]> BookStack Code Mirror - bookstack/blob - app/Services/PermissionService.php
Fixes typo causing the message not to be displayed
[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->with('jointPermissions')->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->with('jointPermissions')->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         $query = $this->jointPermission->newQuery();
247         foreach ($entities as $entity) {
248             $query->orWhere(function($query) use ($entity) {
249                 $query->where('entity_id', '=', $entity->id)
250                     ->where('entity_type', '=', $entity->getMorphClass());
251             });
252         }
253         $query->delete();
254     }
255
256     /**
257      * Create & Save entity jointPermissions for many entities and jointPermissions.
258      * @param Collection $entities
259      * @param Collection $roles
260      */
261     protected function createManyJointPermissions($entities, $roles)
262     {
263         $this->readyEntityCache();
264         $jointPermissions = [];
265         foreach ($entities as $entity) {
266             foreach ($roles as $role) {
267                 foreach ($this->getActions($entity) as $action) {
268                     $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
269                 }
270             }
271         }
272         $this->jointPermission->insert($jointPermissions);
273     }
274
275
276     /**
277      * Get the actions related to an entity.
278      * @param $entity
279      * @return array
280      */
281     protected function getActions($entity)
282     {
283         $baseActions = ['view', 'update', 'delete'];
284
285         if ($entity->isA('chapter')) {
286             $baseActions[] = 'page-create';
287         } else if ($entity->isA('book')) {
288             $baseActions[] = 'page-create';
289             $baseActions[] = 'chapter-create';
290         }
291
292          return $baseActions;
293     }
294
295     /**
296      * Create entity permission data for an entity and role
297      * for a particular action.
298      * @param Entity $entity
299      * @param Role $role
300      * @param $action
301      * @return array
302      */
303     protected function createJointPermissionData(Entity $entity, Role $role, $action)
304     {
305         $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
306         $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
307         $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
308         $explodedAction = explode('-', $action);
309         $restrictionAction = end($explodedAction);
310
311         if ($role->system_name === 'admin') {
312             return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
313         }
314
315         if ($entity->isA('book')) {
316
317             if (!$entity->restricted) {
318                 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
319             } else {
320                 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
321                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
322             }
323
324         } elseif ($entity->isA('chapter')) {
325
326             if (!$entity->restricted) {
327                 $book = $this->getBook($entity->book_id);
328                 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
329                 $hasPermissiveAccessToBook = !$book->restricted;
330                 return $this->createJointPermissionDataArray($entity, $role, $action,
331                     ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
332                     ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
333             } else {
334                 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
335                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
336             }
337
338         } elseif ($entity->isA('page')) {
339
340             if (!$entity->restricted) {
341                 $book = $this->getBook($entity->book_id);
342                 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
343                 $hasPermissiveAccessToBook = !$book->restricted;
344
345                 $chapter = $this->getChapter($entity->chapter_id);
346                 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
347                 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
348                 $acknowledgeChapter = ($chapter && $chapter->restricted);
349
350                 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
351                 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
352
353                 return $this->createJointPermissionDataArray($entity, $role, $action,
354                     ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
355                     ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
356                 );
357             } else {
358                 $hasAccess = $entity->hasRestriction($role->id, $action);
359                 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
360             }
361
362         }
363     }
364
365     /**
366      * Create an array of data with the information of an entity jointPermissions.
367      * Used to build data for bulk insertion.
368      * @param Entity $entity
369      * @param Role $role
370      * @param $action
371      * @param $permissionAll
372      * @param $permissionOwn
373      * @return array
374      */
375     protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
376     {
377         $entityClass = get_class($entity);
378         return [
379             'role_id'            => $role->getRawAttribute('id'),
380             'entity_id'          => $entity->getRawAttribute('id'),
381             'entity_type'        => $entityClass,
382             'action'             => $action,
383             'has_permission'     => $permissionAll,
384             'has_permission_own' => $permissionOwn,
385             'created_by'         => $entity->getRawAttribute('created_by')
386         ];
387     }
388
389     /**
390      * Checks if an entity has a restriction set upon it.
391      * @param Ownable $ownable
392      * @param $permission
393      * @return bool
394      */
395     public function checkOwnableUserAccess(Ownable $ownable, $permission)
396     {
397         if ($this->isAdmin()) {
398             $this->clean();
399             return true;
400         }
401
402         $explodedPermission = explode('-', $permission);
403
404         $baseQuery = $ownable->where('id', '=', $ownable->id);
405         $action = end($explodedPermission);
406         $this->currentAction = $action;
407
408         $nonJointPermissions = ['restrictions'];
409
410         // Handle non entity specific jointPermissions
411         if (in_array($explodedPermission[0], $nonJointPermissions)) {
412             $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
413             $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
414             $this->currentAction = 'view';
415             $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
416             return ($allPermission || ($isOwner && $ownPermission));
417         }
418
419         // Handle abnormal create jointPermissions
420         if ($action === 'create') {
421             $this->currentAction = $permission;
422         }
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     public function bookChildrenQuery($book_id, $filterDrafts = false) {
475
476         // Draft setup
477         $params = [
478             'userId' => $this->currentUser()->id,
479             'bookIdPage' => $book_id,
480             'bookIdChapter' => $book_id
481         ];
482         if (!$filterDrafts) {
483             $params['userIdDrafts'] = $this->currentUser()->id;
484         }
485         // Role setup
486         $userRoles = $this->getRoles();
487         $roleBindings = [];
488         $roleValues = [];
489         foreach ($userRoles as $index => $roleId) {
490             $roleBindings[':role'.$index] = $roleId;
491             $roleValues['role'.$index] = $roleId;
492         }
493         // TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
494         // Something which will handle the above role crap in a nice clean way
495         $roleBindingString = implode(',', array_keys($roleBindings));
496         $query = "SELECT * from (
497 (SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
498     where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
499 UNION
500 (SELECT 'Bookstack\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft FROM {$this->chapter->getTable()} WHERE book_id = :bookIdChapter)
501 ) as U  WHERE (
502         SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
503     WHERE
504                 jp.entity_id=U.id AND
505         jp.entity_type=U.entity_type AND
506                 jp.action = 'view' AND
507         jp.role_id IN ({$roleBindingString}) AND
508         (
509                         jp.has_permission = 1 OR
510             (jp.has_permission_own = 1 AND jp.created_by = :userId)
511         )
512 ) > 0
513 ORDER BY draft desc, priority asc";
514
515         $this->clean();
516         return $this->db->select($query, array_replace($roleValues, $params));
517     }
518
519     /**
520      * Add restrictions for a generic entity
521      * @param string $entityType
522      * @param Builder|Entity $query
523      * @param string $action
524      * @return mixed
525      */
526     public function enforceEntityRestrictions($entityType, $query, $action = 'view')
527     {
528         if (strtolower($entityType) === 'page') {
529             // Prevent drafts being visible to others.
530             $query = $query->where(function ($query) {
531                 $query->where('draft', '=', false);
532                 if ($this->currentUser()) {
533                     $query->orWhere(function ($query) {
534                         $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
535                     });
536                 }
537             });
538         }
539
540         if ($this->isAdmin()) {
541             $this->clean();
542             return $query;
543         }
544
545         $this->currentAction = $action;
546         return $this->entityRestrictionQuery($query);
547     }
548
549     /**
550      * Filter items that have entities set a a polymorphic relation.
551      * @param $query
552      * @param string $tableName
553      * @param string $entityIdColumn
554      * @param string $entityTypeColumn
555      * @return mixed
556      */
557     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
558     {
559         if ($this->isAdmin()) {
560             $this->clean();
561             return $query;
562         }
563
564         $this->currentAction = 'view';
565         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
566
567         $q = $query->where(function ($query) use ($tableDetails) {
568             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
569                 $permissionQuery->select('id')->from('joint_permissions')
570                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
571                     ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
572                     ->where('action', '=', $this->currentAction)
573                     ->whereIn('role_id', $this->getRoles())
574                     ->where(function ($query) {
575                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
576                             $query->where('has_permission_own', '=', true)
577                                 ->where('created_by', '=', $this->currentUser()->id);
578                         });
579                     });
580             });
581         });
582         return $q;
583     }
584
585     /**
586      * Filters pages that are a direct relation to another item.
587      * @param $query
588      * @param $tableName
589      * @param $entityIdColumn
590      * @return mixed
591      */
592     public function filterRelatedPages($query, $tableName, $entityIdColumn)
593     {
594         if ($this->isAdmin()) {
595             $this->clean();
596             return $query;
597         }
598
599         $this->currentAction = 'view';
600         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
601
602         $q = $query->where(function ($query) use ($tableDetails) {
603             $query->where(function ($query) use (&$tableDetails) {
604                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
605                     $permissionQuery->select('id')->from('joint_permissions')
606                         ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
607                         ->where('entity_type', '=', 'Bookstack\\Page')
608                         ->where('action', '=', $this->currentAction)
609                         ->whereIn('role_id', $this->getRoles())
610                         ->where(function ($query) {
611                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
612                                 $query->where('has_permission_own', '=', true)
613                                     ->where('created_by', '=', $this->currentUser()->id);
614                             });
615                         });
616                 });
617             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
618         });
619         $this->clean();
620         return $q;
621     }
622
623     /**
624      * Check if the current user is an admin.
625      * @return bool
626      */
627     private function isAdmin()
628     {
629         if ($this->isAdminUser === null) {
630             $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
631         }
632
633         return $this->isAdminUser;
634     }
635
636     /**
637      * Get the current user
638      * @return User
639      */
640     private function currentUser()
641     {
642         if ($this->currentUserModel === false) {
643             $this->currentUserModel = user();
644         }
645
646         return $this->currentUserModel;
647     }
648
649     /**
650      * Clean the cached user elements.
651      */
652     private function clean()
653     {
654         $this->currentUserModel = false;
655         $this->userRoles = false;
656         $this->isAdminUser = null;
657     }
658
659 }