1 <?php namespace BookStack\Services;
6 use BookStack\EntityPermission;
10 use Illuminate\Database\Eloquent\Collection;
12 class RestrictionService
17 protected $currentAction;
18 protected $currentUser;
24 protected $entityPermission;
28 * RestrictionService constructor.
29 * @param EntityPermission $entityPermission
31 * @param Chapter $chapter
35 public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role)
37 $this->currentUser = auth()->user();
38 if ($this->currentUser === null) $this->currentUser = new User(['id' => 0]);
39 $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
40 $this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
42 $this->entityPermission = $entityPermission;
45 $this->chapter = $chapter;
50 * Re-generate all entity permission from scratch.
52 public function buildEntityPermissions()
54 $this->entityPermission->truncate();
56 // Get all roles (Should be the most limited dimension)
57 $roles = $this->role->with('permissions')->get();
59 // Chunk through all books
60 $this->book->with('restrictions')->chunk(500, function ($books) use ($roles) {
61 $this->createManyEntityPermissions($books, $roles);
64 // Chunk through all chapters
65 $this->chapter->with('book', 'restrictions')->chunk(500, function ($chapters) use ($roles) {
66 $this->createManyEntityPermissions($chapters, $roles);
69 // Chunk through all pages
70 $this->page->with('book', 'chapter', 'restrictions')->chunk(500, function ($pages) use ($roles) {
71 $this->createManyEntityPermissions($pages, $roles);
76 * Create the entity permissions for a particular entity.
77 * @param Entity $entity
79 public function buildEntityPermissionsForEntity(Entity $entity)
81 $roles = $this->role->with('permissions')->get();
82 $entities = collect([$entity]);
84 if ($entity->isA('book')) {
85 $entities = $entities->merge($entity->chapters);
86 $entities = $entities->merge($entity->pages);
87 } elseif ($entity->isA('chapter')) {
88 $entities = $entities->merge($entity->pages);
91 $this->deleteManyEntityPermissionsForEntities($entities);
92 $this->createManyEntityPermissions($entities, $roles);
96 * Build the entity permissions for a particular role.
99 public function buildEntityPermissionForRole(Role $role)
101 $roles = collect([$role]);
103 $this->deleteManyEntityPermissionsForRoles($roles);
105 // Chunk through all books
106 $this->book->with('restrictions')->chunk(500, function ($books) use ($roles) {
107 $this->createManyEntityPermissions($books, $roles);
110 // Chunk through all chapters
111 $this->chapter->with('book', 'restrictions')->chunk(500, function ($books) use ($roles) {
112 $this->createManyEntityPermissions($books, $roles);
115 // Chunk through all pages
116 $this->page->with('book', 'chapter', 'restrictions')->chunk(500, function ($books) use ($roles) {
117 $this->createManyEntityPermissions($books, $roles);
122 * Delete the entity permissions attached to a particular role.
125 public function deleteEntityPermissionsForRole(Role $role)
127 $this->deleteManyEntityPermissionsForRoles([$role]);
131 * Delete all of the entity permissions for a list of entities.
132 * @param Role[] $roles
134 protected function deleteManyEntityPermissionsForRoles($roles)
136 foreach ($roles as $role) {
137 $role->entityPermissions()->delete();
142 * Delete the entity permissions for a particular entity.
143 * @param Entity $entity
145 public function deleteEntityPermissionsForEntity(Entity $entity)
147 $this->deleteManyEntityPermissionsForEntities([$entity]);
151 * Delete all of the entity permissions for a list of entities.
152 * @param Entity[] $entities
154 protected function deleteManyEntityPermissionsForEntities($entities)
156 foreach ($entities as $entity) {
157 $entity->permissions()->delete();
162 * Create & Save entity permissions for many entities and permissions.
163 * @param Collection $entities
164 * @param Collection $roles
166 protected function createManyEntityPermissions($entities, $roles)
168 $entityPermissions = [];
169 foreach ($entities as $entity) {
170 foreach ($roles as $role) {
171 foreach ($this->getActions($entity) as $action) {
172 $entityPermissions[] = $this->createEntityPermissionData($entity, $role, $action);
176 $this->entityPermission->insert($entityPermissions);
181 * Get the actions related to an entity.
185 protected function getActions($entity)
187 $baseActions = ['view', 'update', 'delete'];
189 if ($entity->isA('chapter')) {
190 $baseActions[] = 'page-create';
191 } else if ($entity->isA('book')) {
192 $baseActions[] = 'page-create';
193 $baseActions[] = 'chapter-create';
200 * Create entity permission data for an entity and role
201 * for a particular action.
202 * @param Entity $entity
207 protected function createEntityPermissionData(Entity $entity, Role $role, $action)
209 $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
210 $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
211 $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
212 $explodedAction = explode('-', $action);
213 $restrictionAction = end($explodedAction);
215 if ($entity->isA('book')) {
217 if (!$entity->restricted) {
218 return $this->createEntityPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
220 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
221 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
224 } elseif ($entity->isA('chapter')) {
226 if (!$entity->restricted) {
227 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
228 $hasPermissiveAccessToBook = !$entity->book->restricted;
229 return $this->createEntityPermissionDataArray($entity, $role, $action,
230 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
231 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
233 $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
234 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
237 } elseif ($entity->isA('page')) {
239 if (!$entity->restricted) {
240 $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
241 $hasPermissiveAccessToBook = !$entity->book->restricted;
242 $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
243 $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
244 $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
246 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
247 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
249 return $this->createEntityPermissionDataArray($entity, $role, $action,
250 ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
251 ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
254 $hasAccess = $entity->hasRestriction($role->id, $action);
255 return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
262 * Create an array of data with the information of an entity permissions.
263 * Used to build data for bulk insertion.
264 * @param Entity $entity
267 * @param $permissionAll
268 * @param $permissionOwn
271 protected function createEntityPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
273 $entityClass = get_class($entity);
275 'role_id' => $role->getRawAttribute('id'),
276 'entity_id' => $entity->getRawAttribute('id'),
277 'entity_type' => $entityClass,
279 'has_permission' => $permissionAll,
280 'has_permission_own' => $permissionOwn,
281 'created_by' => $entity->getRawAttribute('created_by')
286 * Checks if an entity has a restriction set upon it.
287 * @param Entity $entity
291 public function checkEntityUserAccess(Entity $entity, $permission)
293 if ($this->isAdmin) return true;
294 $explodedPermission = explode('-', $permission);
296 $baseQuery = $entity->where('id', '=', $entity->id);
297 $action = end($explodedPermission);
298 $this->currentAction = $action;
300 $nonEntityPermissions = ['restrictions'];
302 // Handle non entity specific permissions
303 if (in_array($explodedPermission[0], $nonEntityPermissions)) {
304 $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
305 $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
306 $this->currentAction = 'view';
307 $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
308 return ($allPermission || ($isOwner && $ownPermission));
311 // Handle abnormal create permissions
312 if ($action === 'create') {
313 $this->currentAction = $permission;
317 return $this->entityRestrictionQuery($baseQuery)->count() > 0;
321 * Check if an entity has restrictions set on itself or its
323 * @param Entity $entity
327 public function checkIfRestrictionsSet(Entity $entity, $action)
329 $this->currentAction = $action;
330 if ($entity->isA('page')) {
331 return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
332 } elseif ($entity->isA('chapter')) {
333 return $entity->restricted || $entity->book->restricted;
334 } elseif ($entity->isA('book')) {
335 return $entity->restricted;
340 * The general query filter to remove all entities
341 * that the current user does not have access to.
345 protected function entityRestrictionQuery($query)
347 return $query->where(function ($parentQuery) {
348 $parentQuery->whereHas('permissions', function ($permissionQuery) {
349 $permissionQuery->whereIn('role_id', $this->userRoles)
350 ->where('action', '=', $this->currentAction)
351 ->where(function ($query) {
352 $query->where('has_permission', '=', true)
353 ->orWhere(function ($query) {
354 $query->where('has_permission_own', '=', true)
355 ->where('created_by', '=', $this->currentUser->id);
363 * Add restrictions for a page query
365 * @param string $action
368 public function enforcePageRestrictions($query, $action = 'view')
370 // Prevent drafts being visible to others.
371 $query = $query->where(function ($query) {
372 $query->where('draft', '=', false);
373 if ($this->currentUser) {
374 $query->orWhere(function ($query) {
375 $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
380 if ($this->isAdmin) return $query;
381 $this->currentAction = $action;
382 return $this->entityRestrictionQuery($query);
386 * Add on permission restrictions to a chapter query.
388 * @param string $action
391 public function enforceChapterRestrictions($query, $action = 'view')
393 if ($this->isAdmin) return $query;
394 $this->currentAction = $action;
395 return $this->entityRestrictionQuery($query);
399 * Add restrictions to a book query.
401 * @param string $action
404 public function enforceBookRestrictions($query, $action = 'view')
406 if ($this->isAdmin) return $query;
407 $this->currentAction = $action;
408 return $this->entityRestrictionQuery($query);
412 * Filter items that have entities set a a polymorphic relation.
414 * @param string $tableName
415 * @param string $entityIdColumn
416 * @param string $entityTypeColumn
419 public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
421 if ($this->isAdmin) return $query;
422 $this->currentAction = 'view';
423 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
425 return $query->where(function ($query) use ($tableDetails) {
426 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
427 $permissionQuery->select('id')->from('entity_permissions')
428 ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
429 ->whereRaw('entity_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
430 ->where('action', '=', $this->currentAction)
431 ->whereIn('role_id', $this->userRoles)
432 ->where(function ($query) {
433 $query->where('has_permission', '=', true)->orWhere(function ($query) {
434 $query->where('has_permission_own', '=', true)
435 ->where('created_by', '=', $this->currentUser->id);
444 * Filters pages that are a direct relation to another item.
447 * @param $entityIdColumn
450 public function filterRelatedPages($query, $tableName, $entityIdColumn)
452 if ($this->isAdmin) return $query;
453 $this->currentAction = 'view';
454 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
456 return $query->where(function ($query) use ($tableDetails) {
457 $query->where(function ($query) use (&$tableDetails) {
458 $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
459 $permissionQuery->select('id')->from('entity_permissions')
460 ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
461 ->where('entity_type', '=', 'Bookstack\\Page')
462 ->where('action', '=', $this->currentAction)
463 ->whereIn('role_id', $this->userRoles)
464 ->where(function ($query) {
465 $query->where('has_permission', '=', true)->orWhere(function ($query) {
466 $query->where('has_permission_own', '=', true)
467 ->where('created_by', '=', $this->currentUser->id);
471 })->orWhere($tableDetails['entityIdColumn'], '=', 0);