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 * @param bool $fetchPageContent
479 * @return \Illuminate\Database\Query\Builder
481 public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
482 $pageContentSelect = $fetchPageContent ? 'html' : "''";
483 $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
484 $query->where('draft', '=', 0);
485 if (!$filterDrafts) {
486 $query->orWhere(function($query) {
487 $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
491 $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);
492 $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
493 ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
495 if (!$this->isAdmin()) {
496 $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
497 ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
498 ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
499 ->where(function($query) {
500 $query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
501 $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
504 $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
507 $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
513 * Add restrictions for a generic entity
514 * @param string $entityType
515 * @param Builder|Entity $query
516 * @param string $action
519 public function enforceEntityRestrictions($entityType, $query, $action = 'view')
521 if (strtolower($entityType) === 'page') {
522 // Prevent drafts being visible to others.
523 $query = $query->where(function ($query) {
524 $query->where('draft', '=', false);
525 if ($this->currentUser()) {
526 $query->orWhere(function ($query) {
527 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
533 if ($this->isAdmin()) {
538 $this->currentAction = $action;
539 return $this->entityRestrictionQuery($query);
543 * Filter items that have entities set a a polymorphic relation.
545 * @param string $tableName
546 * @param string $entityIdColumn
547 * @param string $entityTypeColumn
550 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
552 if ($this->isAdmin()) {
557 $this->currentAction = 'view';
558 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
560 $q = $query->where(function ($query) use ($tableDetails) {
561 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
562 $permissionQuery->select('id')->from('joint_permissions')
563 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
564 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
565 ->where('action', '=', $this->currentAction)
566 ->whereIn('role_id', $this->getRoles())
567 ->where(function ($query) {
568 $query->where('has_permission', '=', true)->orWhere(function ($query) {
569 $query->where('has_permission_own', '=', true)
570 ->where('created_by', '=', $this->currentUser()->id);
580 * Filters pages that are a direct relation to another item.
583 * @param $entityIdColumn
586 public function filterRelatedPages($query, $tableName, $entityIdColumn)
588 if ($this->isAdmin()) {
593 $this->currentAction = 'view';
594 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
596 $q = $query->where(function ($query) use ($tableDetails) {
597 $query->where(function ($query) use (&$tableDetails) {
598 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
599 $permissionQuery->select('id')->from('joint_permissions')
600 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
601 ->where('entity_type', '=', 'Bookstack\\Page')
602 ->where('action', '=', $this->currentAction)
603 ->whereIn('role_id', $this->getRoles())
604 ->where(function ($query) {
605 $query->where('has_permission', '=', true)->orWhere(function ($query) {
606 $query->where('has_permission_own', '=', true)
607 ->where('created_by', '=', $this->currentUser()->id);
611 })->orWhere($tableDetails['entityIdColumn'], '=', 0);
618 * Check if the current user is an admin.
621 private function isAdmin()
623 if ($this->isAdminUser === null) {
624 $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
627 return $this->isAdminUser;
631 * Get the current user
634 private function currentUser()
636 if ($this->currentUserModel === false) {
637 $this->currentUserModel = user();
640 return $this->currentUserModel;
644 * Clean the cached user elements.
646 private function clean()
648 $this->currentUserModel = false;
649 $this->userRoles = false;
650 $this->isAdminUser = null;