1 <?php namespace BookStack\Services;
6 use BookStack\JointPermission;
11 use Illuminate\Database\Connection;
12 use Illuminate\Database\Eloquent\Builder;
13 use Illuminate\Support\Collection;
15 class PermissionService
18 protected $currentAction;
19 protected $isAdminUser;
20 protected $userRoles = false;
21 protected $currentUserModel = false;
29 protected $jointPermission;
32 protected $entityCache;
35 * PermissionService constructor.
36 * @param JointPermission $jointPermission
37 * @param Connection $db
39 * @param Chapter $chapter
43 public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
46 $this->jointPermission = $jointPermission;
49 $this->chapter = $chapter;
51 // TODO - Update so admin still goes through filters
55 * Prepare the local entity cache and ensure it's empty
57 protected function readyEntityCache()
59 $this->entityCache = [
61 'chapters' => collect()
66 * Get a book via ID, Checks local cache
70 protected function getBook($bookId)
72 if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
73 return $this->entityCache['books']->get($bookId);
76 $book = $this->book->find($bookId);
77 if ($book === null) $book = false;
78 if (isset($this->entityCache['books'])) {
79 $this->entityCache['books']->put($bookId, $book);
86 * Get a chapter via ID, Checks local cache
90 protected function getChapter($chapterId)
92 if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
93 return $this->entityCache['chapters']->get($chapterId);
96 $chapter = $this->chapter->find($chapterId);
97 if ($chapter === null) $chapter = false;
98 if (isset($this->entityCache['chapters'])) {
99 $this->entityCache['chapters']->put($chapterId, $chapter);
106 * Get the roles for the current user;
109 protected function getRoles()
111 if ($this->userRoles !== false) return $this->userRoles;
115 if (auth()->guest()) {
116 $roles[] = $this->role->getSystemRole('public')->id;
121 foreach ($this->currentUser()->roles as $role) {
122 $roles[] = $role->id;
128 * Re-generate all entity permission from scratch.
130 public function buildJointPermissions()
132 $this->jointPermission->truncate();
133 $this->readyEntityCache();
135 // Get all roles (Should be the most limited dimension)
136 $roles = $this->role->with('permissions')->get();
138 // Chunk through all books
139 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
140 $this->createManyJointPermissions($books, $roles);
143 // Chunk through all chapters
144 $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
145 $this->createManyJointPermissions($chapters, $roles);
148 // Chunk through all pages
149 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
150 $this->createManyJointPermissions($pages, $roles);
155 * Rebuild the entity jointPermissions for a particular entity.
156 * @param Entity $entity
158 public function buildJointPermissionsForEntity(Entity $entity)
160 $roles = $this->role->get();
161 $entities = collect([$entity]);
163 if ($entity->isA('book')) {
164 $entities = $entities->merge($entity->chapters);
165 $entities = $entities->merge($entity->pages);
166 } elseif ($entity->isA('chapter')) {
167 $entities = $entities->merge($entity->pages);
170 $this->deleteManyJointPermissionsForEntities($entities);
171 $this->createManyJointPermissions($entities, $roles);
175 * Rebuild the entity jointPermissions for a collection of entities.
176 * @param Collection $entities
178 public function buildJointPermissionsForEntities(Collection $entities)
180 $roles = $this->role->get();
181 $this->deleteManyJointPermissionsForEntities($entities);
182 $this->createManyJointPermissions($entities, $roles);
186 * Build the entity jointPermissions for a particular role.
189 public function buildJointPermissionForRole(Role $role)
191 $roles = collect([$role]);
193 $this->deleteManyJointPermissionsForRoles($roles);
195 // Chunk through all books
196 $this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
197 $this->createManyJointPermissions($books, $roles);
200 // Chunk through all chapters
201 $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
202 $this->createManyJointPermissions($books, $roles);
205 // Chunk through all pages
206 $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
207 $this->createManyJointPermissions($books, $roles);
212 * Delete the entity jointPermissions attached to a particular role.
215 public function deleteJointPermissionsForRole(Role $role)
217 $this->deleteManyJointPermissionsForRoles([$role]);
221 * Delete all of the entity jointPermissions for a list of entities.
222 * @param Role[] $roles
224 protected function deleteManyJointPermissionsForRoles($roles)
226 foreach ($roles as $role) {
227 $role->jointPermissions()->delete();
232 * Delete the entity jointPermissions for a particular entity.
233 * @param Entity $entity
235 public function deleteJointPermissionsForEntity(Entity $entity)
237 $this->deleteManyJointPermissionsForEntities([$entity]);
241 * Delete all of the entity jointPermissions for a list of entities.
242 * @param Entity[] $entities
244 protected function deleteManyJointPermissionsForEntities($entities)
246 if (count($entities) === 0) return;
247 $query = $this->jointPermission->newQuery();
248 foreach ($entities as $entity) {
249 $query->orWhere(function($query) use ($entity) {
250 $query->where('entity_id', '=', $entity->id)
251 ->where('entity_type', '=', $entity->getMorphClass());
258 * Create & Save entity jointPermissions for many entities and jointPermissions.
259 * @param Collection $entities
260 * @param Collection $roles
262 protected function createManyJointPermissions($entities, $roles)
264 $this->readyEntityCache();
265 $jointPermissions = [];
266 foreach ($entities as $entity) {
267 foreach ($roles as $role) {
268 foreach ($this->getActions($entity) as $action) {
269 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
273 $this->jointPermission->insert($jointPermissions);
278 * Get the actions related to an entity.
282 protected function getActions($entity)
284 $baseActions = ['view', 'update', 'delete'];
286 if ($entity->isA('chapter')) {
287 $baseActions[] = 'page-create';
288 } else if ($entity->isA('book')) {
289 $baseActions[] = 'page-create';
290 $baseActions[] = 'chapter-create';
297 * Create entity permission data for an entity and role
298 * for a particular action.
299 * @param Entity $entity
304 protected function createJointPermissionData(Entity $entity, Role $role, $action)
306 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
307 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
308 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
309 $explodedAction = explode('-', $action);
310 $restrictionAction = end($explodedAction);
312 if ($role->system_name === 'admin') {
313 return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
316 if ($entity->isA('book')) {
318 if (!$entity->restricted) {
319 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
321 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
322 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
325 } elseif ($entity->isA('chapter')) {
327 if (!$entity->restricted) {
328 $book = $this->getBook($entity->book_id);
329 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
330 $hasPermissiveAccessToBook = !$book->restricted;
331 return $this->createJointPermissionDataArray($entity, $role, $action,
332 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
333 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
335 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
336 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
339 } elseif ($entity->isA('page')) {
341 if (!$entity->restricted) {
342 $book = $this->getBook($entity->book_id);
343 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
344 $hasPermissiveAccessToBook = !$book->restricted;
346 $chapter = $this->getChapter($entity->chapter_id);
347 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
348 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
349 $acknowledgeChapter = ($chapter && $chapter->restricted);
351 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
352 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
354 return $this->createJointPermissionDataArray($entity, $role, $action,
355 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
356 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
359 $hasAccess = $entity->hasRestriction($role->id, $action);
360 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
367 * Create an array of data with the information of an entity jointPermissions.
368 * Used to build data for bulk insertion.
369 * @param Entity $entity
372 * @param $permissionAll
373 * @param $permissionOwn
376 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
378 $entityClass = get_class($entity);
380 'role_id' => $role->getRawAttribute('id'),
381 'entity_id' => $entity->getRawAttribute('id'),
382 'entity_type' => $entityClass,
384 'has_permission' => $permissionAll,
385 'has_permission_own' => $permissionOwn,
386 'created_by' => $entity->getRawAttribute('created_by')
391 * Checks if an entity has a restriction set upon it.
392 * @param Ownable $ownable
396 public function checkOwnableUserAccess(Ownable $ownable, $permission)
398 if ($this->isAdmin()) {
403 $explodedPermission = explode('-', $permission);
405 $baseQuery = $ownable->where('id', '=', $ownable->id);
406 $action = end($explodedPermission);
407 $this->currentAction = $action;
409 $nonJointPermissions = ['restrictions', 'image', 'attachment'];
411 // Handle non entity specific jointPermissions
412 if (in_array($explodedPermission[0], $nonJointPermissions)) {
413 $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
414 $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
415 $this->currentAction = 'view';
416 $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
417 return ($allPermission || ($isOwner && $ownPermission));
420 // Handle abnormal create jointPermissions
421 if ($action === 'create') {
422 $this->currentAction = $permission;
425 $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
431 * Check if an entity has restrictions set on itself or its
433 * @param Entity $entity
437 public function checkIfRestrictionsSet(Entity $entity, $action)
439 $this->currentAction = $action;
440 if ($entity->isA('page')) {
441 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
442 } elseif ($entity->isA('chapter')) {
443 return $entity->restricted || $entity->book->restricted;
444 } elseif ($entity->isA('book')) {
445 return $entity->restricted;
450 * The general query filter to remove all entities
451 * that the current user does not have access to.
455 protected function entityRestrictionQuery($query)
457 $q = $query->where(function ($parentQuery) {
458 $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
459 $permissionQuery->whereIn('role_id', $this->getRoles())
460 ->where('action', '=', $this->currentAction)
461 ->where(function ($query) {
462 $query->where('has_permission', '=', true)
463 ->orWhere(function ($query) {
464 $query->where('has_permission_own', '=', true)
465 ->where('created_by', '=', $this->currentUser()->id);
475 * Get the children of a book in an efficient single query, Filtered by the permission system.
476 * @param integer $book_id
477 * @param bool $filterDrafts
478 * @return \Illuminate\Database\Query\Builder
480 public function bookChildrenQuery($book_id, $filterDrafts = false) {
481 $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
482 $query->where('draft', '=', 0);
483 if (!$filterDrafts) {
484 $query->orWhere(function($query) {
485 $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
489 $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
490 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
491 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
493 if (!$this->isAdmin()) {
494 $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
495 ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
496 ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
497 ->where(function($query) {
498 $query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
499 $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
502 $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
505 $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
511 * Add restrictions for a generic entity
512 * @param string $entityType
513 * @param Builder|Entity $query
514 * @param string $action
517 public function enforceEntityRestrictions($entityType, $query, $action = 'view')
519 if (strtolower($entityType) === 'page') {
520 // Prevent drafts being visible to others.
521 $query = $query->where(function ($query) {
522 $query->where('draft', '=', false);
523 if ($this->currentUser()) {
524 $query->orWhere(function ($query) {
525 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
531 if ($this->isAdmin()) {
536 $this->currentAction = $action;
537 return $this->entityRestrictionQuery($query);
541 * Filter items that have entities set a a polymorphic relation.
543 * @param string $tableName
544 * @param string $entityIdColumn
545 * @param string $entityTypeColumn
548 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
550 if ($this->isAdmin()) {
555 $this->currentAction = 'view';
556 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
558 $q = $query->where(function ($query) use ($tableDetails) {
559 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
560 $permissionQuery->select('id')->from('joint_permissions')
561 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
562 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
563 ->where('action', '=', $this->currentAction)
564 ->whereIn('role_id', $this->getRoles())
565 ->where(function ($query) {
566 $query->where('has_permission', '=', true)->orWhere(function ($query) {
567 $query->where('has_permission_own', '=', true)
568 ->where('created_by', '=', $this->currentUser()->id);
578 * Filters pages that are a direct relation to another item.
581 * @param $entityIdColumn
584 public function filterRelatedPages($query, $tableName, $entityIdColumn)
586 if ($this->isAdmin()) {
591 $this->currentAction = 'view';
592 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
594 $q = $query->where(function ($query) use ($tableDetails) {
595 $query->where(function ($query) use (&$tableDetails) {
596 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
597 $permissionQuery->select('id')->from('joint_permissions')
598 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
599 ->where('entity_type', '=', 'Bookstack\\Page')
600 ->where('action', '=', $this->currentAction)
601 ->whereIn('role_id', $this->getRoles())
602 ->where(function ($query) {
603 $query->where('has_permission', '=', true)->orWhere(function ($query) {
604 $query->where('has_permission_own', '=', true)
605 ->where('created_by', '=', $this->currentUser()->id);
609 })->orWhere($tableDetails['entityIdColumn'], '=', 0);
616 * Check if the current user is an admin.
619 private function isAdmin()
621 if ($this->isAdminUser === null) {
622 $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
625 return $this->isAdminUser;
629 * Get the current user
632 private function currentUser()
634 if ($this->currentUserModel === false) {
635 $this->currentUserModel = user();
638 return $this->currentUserModel;
642 * Clean the cached user elements.
644 private function clean()
646 $this->currentUserModel = false;
647 $this->userRoles = false;
648 $this->isAdminUser = null;