]> BookStack Code Mirror - bookstack/blobdiff - app/Auth/Permissions/PermissionService.php
Updated minimum php version from 7.3 to 7.4
[bookstack] / app / Auth / Permissions / PermissionService.php
index 57b4a6bd64d8888c01543ad89f095d588008d37a..59ff37dc9bcb7ebb9c3b856cc40868e5ccbcae9c 100644 (file)
@@ -1,4 +1,6 @@
-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
 
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
@@ -48,7 +50,7 @@ class PermissionService
     }
 
     /**
-     * Set the database connection
+     * Set the database connection.
      */
     public function setConnection(Connection $connection)
     {
@@ -56,7 +58,8 @@ class PermissionService
     }
 
     /**
-     * Prepare the local entity cache and ensure it's empty
+     * Prepare the local entity cache and ensure it's empty.
+     *
      * @param Entity[] $entities
      */
     protected function readyEntityCache(array $entities = [])
@@ -73,7 +76,7 @@ class PermissionService
     }
 
     /**
-     * Get a book via ID, Checks local cache
+     * Get a book via ID, Checks local cache.
      */
     protected function getBook(int $bookId): ?Book
     {
@@ -85,7 +88,7 @@ class PermissionService
     }
 
     /**
-     * Get a chapter via ID, Checks local cache
+     * Get a chapter via ID, Checks local cache.
      */
     protected function getChapter(int $chapterId): ?Chapter
     {
@@ -151,12 +154,13 @@ class PermissionService
                 },
                 'pages' => function ($query) {
                     $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
-                }
+                },
             ]);
     }
 
     /**
      * Build joint permissions for the given shelf and role combinations.
+     *
      * @throws Throwable
      */
     protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
@@ -169,6 +173,7 @@ class PermissionService
 
     /**
      * Build joint permissions for the given book and role combinations.
+     *
      * @throws Throwable
      */
     protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
@@ -193,6 +198,7 @@ class PermissionService
 
     /**
      * Rebuild the entity jointPermissions for a particular entity.
+     *
      * @throws Throwable
      */
     public function buildJointPermissionsForEntity(Entity $entity)
@@ -201,6 +207,7 @@ class PermissionService
         if ($entity instanceof Book) {
             $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
             $this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
+
             return;
         }
 
@@ -224,6 +231,7 @@ class PermissionService
 
     /**
      * Rebuild the entity jointPermissions for a collection of entities.
+     *
      * @throws Throwable
      */
     public function buildJointPermissionsForEntities(array $entities)
@@ -263,6 +271,7 @@ class PermissionService
 
     /**
      * Delete all of the entity jointPermissions for a list of entities.
+     *
      * @param Role[] $roles
      */
     protected function deleteManyJointPermissionsForRoles($roles)
@@ -275,7 +284,9 @@ class PermissionService
 
     /**
      * Delete the entity jointPermissions for a particular entity.
+     *
      * @param Entity $entity
+     *
      * @throws Throwable
      */
     public function deleteJointPermissionsForEntity(Entity $entity)
@@ -285,7 +296,9 @@ class PermissionService
 
     /**
      * Delete all of the entity jointPermissions for a list of entities.
+     *
      * @param Entity[] $entities
+     *
      * @throws Throwable
      */
     protected function deleteManyJointPermissionsForEntities(array $entities)
@@ -295,7 +308,6 @@ class PermissionService
         }
 
         $this->db->transaction(function () use ($entities) {
-
             foreach (array_chunk($entities, 1000) as $entityChunk) {
                 $query = $this->db->table('joint_permissions');
                 foreach ($entityChunk as $entity) {
@@ -311,8 +323,10 @@ class PermissionService
 
     /**
      * Create & Save entity jointPermissions for many entities and roles.
+     *
      * @param Entity[] $entities
-     * @param Role[] $roles
+     * @param Role[]   $roles
+     *
      * @throws Throwable
      */
     protected function createManyJointPermissions(array $entities, array $roles)
@@ -363,7 +377,6 @@ class PermissionService
         });
     }
 
-
     /**
      * Get the actions related to an entity.
      */
@@ -376,6 +389,7 @@ class PermissionService
         if ($entity instanceof Book) {
             $baseActions[] = 'chapter-create';
         }
+
         return $baseActions;
     }
 
@@ -397,6 +411,7 @@ class PermissionService
 
         if ($entity->restricted) {
             $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
+
             return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
         }
 
@@ -433,6 +448,7 @@ class PermissionService
     protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
     {
         $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
+
         return $entityMap[$key] ?? false;
     }
 
@@ -443,18 +459,19 @@ class PermissionService
     protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
     {
         return [
-            'role_id' => $role->getRawAttribute('id'),
-            'entity_id' => $entity->getRawAttribute('id'),
-            'entity_type' => $entity->getMorphClass(),
-            'action' => $action,
-            'has_permission' => $permissionAll,
+            'role_id'            => $role->getRawAttribute('id'),
+            'entity_id'          => $entity->getRawAttribute('id'),
+            'entity_type'        => $entity->getMorphClass(),
+            'action'             => $action,
+            'has_permission'     => $permissionAll,
             'has_permission_own' => $permissionOwn,
-            'owned_by' => $entity->getRawAttribute('owned_by'),
+            'owned_by'           => $entity->getRawAttribute('owned_by'),
         ];
     }
 
     /**
      * Checks if an entity has a restriction set upon it.
+     *
      * @param HasCreatorAndUpdater|HasOwner $ownable
      */
     public function checkOwnableUserAccess(Model $ownable, string $permission): bool
@@ -473,7 +490,8 @@ class PermissionService
             $ownPermission = $user && $user->can($permission . '-own');
             $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
             $isOwner = $user && $user->id === $ownable->$ownerField;
-            return ($allPermission || ($isOwner && $ownPermission));
+
+            return $allPermission || ($isOwner && $ownPermission);
         }
 
         // Handle abnormal create jointPermissions
@@ -483,6 +501,7 @@ class PermissionService
 
         $hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
         $this->clean();
+
         return $hasAccess;
     }
 
@@ -495,24 +514,21 @@ class PermissionService
         $userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
         $userId = $this->currentUser()->id;
 
-        $permissionQuery = $this->db->table('joint_permissions')
+        $permissionQuery = JointPermission::query()
             ->where('action', '=', $permission)
             ->whereIn('role_id', $userRoleIds)
-            ->where(function ($query) use ($userId) {
-                $query->where('has_permission', '=', 1)
-                    ->orWhere(function ($query2) use ($userId) {
-                        $query2->where('has_permission_own', '=', 1)
-                            ->where('owned_by', '=', $userId);
-                    });
+            ->where(function (Builder $query) use ($userId) {
+                $this->addJointHasPermissionCheck($query, $userId);
             });
 
         if (!is_null($entityClass)) {
-            $entityInstance = app()->make($entityClass);
+            $entityInstance = app($entityClass);
             $permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
         }
 
         $hasPermission = $permissionQuery->count() > 0;
         $this->clean();
+
         return $hasPermission;
     }
 
@@ -526,17 +542,14 @@ class PermissionService
             $parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
                 $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
                     ->where('action', '=', $action)
-                    ->where(function ($query) {
-                        $query->where('has_permission', '=', true)
-                            ->orWhere(function ($query) {
-                                $query->where('has_permission_own', '=', true)
-                                    ->where('owned_by', '=', $this->currentUser()->id);
-                            });
+                    ->where(function (Builder $query) {
+                        $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
                     });
             });
         });
 
         $this->clean();
+
         return $q;
     }
 
@@ -547,16 +560,13 @@ class PermissionService
     public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
     {
         $this->clean();
+
         return $query->where(function (Builder $parentQuery) use ($ability) {
             $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
                 $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
                     ->where('action', '=', $ability)
                     ->where(function (Builder $query) {
-                        $query->where('has_permission', '=', true)
-                            ->orWhere(function (Builder $query) {
-                                $query->where('has_permission_own', '=', true)
-                                    ->where('owned_by', '=', $this->currentUser()->id);
-                            });
+                        $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
                     });
             });
         });
@@ -580,17 +590,11 @@ class PermissionService
     /**
      * Add restrictions for a generic entity.
      */
-    public function enforceEntityRestrictions(string $entityType, Builder $query, string $action = 'view'): Builder
+    public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
     {
-        if (strtolower($entityType) === 'page') {
+        if ($entity instanceof Page) {
             // Prevent drafts being visible to others.
-            $query->where(function ($query) {
-                $query->where('draft', '=', false)
-                    ->orWhere(function ($query) {
-                        $query->where('draft', '=', true)
-                            ->where('owned_by', '=', $this->currentUser()->id);
-                    });
-            });
+            $this->enforceDraftVisibilityOnQuery($query);
         }
 
         return $this->entityRestrictionQuery($query, $action);
@@ -598,28 +602,39 @@ class PermissionService
 
     /**
      * Filter items that have entities set as a polymorphic relation.
+     * For simplicity, this will not return results attached to draft pages.
+     * Draft pages should never really have related items though.
+     *
+     * @param Builder|QueryBuilder $query
      */
-    public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
+    public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
     {
         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
-
-        $q = $query->where(function ($query) use ($tableDetails, $action) {
-            $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
-                $permissionQuery->select('id')->from('joint_permissions')
-                    ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
-                    ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
-                    ->where('action', '=', $action)
-                    ->whereIn('role_id', $this->getCurrentUserRoles())
-                    ->where(function ($query) {
-                        $query->where('has_permission', '=', true)->orWhere(function ($query) {
-                            $query->where('has_permission_own', '=', true)
-                                ->where('owned_by', '=', $this->currentUser()->id);
-                        });
-                    });
-            });
+        $pageMorphClass = (new Page())->getMorphClass();
+
+        $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
+            /** @var Builder $permissionQuery */
+            $permissionQuery->select(['role_id'])->from('joint_permissions')
+                ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+                ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
+                ->where('joint_permissions.action', '=', $action)
+                ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
+                ->where(function (QueryBuilder $query) {
+                    $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
+                });
+        })->where(function ($query) use ($tableDetails, $pageMorphClass) {
+            /** @var Builder $query */
+            $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
+                ->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
+                    $query->select('id')->from('pages')
+                        ->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+                        ->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
+                        ->where('pages.draft', '=', false);
+                });
         });
 
         $this->clean();
+
         return $q;
     }
 
@@ -629,33 +644,60 @@ class PermissionService
      */
     public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
     {
-        $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
-        $morphClass = app($entityClass)->getMorphClass();
-
-        $q = $query->where(function ($query) use ($tableDetails, $morphClass) {
-            $query->where(function ($query) use (&$tableDetails, $morphClass) {
-                $query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
-                    $permissionQuery->select('id')->from('joint_permissions')
-                        ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
-                        ->where('entity_type', '=', $morphClass)
-                        ->where('action', '=', 'view')
-                        ->whereIn('role_id', $this->getCurrentUserRoles())
-                        ->where(function ($query) {
-                            $query->where('has_permission', '=', true)->orWhere(function ($query) {
-                                $query->where('has_permission_own', '=', true)
-                                    ->where('owned_by', '=', $this->currentUser()->id);
-                            });
-                        });
+        $fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
+        $instance = new $entityClass();
+        $morphClass = $instance->getMorphClass();
+
+        $existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
+            /** @var Builder $permissionQuery */
+            $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
+                ->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
+                ->where('joint_permissions.entity_type', '=', $morphClass)
+                ->where('joint_permissions.action', '=', 'view')
+                ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
+                ->where(function (QueryBuilder $query) {
+                    $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
                 });
-            })->orWhere($tableDetails['entityIdColumn'], '=', 0);
+        };
+
+        $q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
+            $query->whereExists($existsQuery)
+                ->orWhere($fullEntityIdColumn, '=', 0);
         });
 
+        if ($instance instanceof Page) {
+            // Prevent visibility of non-owned draft pages
+            $q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
+                $query->select('id')->from('pages')
+                    ->whereColumn('pages.id', '=', $fullEntityIdColumn)
+                    ->where(function (QueryBuilder $query) {
+                        $query->where('pages.draft', '=', false)
+                            ->orWhere('pages.owned_by', '=', $this->currentUser()->id);
+                    });
+            });
+        }
+
         $this->clean();
+
         return $q;
     }
 
     /**
-     * Get the current user
+     * Add the query for checking the given user id has permission
+     * within the join_permissions table.
+     *
+     * @param QueryBuilder|Builder $query
+     */
+    protected function addJointHasPermissionCheck($query, int $userIdToCheck)
+    {
+        $query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
+            $query->where('joint_permissions.has_permission_own', '=', true)
+                ->where('joint_permissions.owned_by', '=', $userIdToCheck);
+        });
+    }
+
+    /**
+     * Get the current user.
      */
     private function currentUser(): User
     {