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