1 <?php namespace BookStack\Services;
6 use BookStack\JointPermission;
11 use Illuminate\Database\Eloquent\Collection;
13 class PermissionService
18 protected $currentAction;
19 protected $currentUser;
25 protected $jointPermission;
29 * PermissionService constructor.
30 * @param JointPermission $jointPermission
32 * @param Chapter $chapter
36 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
38 $this->currentUser = auth()->user();
39 $userSet = $this->currentUser !== null;
40 $this->userRoles = false;
41 $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
42 if (!$userSet) $this->currentUser = new User();
44 $this->jointPermission = $jointPermission;
47 $this->chapter = $chapter;
52 * Get the roles for the current user;
55 protected function getRoles()
57 if ($this->userRoles !== false) return $this->userRoles;
61 if (auth()->guest()) {
62 $roles[] = $this->role->getSystemRole('public')->id;
67 foreach ($this->currentUser->roles as $role) {
74 * Re-generate all entity permission from scratch.
76 public function buildJointPermissions()
78 $this->jointPermission->truncate();
80 // Get all roles (Should be the most limited dimension)
81 $roles = $this->role->with('permissions')->get();
83 // Chunk through all books
84 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
85 $this->createManyJointPermissions($books, $roles);
88 // Chunk through all chapters
89 $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
90 $this->createManyJointPermissions($chapters, $roles);
93 // Chunk through all pages
94 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
95 $this->createManyJointPermissions($pages, $roles);
100 * Create the entity jointPermissions for a particular entity.
101 * @param Entity $entity
103 public function buildJointPermissionsForEntity(Entity $entity)
105 $roles = $this->role->with('jointPermissions')->get();
106 $entities = collect([$entity]);
108 if ($entity->isA('book')) {
109 $entities = $entities->merge($entity->chapters);
110 $entities = $entities->merge($entity->pages);
111 } elseif ($entity->isA('chapter')) {
112 $entities = $entities->merge($entity->pages);
115 $this->deleteManyJointPermissionsForEntities($entities);
116 $this->createManyJointPermissions($entities, $roles);
120 * Build the entity jointPermissions for a particular role.
123 public function buildJointPermissionForRole(Role $role)
125 $roles = collect([$role]);
127 $this->deleteManyJointPermissionsForRoles($roles);
129 // Chunk through all books
130 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
131 $this->createManyJointPermissions($books, $roles);
134 // Chunk through all chapters
135 $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
136 $this->createManyJointPermissions($books, $roles);
139 // Chunk through all pages
140 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
141 $this->createManyJointPermissions($books, $roles);
146 * Delete the entity jointPermissions attached to a particular role.
149 public function deleteJointPermissionsForRole(Role $role)
151 $this->deleteManyJointPermissionsForRoles([$role]);
155 * Delete all of the entity jointPermissions for a list of entities.
156 * @param Role[] $roles
158 protected function deleteManyJointPermissionsForRoles($roles)
160 foreach ($roles as $role) {
161 $role->jointPermissions()->delete();
166 * Delete the entity jointPermissions for a particular entity.
167 * @param Entity $entity
169 public function deleteJointPermissionsForEntity(Entity $entity)
171 $this->deleteManyJointPermissionsForEntities([$entity]);
175 * Delete all of the entity jointPermissions for a list of entities.
176 * @param Entity[] $entities
178 protected function deleteManyJointPermissionsForEntities($entities)
180 foreach ($entities as $entity) {
181 $entity->jointPermissions()->delete();
186 * Create & Save entity jointPermissions for many entities and jointPermissions.
187 * @param Collection $entities
188 * @param Collection $roles
190 protected function createManyJointPermissions($entities, $roles)
192 $jointPermissions = [];
193 foreach ($entities as $entity) {
194 foreach ($roles as $role) {
195 foreach ($this->getActions($entity) as $action) {
196 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
200 $this->jointPermission->insert($jointPermissions);
205 * Get the actions related to an entity.
209 protected function getActions($entity)
211 $baseActions = ['view', 'update', 'delete'];
213 if ($entity->isA('chapter')) {
214 $baseActions[] = 'page-create';
215 } else if ($entity->isA('book')) {
216 $baseActions[] = 'page-create';
217 $baseActions[] = 'chapter-create';
224 * Create entity permission data for an entity and role
225 * for a particular action.
226 * @param Entity $entity
231 protected function createJointPermissionData(Entity $entity, Role $role, $action)
233 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
234 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
235 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
236 $explodedAction = explode('-', $action);
237 $restrictionAction = end($explodedAction);
239 if ($entity->isA('book')) {
241 if (!$entity->restricted) {
242 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
244 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
245 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
248 } elseif ($entity->isA('chapter')) {
250 if (!$entity->restricted) {
251 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
252 $hasPermissiveAccessToBook = !$entity->book->restricted;
253 return $this->createJointPermissionDataArray($entity, $role, $action,
254 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
255 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
257 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
258 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
261 } elseif ($entity->isA('page')) {
263 if (!$entity->restricted) {
264 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
265 $hasPermissiveAccessToBook = !$entity->book->restricted;
266 $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
267 $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
268 $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
270 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
271 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
273 return $this->createJointPermissionDataArray($entity, $role, $action,
274 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
275 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
278 $hasAccess = $entity->hasRestriction($role->id, $action);
279 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
286 * Create an array of data with the information of an entity jointPermissions.
287 * Used to build data for bulk insertion.
288 * @param Entity $entity
291 * @param $permissionAll
292 * @param $permissionOwn
295 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
297 $entityClass = get_class($entity);
299 'role_id' => $role->getRawAttribute('id'),
300 'entity_id' => $entity->getRawAttribute('id'),
301 'entity_type' => $entityClass,
303 'has_permission' => $permissionAll,
304 'has_permission_own' => $permissionOwn,
305 'created_by' => $entity->getRawAttribute('created_by')
310 * Checks if an entity has a restriction set upon it.
311 * @param Ownable $ownable
315 public function checkOwnableUserAccess(Ownable $ownable, $permission)
317 if ($this->isAdmin) return true;
318 $explodedPermission = explode('-', $permission);
320 $baseQuery = $ownable->where('id', '=', $ownable->id);
321 $action = end($explodedPermission);
322 $this->currentAction = $action;
324 $nonJointPermissions = ['restrictions'];
326 // Handle non entity specific jointPermissions
327 if (in_array($explodedPermission[0], $nonJointPermissions)) {
328 $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
329 $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
330 $this->currentAction = 'view';
331 $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
332 return ($allPermission || ($isOwner && $ownPermission));
335 // Handle abnormal create jointPermissions
336 if ($action === 'create') {
337 $this->currentAction = $permission;
341 return $this->entityRestrictionQuery($baseQuery)->count() > 0;
345 * Check if an entity has restrictions set on itself or its
347 * @param Entity $entity
351 public function checkIfRestrictionsSet(Entity $entity, $action)
353 $this->currentAction = $action;
354 if ($entity->isA('page')) {
355 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
356 } elseif ($entity->isA('chapter')) {
357 return $entity->restricted || $entity->book->restricted;
358 } elseif ($entity->isA('book')) {
359 return $entity->restricted;
364 * The general query filter to remove all entities
365 * that the current user does not have access to.
369 protected function entityRestrictionQuery($query)
371 return $query->where(function ($parentQuery) {
372 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
373 $permissionQuery->whereIn('role_id', $this->getRoles())
374 ->where('action', '=', $this->currentAction)
375 ->where(function ($query) {
376 $query->where('has_permission', '=', true)
377 ->orWhere(function ($query) {
378 $query->where('has_permission_own', '=', true)
379 ->where('created_by', '=', $this->currentUser->id);
387 * Add restrictions for a page query
389 * @param string $action
392 public function enforcePageRestrictions($query, $action = 'view')
394 // Prevent drafts being visible to others.
395 $query = $query->where(function ($query) {
396 $query->where('draft', '=', false);
397 if ($this->currentUser) {
398 $query->orWhere(function ($query) {
399 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
404 return $this->enforceEntityRestrictions($query, $action);
408 * Add on permission restrictions to a chapter query.
410 * @param string $action
413 public function enforceChapterRestrictions($query, $action = 'view')
415 return $this->enforceEntityRestrictions($query, $action);
419 * Add restrictions to a book query.
421 * @param string $action
424 public function enforceBookRestrictions($query, $action = 'view')
426 return $this->enforceEntityRestrictions($query, $action);
430 * Add restrictions for a generic entity
432 * @param string $action
435 public function enforceEntityRestrictions($query, $action = 'view')
437 if ($this->isAdmin) return $query;
438 $this->currentAction = $action;
439 return $this->entityRestrictionQuery($query);
443 * Filter items that have entities set a a polymorphic relation.
445 * @param string $tableName
446 * @param string $entityIdColumn
447 * @param string $entityTypeColumn
450 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
452 if ($this->isAdmin) return $query;
453 $this->currentAction = 'view';
454 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
456 return $query->where(function ($query) use ($tableDetails) {
457 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
458 $permissionQuery->select('id')->from('joint_permissions')
459 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
460 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
461 ->where('action', '=', $this->currentAction)
462 ->whereIn('role_id', $this->getRoles())
463 ->where(function ($query) {
464 $query->where('has_permission', '=', true)->orWhere(function ($query) {
465 $query->where('has_permission_own', '=', true)
466 ->where('created_by', '=', $this->currentUser->id);
475 * Filters pages that are a direct relation to another item.
478 * @param $entityIdColumn
481 public function filterRelatedPages($query, $tableName, $entityIdColumn)
483 if ($this->isAdmin) return $query;
484 $this->currentAction = 'view';
485 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
487 return $query->where(function ($query) use ($tableDetails) {
488 $query->where(function ($query) use (&$tableDetails) {
489 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
490 $permissionQuery->select('id')->from('joint_permissions')
491 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
492 ->where('entity_type', '=', 'Bookstack\\Page')
493 ->where('action', '=', $this->currentAction)
494 ->whereIn('role_id', $this->getRoles())
495 ->where(function ($query) {
496 $query->where('has_permission', '=', true)->orWhere(function ($query) {
497 $query->where('has_permission_own', '=', true)
498 ->where('created_by', '=', $this->currentUser->id);
502 })->orWhere($tableDetails['entityIdColumn'], '=', 0);