1 <?php namespace BookStack\Services;
6 use BookStack\JointPermission;
10 use Illuminate\Database\Eloquent\Collection;
12 class PermissionService
17 protected $currentAction;
18 protected $currentUser;
24 protected $jointPermission;
28 * PermissionService constructor.
29 * @param JointPermission $jointPermission
31 * @param Chapter $chapter
35 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
37 $this->currentUser = auth()->user();
38 $userSet = $this->currentUser !== null;
39 $this->userRoles = false;
40 $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
41 if (!$userSet) $this->currentUser = new User();
43 $this->jointPermission = $jointPermission;
46 $this->chapter = $chapter;
51 * Get the roles for the current user;
54 protected function getRoles()
56 if ($this->userRoles !== false) return $this->userRoles;
60 if (auth()->guest()) {
61 $roles[] = $this->role->getSystemRole('public')->id;
66 foreach ($this->currentUser->roles as $role) {
73 * Re-generate all entity permission from scratch.
75 public function buildJointPermissions()
77 $this->jointPermission->truncate();
79 // Get all roles (Should be the most limited dimension)
80 $roles = $this->role->with('permissions')->get();
82 // Chunk through all books
83 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
84 $this->createManyJointPermissions($books, $roles);
87 // Chunk through all chapters
88 $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
89 $this->createManyJointPermissions($chapters, $roles);
92 // Chunk through all pages
93 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
94 $this->createManyJointPermissions($pages, $roles);
99 * Create the entity jointPermissions for a particular entity.
100 * @param Entity $entity
102 public function buildJointPermissionsForEntity(Entity $entity)
104 $roles = $this->role->with('jointPermissions')->get();
105 $entities = collect([$entity]);
107 if ($entity->isA('book')) {
108 $entities = $entities->merge($entity->chapters);
109 $entities = $entities->merge($entity->pages);
110 } elseif ($entity->isA('chapter')) {
111 $entities = $entities->merge($entity->pages);
114 $this->deleteManyJointPermissionsForEntities($entities);
115 $this->createManyJointPermissions($entities, $roles);
119 * Build the entity jointPermissions for a particular role.
122 public function buildJointPermissionForRole(Role $role)
124 $roles = collect([$role]);
126 $this->deleteManyJointPermissionsForRoles($roles);
128 // Chunk through all books
129 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
130 $this->createManyJointPermissions($books, $roles);
133 // Chunk through all chapters
134 $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
135 $this->createManyJointPermissions($books, $roles);
138 // Chunk through all pages
139 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
140 $this->createManyJointPermissions($books, $roles);
145 * Delete the entity jointPermissions attached to a particular role.
148 public function deleteJointPermissionsForRole(Role $role)
150 $this->deleteManyJointPermissionsForRoles([$role]);
154 * Delete all of the entity jointPermissions for a list of entities.
155 * @param Role[] $roles
157 protected function deleteManyJointPermissionsForRoles($roles)
159 foreach ($roles as $role) {
160 $role->jointPermissions()->delete();
165 * Delete the entity jointPermissions for a particular entity.
166 * @param Entity $entity
168 public function deleteJointPermissionsForEntity(Entity $entity)
170 $this->deleteManyJointPermissionsForEntities([$entity]);
174 * Delete all of the entity jointPermissions for a list of entities.
175 * @param Entity[] $entities
177 protected function deleteManyJointPermissionsForEntities($entities)
179 foreach ($entities as $entity) {
180 $entity->jointPermissions()->delete();
185 * Create & Save entity jointPermissions for many entities and jointPermissions.
186 * @param Collection $entities
187 * @param Collection $roles
189 protected function createManyJointPermissions($entities, $roles)
191 $jointPermissions = [];
192 foreach ($entities as $entity) {
193 foreach ($roles as $role) {
194 foreach ($this->getActions($entity) as $action) {
195 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
199 $this->jointPermission->insert($jointPermissions);
204 * Get the actions related to an entity.
208 protected function getActions($entity)
210 $baseActions = ['view', 'update', 'delete'];
212 if ($entity->isA('chapter')) {
213 $baseActions[] = 'page-create';
214 } else if ($entity->isA('book')) {
215 $baseActions[] = 'page-create';
216 $baseActions[] = 'chapter-create';
223 * Create entity permission data for an entity and role
224 * for a particular action.
225 * @param Entity $entity
230 protected function createJointPermissionData(Entity $entity, Role $role, $action)
232 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
233 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
234 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
235 $explodedAction = explode('-', $action);
236 $restrictionAction = end($explodedAction);
238 if ($entity->isA('book')) {
240 if (!$entity->restricted) {
241 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
243 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
244 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
247 } elseif ($entity->isA('chapter')) {
249 if (!$entity->restricted) {
250 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
251 $hasPermissiveAccessToBook = !$entity->book->restricted;
252 return $this->createJointPermissionDataArray($entity, $role, $action,
253 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
254 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
256 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
257 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
260 } elseif ($entity->isA('page')) {
262 if (!$entity->restricted) {
263 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
264 $hasPermissiveAccessToBook = !$entity->book->restricted;
265 $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
266 $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
267 $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
269 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
270 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
272 return $this->createJointPermissionDataArray($entity, $role, $action,
273 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
274 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
277 $hasAccess = $entity->hasRestriction($role->id, $action);
278 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
285 * Create an array of data with the information of an entity jointPermissions.
286 * Used to build data for bulk insertion.
287 * @param Entity $entity
290 * @param $permissionAll
291 * @param $permissionOwn
294 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
296 $entityClass = get_class($entity);
298 'role_id' => $role->getRawAttribute('id'),
299 'entity_id' => $entity->getRawAttribute('id'),
300 'entity_type' => $entityClass,
302 'has_permission' => $permissionAll,
303 'has_permission_own' => $permissionOwn,
304 'created_by' => $entity->getRawAttribute('created_by')
309 * Checks if an entity has a restriction set upon it.
310 * @param Entity $entity
314 public function checkEntityUserAccess(Entity $entity, $permission)
316 if ($this->isAdmin) return true;
317 $explodedPermission = explode('-', $permission);
319 $baseQuery = $entity->where('id', '=', $entity->id);
320 $action = end($explodedPermission);
321 $this->currentAction = $action;
323 $nonJointPermissions = ['restrictions'];
325 // Handle non entity specific jointPermissions
326 if (in_array($explodedPermission[0], $nonJointPermissions)) {
327 $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
328 $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
329 $this->currentAction = 'view';
330 $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
331 return ($allPermission || ($isOwner && $ownPermission));
334 // Handle abnormal create jointPermissions
335 if ($action === 'create') {
336 $this->currentAction = $permission;
340 return $this->entityRestrictionQuery($baseQuery)->count() > 0;
344 * Check if an entity has restrictions set on itself or its
346 * @param Entity $entity
350 public function checkIfRestrictionsSet(Entity $entity, $action)
352 $this->currentAction = $action;
353 if ($entity->isA('page')) {
354 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
355 } elseif ($entity->isA('chapter')) {
356 return $entity->restricted || $entity->book->restricted;
357 } elseif ($entity->isA('book')) {
358 return $entity->restricted;
363 * The general query filter to remove all entities
364 * that the current user does not have access to.
368 protected function entityRestrictionQuery($query)
370 return $query->where(function ($parentQuery) {
371 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
372 $permissionQuery->whereIn('role_id', $this->getRoles())
373 ->where('action', '=', $this->currentAction)
374 ->where(function ($query) {
375 $query->where('has_permission', '=', true)
376 ->orWhere(function ($query) {
377 $query->where('has_permission_own', '=', true)
378 ->where('created_by', '=', $this->currentUser->id);
386 * Add restrictions for a page query
388 * @param string $action
391 public function enforcePageRestrictions($query, $action = 'view')
393 // Prevent drafts being visible to others.
394 $query = $query->where(function ($query) {
395 $query->where('draft', '=', false);
396 if ($this->currentUser) {
397 $query->orWhere(function ($query) {
398 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
403 if ($this->isAdmin) return $query;
404 $this->currentAction = $action;
405 return $this->entityRestrictionQuery($query);
409 * Add on permission restrictions to a chapter query.
411 * @param string $action
414 public function enforceChapterRestrictions($query, $action = 'view')
416 if ($this->isAdmin) return $query;
417 $this->currentAction = $action;
418 return $this->entityRestrictionQuery($query);
422 * Add restrictions to a book query.
424 * @param string $action
427 public function enforceBookRestrictions($query, $action = 'view')
429 if ($this->isAdmin) return $query;
430 $this->currentAction = $action;
431 return $this->entityRestrictionQuery($query);
435 * Filter items that have entities set a a polymorphic relation.
437 * @param string $tableName
438 * @param string $entityIdColumn
439 * @param string $entityTypeColumn
442 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
444 if ($this->isAdmin) return $query;
445 $this->currentAction = 'view';
446 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
448 return $query->where(function ($query) use ($tableDetails) {
449 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
450 $permissionQuery->select('id')->from('joint_permissions')
451 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
452 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
453 ->where('action', '=', $this->currentAction)
454 ->whereIn('role_id', $this->getRoles())
455 ->where(function ($query) {
456 $query->where('has_permission', '=', true)->orWhere(function ($query) {
457 $query->where('has_permission_own', '=', true)
458 ->where('created_by', '=', $this->currentUser->id);
467 * Filters pages that are a direct relation to another item.
470 * @param $entityIdColumn
473 public function filterRelatedPages($query, $tableName, $entityIdColumn)
475 if ($this->isAdmin) return $query;
476 $this->currentAction = 'view';
477 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
479 return $query->where(function ($query) use ($tableDetails) {
480 $query->where(function ($query) use (&$tableDetails) {
481 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
482 $permissionQuery->select('id')->from('joint_permissions')
483 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
484 ->where('entity_type', '=', 'Bookstack\\Page')
485 ->where('action', '=', $this->currentAction)
486 ->whereIn('role_id', $this->getRoles())
487 ->where(function ($query) {
488 $query->where('has_permission', '=', true)->orWhere(function ($query) {
489 $query->where('has_permission_own', '=', true)
490 ->where('created_by', '=', $this->currentUser->id);
494 })->orWhere($tableDetails['entityIdColumn'], '=', 0);