1 <?php namespace BookStack\Services;
6 use BookStack\JointPermission;
11 use Illuminate\Support\Collection;
12 use Illuminate\Support\Facades\Log;
14 class PermissionService
17 protected $currentAction;
18 protected $isAdminUser;
19 protected $userRoles = false;
20 protected $currentUserModel = false;
26 protected $jointPermission;
29 protected $entityCache;
32 * PermissionService constructor.
33 * @param JointPermission $jointPermission
35 * @param Chapter $chapter
39 public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
41 $this->jointPermission = $jointPermission;
44 $this->chapter = $chapter;
49 * Prepare the local entity cache and ensure it's empty
51 protected function readyEntityCache()
53 $this->entityCache = [
55 'chapters' => collect()
60 * Get a book via ID, Checks local cache
64 protected function getBook($bookId)
66 if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
67 return $this->entityCache['books']->get($bookId);
70 $book = $this->book->find($bookId);
71 if ($book === null) $book = false;
72 if (isset($this->entityCache['books'])) {
73 $this->entityCache['books']->put($bookId, $book);
80 * Get a chapter via ID, Checks local cache
84 protected function getChapter($chapterId)
86 if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
87 return $this->entityCache['chapters']->get($chapterId);
90 $chapter = $this->chapter->find($chapterId);
91 if ($chapter === null) $chapter = false;
92 if (isset($this->entityCache['chapters'])) {
93 $this->entityCache['chapters']->put($chapterId, $chapter);
100 * Get the roles for the current user;
103 protected function getRoles()
105 if ($this->userRoles !== false) return $this->userRoles;
109 if (auth()->guest()) {
110 $roles[] = $this->role->getSystemRole('public')->id;
115 foreach ($this->currentUser()->roles as $role) {
116 $roles[] = $role->id;
122 * Re-generate all entity permission from scratch.
124 public function buildJointPermissions()
126 $this->jointPermission->truncate();
127 $this->readyEntityCache();
129 // Get all roles (Should be the most limited dimension)
130 $roles = $this->role->with('permissions')->get();
132 // Chunk through all books
133 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
134 $this->createManyJointPermissions($books, $roles);
137 // Chunk through all chapters
138 $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
139 $this->createManyJointPermissions($chapters, $roles);
142 // Chunk through all pages
143 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
144 $this->createManyJointPermissions($pages, $roles);
149 * Rebuild the entity jointPermissions for a particular entity.
150 * @param Entity $entity
152 public function buildJointPermissionsForEntity(Entity $entity)
154 $roles = $this->role->with('jointPermissions')->get();
155 $entities = collect([$entity]);
157 if ($entity->isA('book')) {
158 $entities = $entities->merge($entity->chapters);
159 $entities = $entities->merge($entity->pages);
160 } elseif ($entity->isA('chapter')) {
161 $entities = $entities->merge($entity->pages);
164 $this->deleteManyJointPermissionsForEntities($entities);
165 $this->createManyJointPermissions($entities, $roles);
169 * Rebuild the entity jointPermissions for a collection of entities.
170 * @param Collection $entities
172 public function buildJointPermissionsForEntities(Collection $entities)
174 $roles = $this->role->with('jointPermissions')->get();
175 $this->deleteManyJointPermissionsForEntities($entities);
176 $this->createManyJointPermissions($entities, $roles);
180 * Build the entity jointPermissions for a particular role.
183 public function buildJointPermissionForRole(Role $role)
185 $roles = collect([$role]);
187 $this->deleteManyJointPermissionsForRoles($roles);
189 // Chunk through all books
190 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
191 $this->createManyJointPermissions($books, $roles);
194 // Chunk through all chapters
195 $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
196 $this->createManyJointPermissions($books, $roles);
199 // Chunk through all pages
200 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
201 $this->createManyJointPermissions($books, $roles);
206 * Delete the entity jointPermissions attached to a particular role.
209 public function deleteJointPermissionsForRole(Role $role)
211 $this->deleteManyJointPermissionsForRoles([$role]);
215 * Delete all of the entity jointPermissions for a list of entities.
216 * @param Role[] $roles
218 protected function deleteManyJointPermissionsForRoles($roles)
220 foreach ($roles as $role) {
221 $role->jointPermissions()->delete();
226 * Delete the entity jointPermissions for a particular entity.
227 * @param Entity $entity
229 public function deleteJointPermissionsForEntity(Entity $entity)
231 $this->deleteManyJointPermissionsForEntities([$entity]);
235 * Delete all of the entity jointPermissions for a list of entities.
236 * @param Entity[] $entities
238 protected function deleteManyJointPermissionsForEntities($entities)
240 $query = $this->jointPermission->newQuery();
241 foreach ($entities as $entity) {
242 $query->orWhere(function($query) use ($entity) {
243 $query->where('entity_id', '=', $entity->id)
244 ->where('entity_type', '=', $entity->getMorphClass());
251 * Create & Save entity jointPermissions for many entities and jointPermissions.
252 * @param Collection $entities
253 * @param Collection $roles
255 protected function createManyJointPermissions($entities, $roles)
257 $this->readyEntityCache();
258 $jointPermissions = [];
259 foreach ($entities as $entity) {
260 foreach ($roles as $role) {
261 foreach ($this->getActions($entity) as $action) {
262 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
266 $this->jointPermission->insert($jointPermissions);
271 * Get the actions related to an entity.
275 protected function getActions($entity)
277 $baseActions = ['view', 'update', 'delete'];
279 if ($entity->isA('chapter')) {
280 $baseActions[] = 'page-create';
281 } else if ($entity->isA('book')) {
282 $baseActions[] = 'page-create';
283 $baseActions[] = 'chapter-create';
290 * Create entity permission data for an entity and role
291 * for a particular action.
292 * @param Entity $entity
297 protected function createJointPermissionData(Entity $entity, Role $role, $action)
299 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
300 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
301 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
302 $explodedAction = explode('-', $action);
303 $restrictionAction = end($explodedAction);
305 if ($entity->isA('book')) {
307 if (!$entity->restricted) {
308 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
310 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
311 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
314 } elseif ($entity->isA('chapter')) {
316 if (!$entity->restricted) {
317 $book = $this->getBook($entity->book_id);
318 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
319 $hasPermissiveAccessToBook = !$book->restricted;
320 return $this->createJointPermissionDataArray($entity, $role, $action,
321 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
322 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
324 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
325 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
328 } elseif ($entity->isA('page')) {
330 if (!$entity->restricted) {
331 $book = $this->getBook($entity->book_id);
332 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
333 $hasPermissiveAccessToBook = !$book->restricted;
335 $chapter = $this->getChapter($entity->chapter_id);
336 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
337 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
338 $acknowledgeChapter = ($chapter && $chapter->restricted);
340 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
341 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
343 return $this->createJointPermissionDataArray($entity, $role, $action,
344 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
345 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
348 $hasAccess = $entity->hasRestriction($role->id, $action);
349 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
356 * Create an array of data with the information of an entity jointPermissions.
357 * Used to build data for bulk insertion.
358 * @param Entity $entity
361 * @param $permissionAll
362 * @param $permissionOwn
365 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
367 $entityClass = get_class($entity);
369 'role_id' => $role->getRawAttribute('id'),
370 'entity_id' => $entity->getRawAttribute('id'),
371 'entity_type' => $entityClass,
373 'has_permission' => $permissionAll,
374 'has_permission_own' => $permissionOwn,
375 'created_by' => $entity->getRawAttribute('created_by')
380 * Checks if an entity has a restriction set upon it.
381 * @param Ownable $ownable
385 public function checkOwnableUserAccess(Ownable $ownable, $permission)
387 if ($this->isAdmin()) {
392 $explodedPermission = explode('-', $permission);
394 $baseQuery = $ownable->where('id', '=', $ownable->id);
395 $action = end($explodedPermission);
396 $this->currentAction = $action;
398 $nonJointPermissions = ['restrictions'];
400 // Handle non entity specific jointPermissions
401 if (in_array($explodedPermission[0], $nonJointPermissions)) {
402 $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
403 $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
404 $this->currentAction = 'view';
405 $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
406 return ($allPermission || ($isOwner && $ownPermission));
409 // Handle abnormal create jointPermissions
410 if ($action === 'create') {
411 $this->currentAction = $permission;
415 $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
421 * Check if an entity has restrictions set on itself or its
423 * @param Entity $entity
427 public function checkIfRestrictionsSet(Entity $entity, $action)
429 $this->currentAction = $action;
430 if ($entity->isA('page')) {
431 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
432 } elseif ($entity->isA('chapter')) {
433 return $entity->restricted || $entity->book->restricted;
434 } elseif ($entity->isA('book')) {
435 return $entity->restricted;
440 * The general query filter to remove all entities
441 * that the current user does not have access to.
445 protected function entityRestrictionQuery($query)
447 $q = $query->where(function ($parentQuery) {
448 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
449 $permissionQuery->whereIn('role_id', $this->getRoles())
450 ->where('action', '=', $this->currentAction)
451 ->where(function ($query) {
452 $query->where('has_permission', '=', true)
453 ->orWhere(function ($query) {
454 $query->where('has_permission_own', '=', true)
455 ->where('created_by', '=', $this->currentUser()->id);
465 * Add restrictions for a page query
467 * @param string $action
470 public function enforcePageRestrictions($query, $action = 'view')
472 // Prevent drafts being visible to others.
473 $query = $query->where(function ($query) {
474 $query->where('draft', '=', false);
475 if ($this->currentUser()) {
476 $query->orWhere(function ($query) {
477 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
482 return $this->enforceEntityRestrictions($query, $action);
486 * Add on permission restrictions to a chapter query.
488 * @param string $action
491 public function enforceChapterRestrictions($query, $action = 'view')
493 return $this->enforceEntityRestrictions($query, $action);
497 * Add restrictions to a book query.
499 * @param string $action
502 public function enforceBookRestrictions($query, $action = 'view')
504 return $this->enforceEntityRestrictions($query, $action);
508 * Add restrictions for a generic entity
510 * @param string $action
513 public function enforceEntityRestrictions($query, $action = 'view')
515 if ($this->isAdmin()) {
519 $this->currentAction = $action;
520 return $this->entityRestrictionQuery($query);
524 * Filter items that have entities set a a polymorphic relation.
526 * @param string $tableName
527 * @param string $entityIdColumn
528 * @param string $entityTypeColumn
531 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
533 if ($this->isAdmin()) {
538 $this->currentAction = 'view';
539 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
541 $q = $query->where(function ($query) use ($tableDetails) {
542 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
543 $permissionQuery->select('id')->from('joint_permissions')
544 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
545 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
546 ->where('action', '=', $this->currentAction)
547 ->whereIn('role_id', $this->getRoles())
548 ->where(function ($query) {
549 $query->where('has_permission', '=', true)->orWhere(function ($query) {
550 $query->where('has_permission_own', '=', true)
551 ->where('created_by', '=', $this->currentUser()->id);
560 * Filters pages that are a direct relation to another item.
563 * @param $entityIdColumn
566 public function filterRelatedPages($query, $tableName, $entityIdColumn)
568 if ($this->isAdmin()) {
573 $this->currentAction = 'view';
574 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
576 $q = $query->where(function ($query) use ($tableDetails) {
577 $query->where(function ($query) use (&$tableDetails) {
578 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
579 $permissionQuery->select('id')->from('joint_permissions')
580 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
581 ->where('entity_type', '=', 'Bookstack\\Page')
582 ->where('action', '=', $this->currentAction)
583 ->whereIn('role_id', $this->getRoles())
584 ->where(function ($query) {
585 $query->where('has_permission', '=', true)->orWhere(function ($query) {
586 $query->where('has_permission_own', '=', true)
587 ->where('created_by', '=', $this->currentUser()->id);
591 })->orWhere($tableDetails['entityIdColumn'], '=', 0);
598 * Check if the current user is an admin.
601 private function isAdmin()
603 if ($this->isAdminUser === null) {
604 $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false;
607 return $this->isAdminUser;
611 * Get the current user
614 private function currentUser()
616 if ($this->currentUserModel === false) {
617 $this->currentUserModel = user();
620 return $this->currentUserModel;
624 * Clean the cached user elements.
626 private function clean()
628 $this->currentUserModel = false;
629 $this->userRoles = false;
630 $this->isAdminUser = null;