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