1 <?php namespace BookStack\Services;
6 use BookStack\JointPermission;
11 use Illuminate\Support\Collection;
13 class PermissionService
18 protected $currentAction;
19 protected $currentUser;
25 protected $jointPermission;
28 protected $entityCache;
31 * PermissionService constructor.
32 * @param JointPermission $jointPermission
34 * @param Chapter $chapter
38 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
40 $this->currentUser = auth()->user();
41 $userSet = $this->currentUser !== null;
42 $this->userRoles = false;
43 $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
44 if (!$userSet) $this->currentUser = new User();
46 $this->jointPermission = $jointPermission;
49 $this->chapter = $chapter;
54 * Prepare the local entity cache and ensure it's empty
56 protected function readyEntityCache()
58 $this->entityCache = [
60 'chapters' => collect()
65 * Get a book via ID, Checks local cache
69 protected function getBook($bookId)
71 if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
72 return $this->entityCache['books']->get($bookId);
75 $book = $this->book->find($bookId);
76 if ($book === null) $book = false;
77 if (isset($this->entityCache['books'])) {
78 $this->entityCache['books']->put($bookId, $book);
85 * Get a chapter via ID, Checks local cache
89 protected function getChapter($chapterId)
91 if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
92 return $this->entityCache['chapters']->get($chapterId);
95 $chapter = $this->chapter->find($chapterId);
96 if ($chapter === null) $chapter = false;
97 if (isset($this->entityCache['chapters'])) {
98 $this->entityCache['chapters']->put($chapterId, $chapter);
105 * Get the roles for the current user;
108 protected function getRoles()
110 if ($this->userRoles !== false) return $this->userRoles;
114 if (auth()->guest()) {
115 $roles[] = $this->role->getSystemRole('public')->id;
120 foreach ($this->currentUser->roles as $role) {
121 $roles[] = $role->id;
127 * Re-generate all entity permission from scratch.
129 public function buildJointPermissions()
131 $this->jointPermission->truncate();
132 $this->readyEntityCache();
134 // Get all roles (Should be the most limited dimension)
135 $roles = $this->role->with('permissions')->get();
137 // Chunk through all books
138 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
139 $this->createManyJointPermissions($books, $roles);
142 // Chunk through all chapters
143 $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
144 $this->createManyJointPermissions($chapters, $roles);
147 // Chunk through all pages
148 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
149 $this->createManyJointPermissions($pages, $roles);
154 * Rebuild the entity jointPermissions for a particular entity.
155 * @param Entity $entity
157 public function buildJointPermissionsForEntity(Entity $entity)
159 $roles = $this->role->with('jointPermissions')->get();
160 $entities = collect([$entity]);
162 if ($entity->isA('book')) {
163 $entities = $entities->merge($entity->chapters);
164 $entities = $entities->merge($entity->pages);
165 } elseif ($entity->isA('chapter')) {
166 $entities = $entities->merge($entity->pages);
169 $this->deleteManyJointPermissionsForEntities($entities);
170 $this->createManyJointPermissions($entities, $roles);
174 * Rebuild the entity jointPermissions for a collection of entities.
175 * @param Collection $entities
177 public function buildJointPermissionsForEntities(Collection $entities)
179 $roles = $this->role->with('jointPermissions')->get();
180 $this->deleteManyJointPermissionsForEntities($entities);
181 $this->createManyJointPermissions($entities, $roles);
185 * Build the entity jointPermissions for a particular role.
188 public function buildJointPermissionForRole(Role $role)
190 $roles = collect([$role]);
192 $this->deleteManyJointPermissionsForRoles($roles);
194 // Chunk through all books
195 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
196 $this->createManyJointPermissions($books, $roles);
199 // Chunk through all chapters
200 $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
201 $this->createManyJointPermissions($books, $roles);
204 // Chunk through all pages
205 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
206 $this->createManyJointPermissions($books, $roles);
211 * Delete the entity jointPermissions attached to a particular role.
214 public function deleteJointPermissionsForRole(Role $role)
216 $this->deleteManyJointPermissionsForRoles([$role]);
220 * Delete all of the entity jointPermissions for a list of entities.
221 * @param Role[] $roles
223 protected function deleteManyJointPermissionsForRoles($roles)
225 foreach ($roles as $role) {
226 $role->jointPermissions()->delete();
231 * Delete the entity jointPermissions for a particular entity.
232 * @param Entity $entity
234 public function deleteJointPermissionsForEntity(Entity $entity)
236 $this->deleteManyJointPermissionsForEntities([$entity]);
240 * Delete all of the entity jointPermissions for a list of entities.
241 * @param Entity[] $entities
243 protected function deleteManyJointPermissionsForEntities($entities)
245 $query = $this->jointPermission->newQuery();
246 foreach ($entities as $entity) {
247 $query->orWhere(function($query) use ($entity) {
248 $query->where('entity_id', '=', $entity->id)
249 ->where('entity_type', '=', $entity->getMorphClass());
256 * Create & Save entity jointPermissions for many entities and jointPermissions.
257 * @param Collection $entities
258 * @param Collection $roles
260 protected function createManyJointPermissions($entities, $roles)
262 $this->readyEntityCache();
263 $jointPermissions = [];
264 foreach ($entities as $entity) {
265 foreach ($roles as $role) {
266 foreach ($this->getActions($entity) as $action) {
267 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
271 $this->jointPermission->insert($jointPermissions);
276 * Get the actions related to an entity.
280 protected function getActions($entity)
282 $baseActions = ['view', 'update', 'delete'];
284 if ($entity->isA('chapter')) {
285 $baseActions[] = 'page-create';
286 } else if ($entity->isA('book')) {
287 $baseActions[] = 'page-create';
288 $baseActions[] = 'chapter-create';
295 * Create entity permission data for an entity and role
296 * for a particular action.
297 * @param Entity $entity
302 protected function createJointPermissionData(Entity $entity, Role $role, $action)
304 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
305 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
306 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
307 $explodedAction = explode('-', $action);
308 $restrictionAction = end($explodedAction);
310 if ($entity->isA('book')) {
312 if (!$entity->restricted) {
313 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
315 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
316 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
319 } elseif ($entity->isA('chapter')) {
321 if (!$entity->restricted) {
322 $book = $this->getBook($entity->book_id);
323 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
324 $hasPermissiveAccessToBook = !$book->restricted;
325 return $this->createJointPermissionDataArray($entity, $role, $action,
326 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
327 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
329 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
330 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
333 } elseif ($entity->isA('page')) {
335 if (!$entity->restricted) {
336 $book = $this->getBook($entity->book_id);
337 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
338 $hasPermissiveAccessToBook = !$book->restricted;
340 $chapter = $this->getChapter($entity->chapter_id);
341 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
342 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
343 $acknowledgeChapter = ($chapter && $chapter->restricted);
345 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
346 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
348 return $this->createJointPermissionDataArray($entity, $role, $action,
349 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
350 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
353 $hasAccess = $entity->hasRestriction($role->id, $action);
354 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
361 * Create an array of data with the information of an entity jointPermissions.
362 * Used to build data for bulk insertion.
363 * @param Entity $entity
366 * @param $permissionAll
367 * @param $permissionOwn
370 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
372 $entityClass = get_class($entity);
374 'role_id' => $role->getRawAttribute('id'),
375 'entity_id' => $entity->getRawAttribute('id'),
376 'entity_type' => $entityClass,
378 'has_permission' => $permissionAll,
379 'has_permission_own' => $permissionOwn,
380 'created_by' => $entity->getRawAttribute('created_by')
385 * Checks if an entity has a restriction set upon it.
386 * @param Ownable $ownable
390 public function checkOwnableUserAccess(Ownable $ownable, $permission)
392 if ($this->isAdmin) return true;
393 $explodedPermission = explode('-', $permission);
395 $baseQuery = $ownable->where('id', '=', $ownable->id);
396 $action = end($explodedPermission);
397 $this->currentAction = $action;
399 $nonJointPermissions = ['restrictions'];
401 // Handle non entity specific jointPermissions
402 if (in_array($explodedPermission[0], $nonJointPermissions)) {
403 $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
404 $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
405 $this->currentAction = 'view';
406 $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
407 return ($allPermission || ($isOwner && $ownPermission));
410 // Handle abnormal create jointPermissions
411 if ($action === 'create') {
412 $this->currentAction = $permission;
416 return $this->entityRestrictionQuery($baseQuery)->count() > 0;
420 * Check if an entity has restrictions set on itself or its
422 * @param Entity $entity
426 public function checkIfRestrictionsSet(Entity $entity, $action)
428 $this->currentAction = $action;
429 if ($entity->isA('page')) {
430 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
431 } elseif ($entity->isA('chapter')) {
432 return $entity->restricted || $entity->book->restricted;
433 } elseif ($entity->isA('book')) {
434 return $entity->restricted;
439 * The general query filter to remove all entities
440 * that the current user does not have access to.
444 protected function entityRestrictionQuery($query)
446 return $query->where(function ($parentQuery) {
447 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
448 $permissionQuery->whereIn('role_id', $this->getRoles())
449 ->where('action', '=', $this->currentAction)
450 ->where(function ($query) {
451 $query->where('has_permission', '=', true)
452 ->orWhere(function ($query) {
453 $query->where('has_permission_own', '=', true)
454 ->where('created_by', '=', $this->currentUser->id);
462 * Add restrictions for a page query
464 * @param string $action
467 public function enforcePageRestrictions($query, $action = 'view')
469 // Prevent drafts being visible to others.
470 $query = $query->where(function ($query) {
471 $query->where('draft', '=', false);
472 if ($this->currentUser) {
473 $query->orWhere(function ($query) {
474 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
479 return $this->enforceEntityRestrictions($query, $action);
483 * Add on permission restrictions to a chapter query.
485 * @param string $action
488 public function enforceChapterRestrictions($query, $action = 'view')
490 return $this->enforceEntityRestrictions($query, $action);
494 * Add restrictions to a book query.
496 * @param string $action
499 public function enforceBookRestrictions($query, $action = 'view')
501 return $this->enforceEntityRestrictions($query, $action);
505 * Add restrictions for a generic entity
507 * @param string $action
510 public function enforceEntityRestrictions($query, $action = 'view')
512 if ($this->isAdmin) return $query;
513 $this->currentAction = $action;
514 return $this->entityRestrictionQuery($query);
518 * Filter items that have entities set a a polymorphic relation.
520 * @param string $tableName
521 * @param string $entityIdColumn
522 * @param string $entityTypeColumn
525 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
527 if ($this->isAdmin) return $query;
528 $this->currentAction = 'view';
529 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
531 return $query->where(function ($query) use ($tableDetails) {
532 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
533 $permissionQuery->select('id')->from('joint_permissions')
534 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
535 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
536 ->where('action', '=', $this->currentAction)
537 ->whereIn('role_id', $this->getRoles())
538 ->where(function ($query) {
539 $query->where('has_permission', '=', true)->orWhere(function ($query) {
540 $query->where('has_permission_own', '=', true)
541 ->where('created_by', '=', $this->currentUser->id);
550 * Filters pages that are a direct relation to another item.
553 * @param $entityIdColumn
556 public function filterRelatedPages($query, $tableName, $entityIdColumn)
558 if ($this->isAdmin) return $query;
559 $this->currentAction = 'view';
560 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
562 return $query->where(function ($query) use ($tableDetails) {
563 $query->where(function ($query) use (&$tableDetails) {
564 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
565 $permissionQuery->select('id')->from('joint_permissions')
566 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
567 ->where('entity_type', '=', 'Bookstack\\Page')
568 ->where('action', '=', $this->currentAction)
569 ->whereIn('role_id', $this->getRoles())
570 ->where(function ($query) {
571 $query->where('has_permission', '=', true)->orWhere(function ($query) {
572 $query->where('has_permission_own', '=', true)
573 ->where('created_by', '=', $this->currentUser->id);
577 })->orWhere($tableDetails['entityIdColumn'], '=', 0);