]> BookStack Code Mirror - bookstack/blob - app/Auth/Permissions/PermissionApplicator.php
e73db5157bac083efdfcd3bc1d14fa449ec4952c
[bookstack] / app / Auth / Permissions / PermissionApplicator.php
1 <?php
2
3 namespace BookStack\Auth\Permissions;
4
5 use BookStack\Auth\Role;
6 use BookStack\Auth\User;
7 use BookStack\Entities\Models\Chapter;
8 use BookStack\Entities\Models\Entity;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Model;
11 use BookStack\Traits\HasCreatorAndUpdater;
12 use BookStack\Traits\HasOwner;
13 use Illuminate\Database\Eloquent\Builder;
14 use Illuminate\Database\Query\Builder as QueryBuilder;
15 use InvalidArgumentException;
16
17 class PermissionApplicator
18 {
19     /**
20      * Checks if an entity has a restriction set upon it.
21      *
22      * @param HasCreatorAndUpdater|HasOwner $ownable
23      */
24     public function checkOwnableUserAccess(Model $ownable, string $permission): bool
25     {
26         $explodedPermission = explode('-', $permission);
27         $action = $explodedPermission[1] ?? $explodedPermission[0];
28         $user = $this->currentUser();
29         $userRoleIds = $this->getCurrentUserRoleIds();
30
31         $allRolePermission = $user->can($permission . '-all');
32         $ownRolePermission = $user->can($permission . '-own');
33         $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
34         $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
35         $isOwner = $user->id === $ownable->getAttribute($ownerField);
36         $hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission);
37
38         // Handle non entity specific jointPermissions
39         if (in_array($explodedPermission[0], $nonJointPermissions)) {
40             return $hasRolePermission;
41         }
42
43         $entityPermissions = $this->getApplicableEntityPermissions($ownable, $userRoleIds, $action);
44         if (is_null($entityPermissions)) {
45             return $hasRolePermission;
46         }
47
48         return count($entityPermissions) > 0;
49     }
50
51     /**
52      * Get the permissions that are applicable for the given entity item.
53      * Returns null when no entity permissions apply otherwise entity permissions
54      * are active, even if the returned array is empty.
55      *
56      * @returns EntityPermission[]
57      */
58     protected function getApplicableEntityPermissions(Entity $entity, array $userRoleIds, string $action): ?array
59     {
60         $chain = [$entity];
61         if ($entity instanceof Page && $entity->chapter_id) {
62             $chain[] = $entity->chapter;
63         }
64
65         if ($entity instanceof Page || $entity instanceof Chapter) {
66             $chain[] = $entity->book;
67         }
68
69         foreach ($chain as $currentEntity) {
70             if ($currentEntity->restricted) {
71                 return $currentEntity->permissions()
72                     ->whereIn('role_id', $userRoleIds)
73                     ->where('action', '=', $action)
74                     ->get()
75                     ->all();
76             }
77         }
78
79         return null;
80     }
81
82     /**
83      * Checks if a user has the given permission for any items in the system.
84      * Can be passed an entity instance to filter on a specific type.
85      */
86     public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool
87     {
88         if (strpos($action, '-') !== false) {
89             throw new InvalidArgumentException("Action should be a simple entity permission action, not a role permission");
90         }
91
92         $permissionQuery = EntityPermission::query()
93             ->where('action', '=', $action)
94             ->whereIn('role_id', $this->getCurrentUserRoleIds());
95
96         if (!empty($entityClass)) {
97             /** @var Entity $entityInstance */
98             $entityInstance = app()->make($entityClass);
99             $permissionQuery = $permissionQuery->where('restrictable_type', '=', $entityInstance->getMorphClass());
100         }
101
102         $hasPermission = $permissionQuery->count() > 0;
103
104         return $hasPermission;
105     }
106
107     /**
108      * Limited the given entity query so that the query will only
109      * return items that the user has view permission for.
110      */
111     public function restrictEntityQuery(Builder $query): Builder
112     {
113         return $query->where(function (Builder $parentQuery) {
114             $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
115                 $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds())
116                     // TODO - Delete line once only views
117                     ->where('action', '=', 'view')
118                     ->where(function (Builder $query) {
119                         $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
120                     });
121             });
122         });
123     }
124
125     /**
126      * Extend the given page query to ensure draft items are not visible
127      * unless created by the given user.
128      */
129     public function enforceDraftVisibilityOnQuery(Builder $query): Builder
130     {
131         return $query->where(function (Builder $query) {
132             $query->where('draft', '=', false)
133                 ->orWhere(function (Builder $query) {
134                     $query->where('draft', '=', true)
135                         ->where('owned_by', '=', $this->currentUser()->id);
136                 });
137         });
138     }
139
140     /**
141      * Add restrictions for a generic entity.
142      */
143     public function enforceEntityRestrictions(Entity $entity, Builder $query): Builder
144     {
145         if ($entity instanceof Page) {
146             // Prevent drafts being visible to others.
147             $this->enforceDraftVisibilityOnQuery($query);
148         }
149
150         return $this->entityRestrictionQuery($query);
151     }
152
153     /**
154      * The general query filter to remove all entities
155      * that the current user does not have access to.
156      */
157     protected function entityRestrictionQuery(Builder $query): Builder
158     {
159         $q = $query->where(function ($parentQuery) {
160             $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
161                 $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds())
162                     // TODO - Delete line once only views
163                     ->where('action', '=', 'view')
164                     ->where(function (Builder $query) {
165                         $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
166                     });
167             });
168         });
169
170         return $q;
171     }
172
173     /**
174      * Filter items that have entities set as a polymorphic relation.
175      * For simplicity, this will not return results attached to draft pages.
176      * Draft pages should never really have related items though.
177      *
178      * @param Builder|QueryBuilder $query
179      */
180     public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
181     {
182         $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
183         $pageMorphClass = (new Page())->getMorphClass();
184
185         $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
186             /** @var Builder $permissionQuery */
187             $permissionQuery->select(['role_id'])->from('joint_permissions')
188                 ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
189                 ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
190                 ->where('joint_permissions.action', '=', 'view')
191                 ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
192                 ->where(function (QueryBuilder $query) {
193                     $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
194                 });
195         })->where(function ($query) use ($tableDetails, $pageMorphClass) {
196             /** @var Builder $query */
197             $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
198                 ->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
199                     $query->select('id')->from('pages')
200                         ->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
201                         ->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
202                         ->where('pages.draft', '=', false);
203                 });
204         });
205
206         return $q;
207     }
208
209     /**
210      * Add conditions to a query to filter the selection to related entities
211      * where view permissions are granted.
212      */
213     public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
214     {
215         $fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
216         $instance = new $entityClass();
217         $morphClass = $instance->getMorphClass();
218
219         $existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
220             /** @var Builder $permissionQuery */
221             $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
222                 ->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
223                 ->where('joint_permissions.entity_type', '=', $morphClass)
224                 ->where('joint_permissions.action', '=', 'view')
225                 ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
226                 ->where(function (QueryBuilder $query) {
227                     $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
228                 });
229         };
230
231         $q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
232             $query->whereExists($existsQuery)
233                 ->orWhere($fullEntityIdColumn, '=', 0);
234         });
235
236         if ($instance instanceof Page) {
237             // Prevent visibility of non-owned draft pages
238             $q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
239                 $query->select('id')->from('pages')
240                     ->whereColumn('pages.id', '=', $fullEntityIdColumn)
241                     ->where(function (QueryBuilder $query) {
242                         $query->where('pages.draft', '=', false)
243                             ->orWhere('pages.owned_by', '=', $this->currentUser()->id);
244                     });
245             });
246         }
247
248         return $q;
249     }
250
251     /**
252      * Add the query for checking the given user id has permission
253      * within the join_permissions table.
254      *
255      * @param QueryBuilder|Builder $query
256      */
257     protected function addJointHasPermissionCheck($query, int $userIdToCheck)
258     {
259         $query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
260             $query->where('joint_permissions.has_permission_own', '=', true)
261                 ->where('joint_permissions.owned_by', '=', $userIdToCheck);
262         });
263     }
264
265     /**
266      * Get the current user.
267      */
268     protected function currentUser(): User
269     {
270         return user();
271     }
272
273     /**
274      * Get the roles for the current logged-in user.
275      *
276      * @return int[]
277      */
278     protected function getCurrentUserRoleIds(): array
279     {
280         if (auth()->guest()) {
281             return [Role::getSystemRole('public')->id];
282         }
283
284         return $this->currentUser()->roles->pluck('id')->values()->all();
285     }
286 }