]> BookStack Code Mirror - bookstack/blob - app/Services/PermissionService.php
Updated issue template and added TinyMCE autolinking
[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', 'image', 'attachment'];
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         $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
425         $this->clean();
426         return $q;
427     }
428
429     /**
430      * Check if an entity has restrictions set on itself or its
431      * parent tree.
432      * @param Entity $entity
433      * @param $action
434      * @return bool|mixed
435      */
436     public function checkIfRestrictionsSet(Entity $entity, $action)
437     {
438         $this->currentAction = $action;
439         if ($entity->isA('page')) {
440             return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
441         } elseif ($entity->isA('chapter')) {
442             return $entity->restricted || $entity->book->restricted;
443         } elseif ($entity->isA('book')) {
444             return $entity->restricted;
445         }
446     }
447
448     /**
449      * The general query filter to remove all entities
450      * that the current user does not have access to.
451      * @param $query
452      * @return mixed
453      */
454     protected function entityRestrictionQuery($query)
455     {
456         $q = $query->where(function ($parentQuery) {
457             $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
458                 $permissionQuery->whereIn('role_id', $this->getRoles())
459                     ->where('action', '=', $this->currentAction)
460                     ->where(function ($query) {
461                         $query->where('has_permission', '=', true)
462                             ->orWhere(function ($query) {
463                                 $query->where('has_permission_own', '=', true)
464                                     ->where('created_by', '=', $this->currentUser()->id);
465                             });
466                     });
467             });
468         });
469         $this->clean();
470         return $q;
471     }
472
473     /**
474      * Get the children of a book in an efficient single query, Filtered by the permission system.
475      * @param integer $book_id
476      * @param bool    $filterDrafts
477      * @return \Illuminate\Database\Query\Builder
478      */
479     public function bookChildrenQuery($book_id, $filterDrafts = false) {
480         $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
481             $query->where('draft', '=', 0);
482             if (!$filterDrafts) {
483                 $query->orWhere(function($query) {
484                     $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
485                 });
486             }
487         });
488         $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);
489         $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
490             ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
491             ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
492             ->where(function($query) {
493                 $query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
494                     $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
495                 });
496             });
497         $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
498             ->mergeBindings($pageSelect)->mergeBindings($chapterSelect)
499             ->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery)->orderBy('draft', 'desc')->orderBy('priority', 'asc');
500         $this->clean();
501         return  $query;
502     }
503
504     /**
505      * Add restrictions for a generic entity
506      * @param string $entityType
507      * @param Builder|Entity $query
508      * @param string $action
509      * @return mixed
510      */
511     public function enforceEntityRestrictions($entityType, $query, $action = 'view')
512     {
513         if (strtolower($entityType) === 'page') {
514             // Prevent drafts being visible to others.
515             $query = $query->where(function ($query) {
516                 $query->where('draft', '=', false);
517                 if ($this->currentUser()) {
518                     $query->orWhere(function ($query) {
519                         $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
520                     });
521                 }
522             });
523         }
524
525         if ($this->isAdmin()) {
526             $this->clean();
527             return $query;
528         }
529
530         $this->currentAction = $action;
531         return $this->entityRestrictionQuery($query);
532     }
533
534     /**
535      * Filter items that have entities set a a polymorphic relation.
536      * @param $query
537      * @param string $tableName
538      * @param string $entityIdColumn
539      * @param string $entityTypeColumn
540      * @return mixed
541      */
542     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
543     {
544         if ($this->isAdmin()) {
545             $this->clean();
546             return $query;
547         }
548
549         $this->currentAction = 'view';
550         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
551
552         $q = $query->where(function ($query) use ($tableDetails) {
553             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
554                 $permissionQuery->select('id')->from('joint_permissions')
555                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
556                     ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
557                     ->where('action', '=', $this->currentAction)
558                     ->whereIn('role_id', $this->getRoles())
559                     ->where(function ($query) {
560                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
561                             $query->where('has_permission_own', '=', true)
562                                 ->where('created_by', '=', $this->currentUser()->id);
563                         });
564                     });
565             });
566         });
567         return $q;
568     }
569
570     /**
571      * Filters pages that are a direct relation to another item.
572      * @param $query
573      * @param $tableName
574      * @param $entityIdColumn
575      * @return mixed
576      */
577     public function filterRelatedPages($query, $tableName, $entityIdColumn)
578     {
579         if ($this->isAdmin()) {
580             $this->clean();
581             return $query;
582         }
583
584         $this->currentAction = 'view';
585         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
586
587         $q = $query->where(function ($query) use ($tableDetails) {
588             $query->where(function ($query) use (&$tableDetails) {
589                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
590                     $permissionQuery->select('id')->from('joint_permissions')
591                         ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
592                         ->where('entity_type', '=', 'Bookstack\\Page')
593                         ->where('action', '=', $this->currentAction)
594                         ->whereIn('role_id', $this->getRoles())
595                         ->where(function ($query) {
596                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
597                                 $query->where('has_permission_own', '=', true)
598                                     ->where('created_by', '=', $this->currentUser()->id);
599                             });
600                         });
601                 });
602             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
603         });
604         $this->clean();
605         return $q;
606     }
607
608     /**
609      * Check if the current user is an admin.
610      * @return bool
611      */
612     private function isAdmin()
613     {
614         if ($this->isAdminUser === null) {
615             $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
616         }
617
618         return $this->isAdminUser;
619     }
620
621     /**
622      * Get the current user
623      * @return User
624      */
625     private function currentUser()
626     {
627         if ($this->currentUserModel === false) {
628             $this->currentUserModel = user();
629         }
630
631         return $this->currentUserModel;
632     }
633
634     /**
635      * Clean the cached user elements.
636      */
637     private function clean()
638     {
639         $this->currentUserModel = false;
640         $this->userRoles = false;
641         $this->isAdminUser = null;
642     }
643
644 }