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->with('jointPermissions')->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->with('jointPermissions')->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 $query = $this->jointPermission->newQuery();
247 foreach ($entities as $entity) {
248 $query->orWhere(function($query) use ($entity) {
249 $query->where('entity_id', '=', $entity->id)
250 ->where('entity_type', '=', $entity->getMorphClass());
257 * Create & Save entity jointPermissions for many entities and jointPermissions.
258 * @param Collection $entities
259 * @param Collection $roles
261 protected function createManyJointPermissions($entities, $roles)
263 $this->readyEntityCache();
264 $jointPermissions = [];
265 foreach ($entities as $entity) {
266 foreach ($roles as $role) {
267 foreach ($this->getActions($entity) as $action) {
268 $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
272 $this->jointPermission->insert($jointPermissions);
277 * Get the actions related to an entity.
281 protected function getActions($entity)
283 $baseActions = ['view', 'update', 'delete'];
285 if ($entity->isA('chapter')) {
286 $baseActions[] = 'page-create';
287 } else if ($entity->isA('book')) {
288 $baseActions[] = 'page-create';
289 $baseActions[] = 'chapter-create';
296 * Create entity permission data for an entity and role
297 * for a particular action.
298 * @param Entity $entity
303 protected function createJointPermissionData(Entity $entity, Role $role, $action)
305 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
306 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
307 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
308 $explodedAction = explode('-', $action);
309 $restrictionAction = end($explodedAction);
311 if ($role->system_name === 'admin') {
312 return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
315 if ($entity->isA('book')) {
317 if (!$entity->restricted) {
318 return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
320 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
321 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
324 } elseif ($entity->isA('chapter')) {
326 if (!$entity->restricted) {
327 $book = $this->getBook($entity->book_id);
328 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
329 $hasPermissiveAccessToBook = !$book->restricted;
330 return $this->createJointPermissionDataArray($entity, $role, $action,
331 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
332 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
334 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
335 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
338 } elseif ($entity->isA('page')) {
340 if (!$entity->restricted) {
341 $book = $this->getBook($entity->book_id);
342 $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
343 $hasPermissiveAccessToBook = !$book->restricted;
345 $chapter = $this->getChapter($entity->chapter_id);
346 $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
347 $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
348 $acknowledgeChapter = ($chapter && $chapter->restricted);
350 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
351 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
353 return $this->createJointPermissionDataArray($entity, $role, $action,
354 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
355 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
358 $hasAccess = $entity->hasRestriction($role->id, $action);
359 return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
366 * Create an array of data with the information of an entity jointPermissions.
367 * Used to build data for bulk insertion.
368 * @param Entity $entity
371 * @param $permissionAll
372 * @param $permissionOwn
375 protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
377 $entityClass = get_class($entity);
379 'role_id' => $role->getRawAttribute('id'),
380 'entity_id' => $entity->getRawAttribute('id'),
381 'entity_type' => $entityClass,
383 'has_permission' => $permissionAll,
384 'has_permission_own' => $permissionOwn,
385 'created_by' => $entity->getRawAttribute('created_by')
390 * Checks if an entity has a restriction set upon it.
391 * @param Ownable $ownable
395 public function checkOwnableUserAccess(Ownable $ownable, $permission)
397 if ($this->isAdmin()) {
402 $explodedPermission = explode('-', $permission);
404 $baseQuery = $ownable->where('id', '=', $ownable->id);
405 $action = end($explodedPermission);
406 $this->currentAction = $action;
408 $nonJointPermissions = ['restrictions'];
410 // Handle non entity specific jointPermissions
411 if (in_array($explodedPermission[0], $nonJointPermissions)) {
412 $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
413 $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
414 $this->currentAction = 'view';
415 $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
416 return ($allPermission || ($isOwner && $ownPermission));
419 // Handle abnormal create jointPermissions
420 if ($action === 'create') {
421 $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);
474 public function bookChildrenQuery($book_id, $filterDrafts = false) {
478 'userId' => $this->currentUser()->id,
479 'bookIdPage' => $book_id,
480 'bookIdChapter' => $book_id
482 if (!$filterDrafts) {
483 $params['userIdDrafts'] = $this->currentUser()->id;
486 $userRoles = $this->getRoles();
489 foreach ($userRoles as $index => $roleId) {
490 $roleBindings[':role'.$index] = $roleId;
491 $roleValues['role'.$index] = $roleId;
493 // TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
494 // Something which will handle the above role crap in a nice clean way
495 $roleBindingString = implode(',', array_keys($roleBindings));
496 $query = "SELECT * from (
497 (SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
498 where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
500 (SELECT 'Bookstack\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft FROM {$this->chapter->getTable()} WHERE book_id = :bookIdChapter)
502 SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
504 jp.entity_id=U.id AND
505 jp.entity_type=U.entity_type AND
506 jp.action = 'view' AND
507 jp.role_id IN ({$roleBindingString}) AND
509 jp.has_permission = 1 OR
510 (jp.has_permission_own = 1 AND jp.created_by = :userId)
513 ORDER BY draft desc, priority asc";
516 return $this->db->select($query, array_replace($roleValues, $params));
520 * Add restrictions for a generic entity
521 * @param string $entityType
522 * @param Builder|Entity $query
523 * @param string $action
526 public function enforceEntityRestrictions($entityType, $query, $action = 'view')
528 if (strtolower($entityType) === 'page') {
529 // Prevent drafts being visible to others.
530 $query = $query->where(function ($query) {
531 $query->where('draft', '=', false);
532 if ($this->currentUser()) {
533 $query->orWhere(function ($query) {
534 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
540 if ($this->isAdmin()) {
545 $this->currentAction = $action;
546 return $this->entityRestrictionQuery($query);
550 * Filter items that have entities set a a polymorphic relation.
552 * @param string $tableName
553 * @param string $entityIdColumn
554 * @param string $entityTypeColumn
557 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
559 if ($this->isAdmin()) {
564 $this->currentAction = 'view';
565 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
567 $q = $query->where(function ($query) use ($tableDetails) {
568 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
569 $permissionQuery->select('id')->from('joint_permissions')
570 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
571 ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
572 ->where('action', '=', $this->currentAction)
573 ->whereIn('role_id', $this->getRoles())
574 ->where(function ($query) {
575 $query->where('has_permission', '=', true)->orWhere(function ($query) {
576 $query->where('has_permission_own', '=', true)
577 ->where('created_by', '=', $this->currentUser()->id);
586 * Filters pages that are a direct relation to another item.
589 * @param $entityIdColumn
592 public function filterRelatedPages($query, $tableName, $entityIdColumn)
594 if ($this->isAdmin()) {
599 $this->currentAction = 'view';
600 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
602 $q = $query->where(function ($query) use ($tableDetails) {
603 $query->where(function ($query) use (&$tableDetails) {
604 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
605 $permissionQuery->select('id')->from('joint_permissions')
606 ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
607 ->where('entity_type', '=', 'Bookstack\\Page')
608 ->where('action', '=', $this->currentAction)
609 ->whereIn('role_id', $this->getRoles())
610 ->where(function ($query) {
611 $query->where('has_permission', '=', true)->orWhere(function ($query) {
612 $query->where('has_permission_own', '=', true)
613 ->where('created_by', '=', $this->currentUser()->id);
617 })->orWhere($tableDetails['entityIdColumn'], '=', 0);
624 * Check if the current user is an admin.
627 private function isAdmin()
629 if ($this->isAdminUser === null) {
630 $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
633 return $this->isAdminUser;
637 * Get the current user
640 private function currentUser()
642 if ($this->currentUserModel === false) {
643 $this->currentUserModel = user();
646 return $this->currentUserModel;
650 * Clean the cached user elements.
652 private function clean()
654 $this->currentUserModel = false;
655 $this->userRoles = false;
656 $this->isAdminUser = null;