]> BookStack Code Mirror - bookstack/blob - app/Services/PermissionService.php
Merge branch 'patch-2' of git://github.com/fredericmohr/BookStack into fredericmohr...
[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     public function bookChildrenQuery($book_id, $filterDrafts = false) {
474
475         // Draft setup
476         $params = [
477             'userId' => $this->currentUser()->id,
478             'bookIdPage' => $book_id,
479             'bookIdChapter' => $book_id
480         ];
481         if (!$filterDrafts) {
482             $params['userIdDrafts'] = $this->currentUser()->id;
483         }
484         // Role setup
485         $userRoles = $this->getRoles();
486         $roleBindings = [];
487         $roleValues = [];
488         foreach ($userRoles as $index => $roleId) {
489             $roleBindings[':role'.$index] = $roleId;
490             $roleValues['role'.$index] = $roleId;
491         }
492         // TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
493         // Something which will handle the above role crap in a nice clean way
494         $roleBindingString = implode(',', array_keys($roleBindings));
495         $query = "SELECT * from (
496 (SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
497     where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
498 UNION
499 (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)
500 ) as U  WHERE (
501         SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
502     WHERE
503                 jp.entity_id=U.id AND
504         jp.entity_type=U.entity_type AND
505                 jp.action = 'view' AND
506         jp.role_id IN ({$roleBindingString}) AND
507         (
508                         jp.has_permission = 1 OR
509             (jp.has_permission_own = 1 AND jp.created_by = :userId)
510         )
511 ) > 0
512 ORDER BY draft desc, priority asc";
513
514         $this->clean();
515         return $this->db->select($query, array_replace($roleValues, $params));
516     }
517
518     /**
519      * Add restrictions for a generic entity
520      * @param string $entityType
521      * @param Builder|Entity $query
522      * @param string $action
523      * @return mixed
524      */
525     public function enforceEntityRestrictions($entityType, $query, $action = 'view')
526     {
527         if (strtolower($entityType) === 'page') {
528             // Prevent drafts being visible to others.
529             $query = $query->where(function ($query) {
530                 $query->where('draft', '=', false);
531                 if ($this->currentUser()) {
532                     $query->orWhere(function ($query) {
533                         $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
534                     });
535                 }
536             });
537         }
538
539         if ($this->isAdmin()) {
540             $this->clean();
541             return $query;
542         }
543
544         $this->currentAction = $action;
545         return $this->entityRestrictionQuery($query);
546     }
547
548     /**
549      * Filter items that have entities set a a polymorphic relation.
550      * @param $query
551      * @param string $tableName
552      * @param string $entityIdColumn
553      * @param string $entityTypeColumn
554      * @return mixed
555      */
556     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
557     {
558         if ($this->isAdmin()) {
559             $this->clean();
560             return $query;
561         }
562
563         $this->currentAction = 'view';
564         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
565
566         $q = $query->where(function ($query) use ($tableDetails) {
567             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
568                 $permissionQuery->select('id')->from('joint_permissions')
569                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
570                     ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
571                     ->where('action', '=', $this->currentAction)
572                     ->whereIn('role_id', $this->getRoles())
573                     ->where(function ($query) {
574                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
575                             $query->where('has_permission_own', '=', true)
576                                 ->where('created_by', '=', $this->currentUser()->id);
577                         });
578                     });
579             });
580         });
581         return $q;
582     }
583
584     /**
585      * Filters pages that are a direct relation to another item.
586      * @param $query
587      * @param $tableName
588      * @param $entityIdColumn
589      * @return mixed
590      */
591     public function filterRelatedPages($query, $tableName, $entityIdColumn)
592     {
593         if ($this->isAdmin()) {
594             $this->clean();
595             return $query;
596         }
597
598         $this->currentAction = 'view';
599         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
600
601         $q = $query->where(function ($query) use ($tableDetails) {
602             $query->where(function ($query) use (&$tableDetails) {
603                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
604                     $permissionQuery->select('id')->from('joint_permissions')
605                         ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
606                         ->where('entity_type', '=', 'Bookstack\\Page')
607                         ->where('action', '=', $this->currentAction)
608                         ->whereIn('role_id', $this->getRoles())
609                         ->where(function ($query) {
610                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
611                                 $query->where('has_permission_own', '=', true)
612                                     ->where('created_by', '=', $this->currentUser()->id);
613                             });
614                         });
615                 });
616             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
617         });
618         $this->clean();
619         return $q;
620     }
621
622     /**
623      * Check if the current user is an admin.
624      * @return bool
625      */
626     private function isAdmin()
627     {
628         if ($this->isAdminUser === null) {
629             $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
630         }
631
632         return $this->isAdminUser;
633     }
634
635     /**
636      * Get the current user
637      * @return User
638      */
639     private function currentUser()
640     {
641         if ($this->currentUserModel === false) {
642             $this->currentUserModel = user();
643         }
644
645         return $this->currentUserModel;
646     }
647
648     /**
649      * Clean the cached user elements.
650      */
651     private function clean()
652     {
653         $this->currentUserModel = false;
654         $this->userRoles = false;
655         $this->isAdminUser = null;
656     }
657
658 }