*/
public function hasRestriction($role_id, $action)
{
- return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
+ return $this->restrictions()->where('role_id', '=', $role_id)
+ ->where('action', '=', $action)->count() > 0;
+ }
+
+ /**
+ * Check if this entity has live (active) restrictions in place.
+ * @param $role_id
+ * @param $action
+ * @return bool
+ */
+ public function hasActiveRestriction($role_id, $action)
+ {
+ return $this->restricted && $this->restrictions()
+ ->where('role_id', '=', $role_id)->where('action', '=', $action)->count() > 0;
}
/**
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
- $book = $this->bookRepo->newFromInput($request->all());
- $book->slug = $this->bookRepo->findSuitableSlug($book->name);
- $book->created_by = Auth::user()->id;
- $book->updated_by = Auth::user()->id;
- $book->save();
+ $book = $this->bookRepo->createFromInput($request->all());
Activity::add($book, 'book_create', $book->id);
return redirect($book->getUrl());
}
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
- $book->fill($request->all());
- $book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
- $book->updated_by = Auth::user()->id;
- $book->save();
+ $book = $this->bookRepo->updateFromInput($book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
// Add activity for books
foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId);
+ $this->bookRepo->updateBookPermissions($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book);
- $this->bookRepo->destroyBySlug($bookSlug);
+ $this->bookRepo->destroy($book);
return redirect('/books');
}
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
- $chapter = $this->chapterRepo->newFromInput($request->all());
- $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
- $chapter->priority = $this->bookRepo->getNewPriority($book);
- $chapter->created_by = auth()->user()->id;
- $chapter->updated_by = auth()->user()->id;
- $book->chapters()->save($chapter);
+ $input = $request->all();
+ $input['priority'] = $this->bookRepo->getNewPriority($book);
+ $chapter = $this->chapterRepo->createFromInput($request->all(), $book);
Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl());
}
use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
+use BookStack\Services\RestrictionService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
-<?php
-
-namespace BookStack;
+<?php namespace BookStack;
use Illuminate\Database\Eloquent\Model;
/**
* Get the permission object by name.
- * @param $roleName
+ * @param $name
* @return mixed
*/
public static function getByName($name)
<?php namespace BookStack\Repos;
+use Alpha\B;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Book;
/**
* Get a new book instance from request input.
+ * @param array $input
+ * @return Book
+ */
+ public function createFromInput($input)
+ {
+ $book = $this->book->newInstance($input);
+ $book->slug = $this->findSuitableSlug($book->name);
+ $book->created_by = auth()->user()->id;
+ $book->updated_by = auth()->user()->id;
+ $book->save();
+ $this->restrictionService->buildEntityPermissionsForEntity($book);
+ return $book;
+ }
+
+ /**
+ * Update the given book from user input.
+ * @param Book $book
* @param $input
* @return Book
*/
- public function newFromInput($input)
+ public function updateFromInput(Book $book, $input)
{
- return $this->book->newInstance($input);
+ $book->fill($input);
+ $book->slug = $this->findSuitableSlug($book->name, $book->id);
+ $book->updated_by = auth()->user()->id;
+ $book->save();
+ $this->restrictionService->buildEntityPermissionsForEntity($book);
+ return $book;
}
/**
- * Destroy a book identified by the given slug.
- * @param $bookSlug
+ * Destroy the given book.
+ * @param Book $book
+ * @throws \Exception
*/
- public function destroyBySlug($bookSlug)
+ public function destroy(Book $book)
{
- $book = $this->getBySlug($bookSlug);
foreach ($book->pages as $page) {
$this->pageRepo->destroy($page);
}
}
$book->views()->delete();
$book->restrictions()->delete();
+ $this->restrictionService->deleteEntityPermissionsForEntity($book);
$book->delete();
}
+ /**
+ * Alias method to update the book permissions in the RestrictionService.
+ * @param Book $book
+ */
+ public function updateBookPermissions(Book $book)
+ {
+ $this->restrictionService->buildEntityPermissionsForEntity($book);
+ }
+
/**
* Get the next child element priority.
* @param Book $book
use Activity;
+use BookStack\Book;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Chapter;
/**
* Create a new chapter from request input.
* @param $input
- * @return $this
+ * @param Book $book
+ * @return Chapter
*/
- public function newFromInput($input)
+ public function createFromInput($input, Book $book)
{
- return $this->chapter->fill($input);
+ $chapter = $this->chapter->newInstance($input);
+ $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
+ $chapter->created_by = auth()->user()->id;
+ $chapter->updated_by = auth()->user()->id;
+ $chapter = $book->chapters()->save($chapter);
+ $this->restrictionService->buildEntityPermissionsForEntity($chapter);
+ return $chapter;
}
/**
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->restrictions()->delete();
+ $this->restrictionService->deleteEntityPermissionsForEntity($chapter);
$chapter->delete();
}
}
}
$entity->save();
+ $this->restrictionService->buildEntityPermissionsForEntity($entity);
}
/**
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
+ $this->restrictionService->buildEntityPermissionsForEntity($page);
return $page;
}
$page->views()->delete();
$page->revisions()->delete();
$page->restrictions()->delete();
+ $this->restrictionService->deleteEntityPermissionsForEntity($page);
$page->delete();
}
use BookStack\Exceptions\PermissionsException;
use BookStack\Permission;
use BookStack\Role;
+use BookStack\Services\RestrictionService;
use Setting;
class PermissionsRepo
protected $permission;
protected $role;
+ protected $restrictionService;
/**
* PermissionsRepo constructor.
- * @param $permission
- * @param $role
+ * @param Permission $permission
+ * @param Role $role
+ * @param RestrictionService $restrictionService
*/
- public function __construct(Permission $permission, Role $role)
+ public function __construct(Permission $permission, Role $role, RestrictionService $restrictionService)
{
$this->permission = $permission;
$this->role = $role;
+ $this->restrictionService = $restrictionService;
}
/**
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
+ $this->restrictionService->buildEntityPermissionForRole($role);
return $role;
}
$role->fill($roleData);
$role->save();
+ $this->restrictionService->buildEntityPermissionForRole($role);
}
/**
}
}
+ $this->restrictionService->deleteEntityPermissionsForRole($role);
$role->delete();
}
return $this->belongsToMany('BookStack\User');
}
+ /**
+ * Get all related entity permissions.
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function entityPermissions()
+ {
+ return $this->hasMany(EntityPermission::class);
+ }
+
/**
* The permissions that belong to the role.
*/
*/
public function entityActivity($entity, $count = 20, $page = 0)
{
- $activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
- ->skip($count * $page)->take($count)->get();
+ if ($entity->isA('book')) {
+ $query = $this->activity->where('book_id', '=', $entity->id);
+ } else {
+ $query = $this->activity->where('entity_type', '=', get_class($entity))
+ ->where('entity_id', '=', $entity->id);
+ }
+
+ $activity = $this->restrictionService
+ ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
+ ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activity);
}
protected $entityPermission;
protected $role;
+ /**
+ * The actions that have permissions attached throughout the application.
+ * @var array
+ */
protected $actions = ['view', 'create', 'update', 'delete'];
/**
* RestrictionService constructor.
- * TODO - Handle events when roles or entities change.
* @param EntityPermission $entityPermission
* @param Book $book
* @param Chapter $chapter
});
}
+ /**
+ * Create the entity permissions for a particular entity.
+ * @param Entity $entity
+ */
+ public function buildEntityPermissionsForEntity(Entity $entity)
+ {
+ $roles = $this->role->load('permissions')->all();
+ $entities = collect([$entity]);
+
+ if ($entity->isA('book')) {
+ $entities = $entities->merge($entity->chapters);
+ $entities = $entities->merge($entity->pages);
+ } elseif ($entity->isA('chapter')) {
+ $entities = $entities->merge($entity->pages);
+ }
+
+ $this->deleteManyEntityPermissionsForEntities($entities);
+ $this->createManyEntityPermissions($entities, $roles);
+ }
+
+ /**
+ * Build the entity permissions for a particular role.
+ * @param Role $role
+ */
+ public function buildEntityPermissionForRole(Role $role)
+ {
+ $roles = collect([$role]);
+
+ $this->deleteManyEntityPermissionsForRoles($roles);
+
+ // Chunk through all books
+ $this->book->chunk(500, function ($books) use ($roles) {
+ $this->createManyEntityPermissions($books, $roles);
+ });
+
+ // Chunk through all chapters
+ $this->chapter->with('book')->chunk(500, function ($books) use ($roles) {
+ $this->createManyEntityPermissions($books, $roles);
+ });
+
+ // Chunk through all pages
+ $this->page->with('book', 'chapter')->chunk(500, function ($books) use ($roles) {
+ $this->createManyEntityPermissions($books, $roles);
+ });
+ }
+
+ /**
+ * Delete the entity permissions attached to a particular role.
+ * @param Role $role
+ */
+ public function deleteEntityPermissionsForRole(Role $role)
+ {
+ $this->deleteManyEntityPermissionsForRoles([$role]);
+ }
+
+ /**
+ * Delete all of the entity permissions for a list of entities.
+ * @param Role[] $roles
+ */
+ protected function deleteManyEntityPermissionsForRoles($roles)
+ {
+ foreach ($roles as $role) {
+ $role->entityPermissions()->delete();
+ }
+ }
+
+ /**
+ * Delete the entity permissions for a particular entity.
+ * @param Entity $entity
+ */
+ public function deleteEntityPermissionsForEntity(Entity $entity)
+ {
+ $this->deleteManyEntityPermissionsForEntities([$entity]);
+ }
+
+ /**
+ * Delete all of the entity permissions for a list of entities.
+ * @param Entity[] $entities
+ */
+ protected function deleteManyEntityPermissionsForEntities($entities)
+ {
+ foreach ($entities as $entity) {
+ $entity->permissions()->delete();
+ }
+ }
+
/**
* Create & Save entity permissions for many entities and permissions.
* @param Collection $entities
}
}
}
+ \Log::info(collect($entityPermissions)->where('entity_id', 1)->where('entity_type', 'BookStack\\Page')->where('role_id', 2)->all());
$this->entityPermission->insert($entityPermissions);
}
-
+ /**
+ * Create entity permission data for an entity and role
+ * for a particular action.
+ * @param Entity $entity
+ * @param Role $role
+ * @param $action
+ * @return array
+ */
protected function createEntityPermissionData(Entity $entity, Role $role, $action)
{
$permissionPrefix = $entity->getType() . '-' . $action;
if (!$entity->restricted) {
return $this->createEntityPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} else {
- $hasAccess = $entity->hasRestriction($role->id, $action);
+ $hasAccess = $entity->hasActiveRestriction($role->id, $action);
return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
- $hasAccessToBook = $entity->book->hasRestriction($role->id, $action);
+ $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $action);
+ $hasPermissiveAccessToBook = !$entity->book->restricted;
return $this->createEntityPermissionDataArray($entity, $role, $action,
- ($roleHasPermission && $hasAccessToBook), ($roleHasPermissionOwn && $hasAccessToBook));
+ ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
+ ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
} else {
- $hasAccess = $entity->hasRestriction($role->id, $action);
+ $hasAccess = $entity->hasActiveRestriction($role->id, $action);
return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
- $hasAccessToBook = $entity->book->hasRestriction($role->id, $action);
- $hasAccessToChapter = $entity->chapter ? ($entity->chapter->hasRestriction($role->id, $action)) : true;
+ $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $action);
+ $hasPermissiveAccessToBook = !$entity->book->restricted;
+ $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $action);
+ $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
+ $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
+
+ $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
+ $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
+
return $this->createEntityPermissionDataArray($entity, $role, $action,
- ($roleHasPermission && $hasAccessToBook && $hasAccessToChapter),
- ($roleHasPermissionOwn && $hasAccessToBook && $hasAccessToChapter));
+ ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
+ ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
+ );
} else {
$hasAccess = $entity->hasRestriction($role->id, $action);
return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
}
+ /**
+ * Create an array of data with the information of an entity permissions.
+ * Used to build data for bulk insertion.
+ * @param Entity $entity
+ * @param Role $role
+ * @param $action
+ * @param $permissionAll
+ * @param $permissionOwn
+ * @return array
+ */
protected function createEntityPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
{
$entityClass = get_class($entity);
/**
* Checks if an entity has a restriction set upon it.
* @param Entity $entity
- * @param $action
+ * @param $permission
* @return bool
*/
- public function checkIfEntityRestricted(Entity $entity, $action)
+ public function checkEntityUserAccess(Entity $entity, $permission)
{
if ($this->isAdmin) return true;
- $this->currentAction = $action;
+ $explodedPermission = explode('-', $permission);
+
$baseQuery = $entity->where('id', '=', $entity->id);
- if ($entity->isA('page')) {
- return $this->pageRestrictionQuery($baseQuery)->count() > 0;
- } elseif ($entity->isA('chapter')) {
- return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
- } elseif ($entity->isA('book')) {
- return $this->bookRestrictionQuery($baseQuery)->count() > 0;
+
+ $nonEntityPermissions = ['restrictions'];
+
+ // Handle non entity specific permissions
+ if (in_array($explodedPermission[0], $nonEntityPermissions)) {
+ $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
+ $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
+ $this->currentAction = 'view';
+ $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by;
+ return ($allPermission || ($isOwner && $ownPermission));
}
- return false;
+
+ $action = end($explodedPermission);
+ $this->currentAction = $action;
+ return $this->entityRestrictionQuery($baseQuery)->count() > 0;
}
/**
}
}
- /**
- * Add restrictions for a page query
- * @param $query
- * @param string $action
- * @return mixed
- */
- public function enforcePageRestrictions($query, $action = 'view')
- {
- // Prevent drafts being visible to others.
- $query = $query->where(function ($query) {
- $query->where('draft', '=', false);
- if ($this->currentUser) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
- });
- }
- });
-
- if ($this->isAdmin) return $query;
- $this->currentAction = $action;
- return $this->entityRestrictionQuery($query);
- }
-
/**
* The general query filter to remove all entities
* that the current user does not have access to.
});
}
+ /**
+ * Add restrictions for a page query
+ * @param $query
+ * @param string $action
+ * @return mixed
+ */
+ public function enforcePageRestrictions($query, $action = 'view')
+ {
+ // Prevent drafts being visible to others.
+ $query = $query->where(function ($query) {
+ $query->where('draft', '=', false);
+ if ($this->currentUser) {
+ $query->orWhere(function ($query) {
+ $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
+ });
+ }
+ });
+
+ if ($this->isAdmin) return $query;
+ $this->currentAction = $action;
+ return $this->entityRestrictionQuery($query);
+ }
+
/**
* Add on permission restrictions to a chapter query.
* @param $query
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
- ->where('created_by', '=', $this->currentUser->id);
+ ->where('created_by', '=', $this->currentUser ? $this->currentUser->id : 0);
});
});
});
}
// Check permission on ownable item
- $permissionBaseName = strtolower($permission) . '-';
- $hasPermission = false;
- if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
- if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
-
- if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
-
- // Check restrictions on the entity
$restrictionService = app('BookStack\Services\RestrictionService');
- $explodedPermission = explode('-', $permission);
- $action = end($explodedPermission);
- $hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
- $restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
- return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
+ return $restrictionService->checkEntityUserAccess($ownable, $permission);
}
/**
$newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
$newPermission->display_name = $op . ' ' . $entity . 's';
$newPermission->save();
+ // Assign view permissions to all current roles
foreach ($currentRoles as $role) {
$role->attachPermission($newPermission);
}
{
protected $user;
protected $viewer;
+ protected $restrictionService;
public function setUp()
{
parent::setUp();
$this->user = $this->getNewUser();
$this->viewer = $this->getViewer();
+ $this->restrictionService = $this->app[\BookStack\Services\RestrictionService::class];
}
protected function getViewer()
}
$entity->save();
$entity->load('restrictions');
+ $this->restrictionService->buildEntityPermissionsForEntity($entity);
+ $entity->load('permissions');
}
public function test_book_view_restriction()
public function setUp()
{
parent::setUp();
- $this->user = $this->getNewBlankUser();
+ $this->user = $this->getViewer();
+ }
+
+ protected function getViewer()
+ {
+ $role = \BookStack\Role::getRole('viewer');
+ $viewer = $this->getNewBlankUser();
+ $viewer->attachRole($role);;
+ return $viewer;
}
/**
public function test_restrictions_manage_own_permission()
{
- $otherUsersPage = \BookStack\Page::take(1)->get()->first();
+ $otherUsersPage = \BookStack\Page::first();
$content = $this->createEntityChainBelongingToUser($this->user);
// Check can't restrict other's content
$this->actingAs($this->user)->visit($otherUsersPage->getUrl())
$page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
$book->chapters()->saveMany([$chapter]);
$chapter->pages()->saveMany([$page]);
+ $restrictionService = $this->app[\BookStack\Services\RestrictionService::class];
+ $restrictionService->buildEntityPermissionsForEntity($book);
return [
'book' => $book,
'chapter' => $chapter,