]> BookStack Code Mirror - bookstack/blobdiff - app/Services/PermissionService.php
Finished refactor of entity repos
[bookstack] / app / Services / PermissionService.php
index 17c1b12858a77ab9b98393afb61d7f307665e520..467bf95da69f7f6dbe35361a2a595669cb977bc0 100644 (file)
@@ -4,47 +4,102 @@ use BookStack\Book;
 use BookStack\Chapter;
 use BookStack\Entity;
 use BookStack\JointPermission;
+use BookStack\Ownable;
 use BookStack\Page;
 use BookStack\Role;
 use BookStack\User;
-use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Connection;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
 
 class PermissionService
 {
 
-    protected $userRoles;
-    protected $isAdmin;
     protected $currentAction;
-    protected $currentUser;
+    protected $isAdminUser;
+    protected $userRoles = false;
+    protected $currentUserModel = false;
 
     public $book;
     public $chapter;
     public $page;
 
+    protected $db;
+
     protected $jointPermission;
     protected $role;
 
+    protected $entityCache;
+
     /**
      * PermissionService constructor.
      * @param JointPermission $jointPermission
+     * @param Connection $db
      * @param Book $book
      * @param Chapter $chapter
      * @param Page $page
      * @param Role $role
      */
-    public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
+    public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
     {
-        $this->currentUser = auth()->user();
-        $userSet = $this->currentUser !== null;
-        $this->userRoles = false;
-        $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
-        if (!$userSet) $this->currentUser = new User();
-
+        $this->db = $db;
         $this->jointPermission = $jointPermission;
         $this->role = $role;
         $this->book = $book;
         $this->chapter = $chapter;
         $this->page = $page;
+        // TODO - Update so admin still goes through filters
+    }
+
+    /**
+     * Prepare the local entity cache and ensure it's empty
+     */
+    protected function readyEntityCache()
+    {
+        $this->entityCache = [
+            'books' => collect(),
+            'chapters' => collect()
+        ];
+    }
+
+    /**
+     * Get a book via ID, Checks local cache
+     * @param $bookId
+     * @return Book
+     */
+    protected function getBook($bookId)
+    {
+        if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
+            return $this->entityCache['books']->get($bookId);
+        }
+
+        $book = $this->book->find($bookId);
+        if ($book === null) $book = false;
+        if (isset($this->entityCache['books'])) {
+            $this->entityCache['books']->put($bookId, $book);
+        }
+
+        return $book;
+    }
+
+    /**
+     * Get a chapter via ID, Checks local cache
+     * @param $chapterId
+     * @return Book
+     */
+    protected function getChapter($chapterId)
+    {
+        if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
+            return $this->entityCache['chapters']->get($chapterId);
+        }
+
+        $chapter = $this->chapter->find($chapterId);
+        if ($chapter === null) $chapter = false;
+        if (isset($this->entityCache['chapters'])) {
+            $this->entityCache['chapters']->put($chapterId, $chapter);
+        }
+
+        return $chapter;
     }
 
     /**
@@ -63,7 +118,7 @@ class PermissionService
         }
 
 
-        foreach ($this->currentUser->roles as $role) {
+        foreach ($this->currentUser()->roles as $role) {
             $roles[] = $role->id;
         }
         return $roles;
@@ -75,6 +130,7 @@ class PermissionService
     public function buildJointPermissions()
     {
         $this->jointPermission->truncate();
+        $this->readyEntityCache();
 
         // Get all roles (Should be the most limited dimension)
         $roles = $this->role->with('permissions')->get();
@@ -96,7 +152,7 @@ class PermissionService
     }
 
     /**
-     * Create the entity jointPermissions for a particular entity.
+     * Rebuild the entity jointPermissions for a particular entity.
      * @param Entity $entity
      */
     public function buildJointPermissionsForEntity(Entity $entity)
@@ -115,6 +171,17 @@ class PermissionService
         $this->createManyJointPermissions($entities, $roles);
     }
 
+    /**
+     * Rebuild the entity jointPermissions for a collection of entities.
+     * @param Collection $entities
+     */
+    public function buildJointPermissionsForEntities(Collection $entities)
+    {
+        $roles = $this->role->with('jointPermissions')->get();
+        $this->deleteManyJointPermissionsForEntities($entities);
+        $this->createManyJointPermissions($entities, $roles);
+    }
+
     /**
      * Build the entity jointPermissions for a particular role.
      * @param Role $role
@@ -176,9 +243,14 @@ class PermissionService
      */
     protected function deleteManyJointPermissionsForEntities($entities)
     {
+        $query = $this->jointPermission->newQuery();
         foreach ($entities as $entity) {
-            $entity->jointPermissions()->delete();
+            $query->orWhere(function($query) use ($entity) {
+                $query->where('entity_id', '=', $entity->id)
+                    ->where('entity_type', '=', $entity->getMorphClass());
+            });
         }
+        $query->delete();
     }
 
     /**
@@ -188,6 +260,7 @@ class PermissionService
      */
     protected function createManyJointPermissions($entities, $roles)
     {
+        $this->readyEntityCache();
         $jointPermissions = [];
         foreach ($entities as $entity) {
             foreach ($roles as $role) {
@@ -235,6 +308,10 @@ class PermissionService
         $explodedAction = explode('-', $action);
         $restrictionAction = end($explodedAction);
 
+        if ($role->system_name === 'admin') {
+            return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
+        }
+
         if ($entity->isA('book')) {
 
             if (!$entity->restricted) {
@@ -247,8 +324,9 @@ class PermissionService
         } elseif ($entity->isA('chapter')) {
 
             if (!$entity->restricted) {
-                $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
-                $hasPermissiveAccessToBook = !$entity->book->restricted;
+                $book = $this->getBook($entity->book_id);
+                $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
+                $hasPermissiveAccessToBook = !$book->restricted;
                 return $this->createJointPermissionDataArray($entity, $role, $action,
                     ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
                     ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
@@ -260,11 +338,14 @@ class PermissionService
         } elseif ($entity->isA('page')) {
 
             if (!$entity->restricted) {
-                $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
-                $hasPermissiveAccessToBook = !$entity->book->restricted;
-                $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
-                $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
-                $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
+                $book = $this->getBook($entity->book_id);
+                $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
+                $hasPermissiveAccessToBook = !$book->restricted;
+
+                $chapter = $this->getChapter($entity->chapter_id);
+                $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
+                $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
+                $acknowledgeChapter = ($chapter && $chapter->restricted);
 
                 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
                 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
@@ -307,16 +388,20 @@ class PermissionService
 
     /**
      * Checks if an entity has a restriction set upon it.
-     * @param Entity $entity
+     * @param Ownable $ownable
      * @param $permission
      * @return bool
      */
-    public function checkEntityUserAccess(Entity $entity, $permission)
+    public function checkOwnableUserAccess(Ownable $ownable, $permission)
     {
-        if ($this->isAdmin) return true;
+        if ($this->isAdmin()) {
+            $this->clean();
+            return true;
+        }
+
         $explodedPermission = explode('-', $permission);
 
-        $baseQuery = $entity->where('id', '=', $entity->id);
+        $baseQuery = $ownable->where('id', '=', $ownable->id);
         $action = end($explodedPermission);
         $this->currentAction = $action;
 
@@ -324,10 +409,10 @@ class PermissionService
 
         // Handle non entity specific jointPermissions
         if (in_array($explodedPermission[0], $nonJointPermissions)) {
-            $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
-            $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
+            $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
+            $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
             $this->currentAction = 'view';
-            $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
+            $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
             return ($allPermission || ($isOwner && $ownPermission));
         }
 
@@ -337,7 +422,9 @@ class PermissionService
         }
 
 
-        return $this->entityRestrictionQuery($baseQuery)->count() > 0;
+        $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
+        $this->clean();
+        return $q;
     }
 
     /**
@@ -367,7 +454,7 @@ class PermissionService
      */
     protected function entityRestrictionQuery($query)
     {
-        return $query->where(function ($parentQuery) {
+        $q = $query->where(function ($parentQuery) {
             $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
                 $permissionQuery->whereIn('role_id', $this->getRoles())
                     ->where('action', '=', $this->currentAction)
@@ -375,65 +462,86 @@ class PermissionService
                         $query->where('has_permission', '=', true)
                             ->orWhere(function ($query) {
                                 $query->where('has_permission_own', '=', true)
-                                    ->where('created_by', '=', $this->currentUser->id);
+                                    ->where('created_by', '=', $this->currentUser()->id);
                             });
                     });
             });
         });
+        $this->clean();
+        return $q;
     }
 
-    /**
-     * Add restrictions for a page query
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforcePageRestrictions($query, $action = 'view')
-    {
-        // Prevent drafts being visible to others.
-        $query = $query->where(function ($query) {
-            $query->where('draft', '=', false);
-            if ($this->currentUser) {
-                $query->orWhere(function ($query) {
-                    $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
-                });
-            }
-        });
+    public function bookChildrenQuery($book_id, $filterDrafts = false) {
 
-        return $this->enforceEntityRestrictions($query, $action);
+        // Draft setup
+        $params = [
+            'userId' => $this->currentUser()->id,
+            'bookIdPage' => $book_id,
+            'bookIdChapter' => $book_id
+        ];
+        if (!$filterDrafts) {
+            $params['userIdDrafts'] = $this->currentUser()->id;
+        }
+        // Role setup
+        $userRoles = $this->getRoles();
+        $roleBindings = [];
+        $roleValues = [];
+        foreach ($userRoles as $index => $roleId) {
+            $roleBindings[':role'.$index] = $roleId;
+            $roleValues['role'.$index] = $roleId;
+        }
+        // TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
+        // Something which will handle the above role crap in a nice clean way
+        $roleBindingString = implode(',', array_keys($roleBindings));
+        $query = "SELECT * from (
+(SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
+    where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
+UNION
+(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)
+) as U  WHERE (
+       SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
+    WHERE
+               jp.entity_id=U.id AND
+        jp.entity_type=U.entity_type AND
+               jp.action = 'view' AND
+        jp.role_id IN ({$roleBindingString}) AND
+        (
+                       jp.has_permission = 1 OR
+            (jp.has_permission_own = 1 AND jp.created_by = :userId)
+        )
+) > 0
+ORDER BY draft desc, priority asc";
+
+        $this->clean();
+        return $this->db->select($query, array_replace($roleValues, $params));
     }
 
     /**
-     * Add on permission restrictions to a chapter query.
-     * @param $query
+     * Add restrictions for a generic entity
+     * @param string $entityType
+     * @param Builder|Entity $query
      * @param string $action
      * @return mixed
      */
-    public function enforceChapterRestrictions($query, $action = 'view')
+    public function enforceEntityRestrictions($entityType, $query, $action = 'view')
     {
-        return $this->enforceEntityRestrictions($query, $action);
-    }
+        if (strtolower($entityType) === 'page') {
+            // Prevent drafts being visible to others.
+            $query = $query->where(function ($query) {
+                $query->where('draft', '=', false);
+                if ($this->currentUser()) {
+                    $query->orWhere(function ($query) {
+                        $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+                    });
+                }
+            });
+        }
 
-    /**
-     * Add restrictions to a book query.
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforceBookRestrictions($query, $action = 'view')
-    {
-        $this->enforceEntityRestrictions($query, $action);
-    }
+        if ($this->isAdmin()) {
+            $this->clean();
+            return $query;
+        }
 
-    /**
-     * Add restrictions for a generic entity
-     * @param $query
-     * @param string $action
-     * @return mixed
-     */
-    public function enforceEntityRestrictions($query, $action = 'view')
-    {
-        if ($this->isAdmin) return $query;
         $this->currentAction = $action;
         return $this->entityRestrictionQuery($query);
     }
@@ -448,11 +556,15 @@ class PermissionService
      */
     public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
     {
-        if ($this->isAdmin) return $query;
+        if ($this->isAdmin()) {
+            $this->clean();
+            return $query;
+        }
+
         $this->currentAction = 'view';
         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
 
-        return $query->where(function ($query) use ($tableDetails) {
+        $q = $query->where(function ($query) use ($tableDetails) {
             $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
                 $permissionQuery->select('id')->from('joint_permissions')
                     ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
@@ -462,12 +574,12 @@ class PermissionService
                     ->where(function ($query) {
                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
                             $query->where('has_permission_own', '=', true)
-                                ->where('created_by', '=', $this->currentUser->id);
+                                ->where('created_by', '=', $this->currentUser()->id);
                         });
                     });
             });
         });
-
+        return $q;
     }
 
     /**
@@ -479,11 +591,15 @@ class PermissionService
      */
     public function filterRelatedPages($query, $tableName, $entityIdColumn)
     {
-        if ($this->isAdmin) return $query;
+        if ($this->isAdmin()) {
+            $this->clean();
+            return $query;
+        }
+
         $this->currentAction = 'view';
         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
 
-        return $query->where(function ($query) use ($tableDetails) {
+        $q = $query->where(function ($query) use ($tableDetails) {
             $query->where(function ($query) use (&$tableDetails) {
                 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
                     $permissionQuery->select('id')->from('joint_permissions')
@@ -494,12 +610,50 @@ class PermissionService
                         ->where(function ($query) {
                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
                                 $query->where('has_permission_own', '=', true)
-                                    ->where('created_by', '=', $this->currentUser->id);
+                                    ->where('created_by', '=', $this->currentUser()->id);
                             });
                         });
                 });
             })->orWhere($tableDetails['entityIdColumn'], '=', 0);
         });
+        $this->clean();
+        return $q;
+    }
+
+    /**
+     * Check if the current user is an admin.
+     * @return bool
+     */
+    private function isAdmin()
+    {
+        if ($this->isAdminUser === null) {
+            $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
+        }
+
+        return $this->isAdminUser;
+    }
+
+    /**
+     * Get the current user
+     * @return User
+     */
+    private function currentUser()
+    {
+        if ($this->currentUserModel === false) {
+            $this->currentUserModel = user();
+        }
+
+        return $this->currentUserModel;
+    }
+
+    /**
+     * Clean the cached user elements.
+     */
+    private function clean()
+    {
+        $this->currentUserModel = false;
+        $this->userRoles = false;
+        $this->isAdminUser = null;
     }
 
 }
\ No newline at end of file