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