Done a refactor pass on PermissionService

Could do with splitting out into seperate query/build classess really.
Closes #2633.
This commit is contained in:
Dan Brown 2021-03-14 19:52:07 +00:00
parent a644f64c6b
commit 1e5951a75f
6 changed files with 147 additions and 247 deletions

View File

@ -78,7 +78,7 @@ class ActivityService
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
$activityList = $this->permissionService $activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->with(['user', 'entity']) ->with(['user', 'entity'])
->skip($count * $page) ->skip($count * $page)
@ -131,7 +131,7 @@ class ActivityService
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
$activityList = $this->permissionService $activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id) ->where('user_id', '=', $user->id)
->skip($count * $page) ->skip($count * $page)

View File

@ -26,7 +26,9 @@ class TagRepo
*/ */
public function getNameSuggestions(?string $searchTerm): Collection public function getNameSuggestions(?string $searchTerm): Collection
{ {
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name'); $query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('name');
if ($searchTerm) { if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc'); $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@ -45,7 +47,9 @@ class TagRepo
*/ */
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
{ {
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value'); $query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('value');
if ($searchTerm) { if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc'); $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');

View File

@ -65,7 +65,7 @@ class ViewService
{ {
$skipCount = $count * $page; $skipCount = $count * $page;
$query = $this->permissionService $query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action) ->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type') ->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc'); ->orderBy('view_count', 'desc');

View File

@ -1,27 +1,33 @@
<?php namespace BookStack\Auth\Permissions; <?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Model; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater; use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner; use BookStack\Traits\HasOwner;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection; use Throwable;
class PermissionService class PermissionService
{ {
/**
* @var ?array
*/
protected $userRoles = null;
protected $currentAction; /**
protected $isAdminUser; * @var ?User
protected $userRoles = false; */
protected $currentUserModel = false; protected $currentUserModel = null;
/** /**
* @var Connection * @var Connection
@ -29,47 +35,20 @@ class PermissionService
protected $db; protected $db;
/** /**
* @var JointPermission * @var array
*/ */
protected $jointPermission;
/**
* @var Role
*/
protected $role;
/**
* @var EntityPermission
*/
protected $entityPermission;
/**
* @var EntityProvider
*/
protected $entityProvider;
protected $entityCache; protected $entityCache;
/** /**
* PermissionService constructor. * PermissionService constructor.
*/ */
public function __construct( public function __construct(Connection $db)
JointPermission $jointPermission, {
Permissions\EntityPermission $entityPermission,
Role $role,
Connection $db,
EntityProvider $entityProvider
) {
$this->db = $db; $this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->entityProvider = $entityProvider;
} }
/** /**
* Set the database connection * Set the database connection
* @param Connection $connection
*/ */
public function setConnection(Connection $connection) public function setConnection(Connection $connection)
{ {
@ -78,38 +57,31 @@ class PermissionService
/** /**
* Prepare the local entity cache and ensure it's empty * Prepare the local entity cache and ensure it's empty
* @param \BookStack\Entities\Models\Entity[] $entities * @param Entity[] $entities
*/ */
protected function readyEntityCache($entities = []) protected function readyEntityCache(array $entities = [])
{ {
$this->entityCache = []; $this->entityCache = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
$type = $entity->getType(); $class = get_class($entity);
if (!isset($this->entityCache[$type])) { if (!isset($this->entityCache[$class])) {
$this->entityCache[$type] = collect(); $this->entityCache[$class] = collect();
} }
$this->entityCache[$type]->put($entity->id, $entity); $this->entityCache[$class]->put($entity->id, $entity);
} }
} }
/** /**
* Get a book via ID, Checks local cache * Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/ */
protected function getBook($bookId) protected function getBook(int $bookId): ?Book
{ {
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) { if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
return $this->entityCache['book']->get($bookId); return $this->entityCache[Book::class]->get($bookId);
} }
$book = $this->entityProvider->book->find($bookId); return Book::query()->withTrashed()->find($bookId);
if ($book === null) {
$book = false;
}
return $book;
} }
/** /**
@ -117,37 +89,31 @@ class PermissionService
*/ */
protected function getChapter(int $chapterId): ?Chapter protected function getChapter(int $chapterId): ?Chapter
{ {
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) { if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
return $this->entityCache['chapter']->get($chapterId); return $this->entityCache[Chapter::class]->get($chapterId);
} }
return $this->entityProvider->chapter->newQuery() return Chapter::query()
->withTrashed() ->withTrashed()
->find($chapterId); ->find($chapterId);
} }
/** /**
* Get the roles for the current user; * Get the roles for the current logged in user.
* @return array|bool
*/ */
protected function getRoles() protected function getCurrentUserRoles(): array
{ {
if ($this->userRoles !== false) { if (!is_null($this->userRoles)) {
return $this->userRoles; return $this->userRoles;
} }
$roles = [];
if (auth()->guest()) { if (auth()->guest()) {
$roles[] = $this->role->getSystemRole('public')->id; $this->userRoles = [Role::getSystemRole('public')->id];
return $roles; } else {
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
} }
return $this->userRoles;
foreach ($this->currentUser()->roles as $role) {
$roles[] = $role->id;
}
return $roles;
} }
/** /**
@ -155,59 +121,57 @@ class PermissionService
*/ */
public function buildJointPermissions() public function buildJointPermissions()
{ {
$this->jointPermission->truncate(); JointPermission::query()->truncate();
$this->readyEntityCache(); $this->readyEntityCache();
// Get all roles (Should be the most limited dimension) // Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get()->all(); $roles = Role::query()->with('permissions')->get()->all();
// Chunk through all books // Chunk through all books
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles); $this->buildJointPermissionsForBooks($books, $roles);
}); });
// Chunk through all bookshelves // Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by']) Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) { ->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles); $this->buildJointPermissionsForShelves($shelves, $roles);
}); });
} }
/** /**
* Get a query for fetching a book with it's children. * Get a query for fetching a book with it's children.
* @return QueryBuilder
*/ */
protected function bookFetchQuery() protected function bookFetchQuery(): Builder
{ {
return $this->entityProvider->book->withTrashed()->newQuery() return Book::query()->withTrashed()
->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) { ->select(['id', 'restricted', 'owned_by'])->with([
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']); 'chapters' => function ($query) {
}, 'pages' => function ($query) { $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']); },
}]); 'pages' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
}
]);
} }
/** /**
* @param Collection $shelves * Build joint permissions for the given shelf and role combinations.
* @param array $roles * @throws Throwable
* @param bool $deleteOld
* @throws \Throwable
*/ */
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false) protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
{ {
if ($deleteOld) { if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all()); $this->deleteManyJointPermissionsForEntities($shelves->all());
} }
$this->createManyJointPermissions($shelves, $roles); $this->createManyJointPermissions($shelves->all(), $roles);
} }
/** /**
* Build joint permissions for an array of books * Build joint permissions for the given book and role combinations.
* @param Collection $books * @throws Throwable
* @param array $roles
* @param bool $deleteOld
*/ */
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
{ {
$entities = clone $books; $entities = clone $books;
@ -224,55 +188,53 @@ class PermissionService
if ($deleteOld) { if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all()); $this->deleteManyJointPermissionsForEntities($entities->all());
} }
$this->createManyJointPermissions($entities, $roles); $this->createManyJointPermissions($entities->all(), $roles);
} }
/** /**
* Rebuild the entity jointPermissions for a particular entity. * Rebuild the entity jointPermissions for a particular entity.
* @param \BookStack\Entities\Models\Entity $entity * @throws Throwable
* @throws \Throwable
*/ */
public function buildJointPermissionsForEntity(Entity $entity) public function buildJointPermissionsForEntity(Entity $entity)
{ {
$entities = [$entity]; $entities = [$entity];
if ($entity->isA('book')) { if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get(); $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true); $this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
return; return;
} }
/** @var BookChild $entity */
if ($entity->book) { if ($entity->book) {
$entities[] = $entity->book; $entities[] = $entity->book;
} }
if ($entity->isA('page') && $entity->chapter_id) { if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter; $entities[] = $entity->chapter;
} }
if ($entity->isA('chapter')) { if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) { foreach ($entity->pages as $page) {
$entities[] = $page; $entities[] = $page;
} }
} }
$this->buildJointPermissionsForEntities(collect($entities)); $this->buildJointPermissionsForEntities($entities);
} }
/** /**
* Rebuild the entity jointPermissions for a collection of entities. * Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities * @throws Throwable
* @throws \Throwable
*/ */
public function buildJointPermissionsForEntities(Collection $entities) public function buildJointPermissionsForEntities(array $entities)
{ {
$roles = $this->role->newQuery()->get(); $roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities->all()); $this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles); $this->createManyJointPermissions($entities, $roles);
} }
/** /**
* Build the entity jointPermissions for a particular role. * Build the entity jointPermissions for a particular role.
* @param Role $role
*/ */
public function buildJointPermissionForRole(Role $role) public function buildJointPermissionForRole(Role $role)
{ {
@ -285,7 +247,7 @@ class PermissionService
}); });
// Chunk through all bookshelves // Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by']) Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) { ->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles); $this->buildJointPermissionsForShelves($shelves, $roles);
}); });
@ -293,7 +255,6 @@ class PermissionService
/** /**
* Delete the entity jointPermissions attached to a particular role. * Delete the entity jointPermissions attached to a particular role.
* @param Role $role
*/ */
public function deleteJointPermissionsForRole(Role $role) public function deleteJointPermissionsForRole(Role $role)
{ {
@ -309,13 +270,13 @@ class PermissionService
$roleIds = array_map(function ($role) { $roleIds = array_map(function ($role) {
return $role->id; return $role->id;
}, $roles); }, $roles);
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete(); JointPermission::query()->whereIn('role_id', $roleIds)->delete();
} }
/** /**
* Delete the entity jointPermissions for a particular entity. * Delete the entity jointPermissions for a particular entity.
* @param Entity $entity * @param Entity $entity
* @throws \Throwable * @throws Throwable
*/ */
public function deleteJointPermissionsForEntity(Entity $entity) public function deleteJointPermissionsForEntity(Entity $entity)
{ {
@ -324,10 +285,10 @@ class PermissionService
/** /**
* Delete all of the entity jointPermissions for a list of entities. * Delete all of the entity jointPermissions for a list of entities.
* @param \BookStack\Entities\Models\Entity[] $entities * @param Entity[] $entities
* @throws \Throwable * @throws Throwable
*/ */
protected function deleteManyJointPermissionsForEntities($entities) protected function deleteManyJointPermissionsForEntities(array $entities)
{ {
if (count($entities) === 0) { if (count($entities) === 0) {
return; return;
@ -349,19 +310,19 @@ class PermissionService
} }
/** /**
* Create & Save entity jointPermissions for many entities and jointPermissions. * Create & Save entity jointPermissions for many entities and roles.
* @param Collection $entities * @param Entity[] $entities
* @param array $roles * @param Role[] $roles
* @throws \Throwable * @throws Throwable
*/ */
protected function createManyJointPermissions($entities, $roles) protected function createManyJointPermissions(array $entities, array $roles)
{ {
$this->readyEntityCache($entities); $this->readyEntityCache($entities);
$jointPermissions = []; $jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses // Fetch Entity Permissions and create a mapping of entity restricted statuses
$entityRestrictedMap = []; $entityRestrictedMap = [];
$permissionFetch = $this->entityPermission->newQuery(); $permissionFetch = EntityPermission::query();
foreach ($entities as $entity) { foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted')); $entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function ($query) use ($entity) { $permissionFetch->orWhere(function ($query) use ($entity) {
@ -405,16 +366,14 @@ class PermissionService
/** /**
* Get the actions related to an entity. * Get the actions related to an entity.
* @param \BookStack\Entities\Models\Entity $entity
* @return array
*/ */
protected function getActions(Entity $entity) protected function getActions(Entity $entity): array
{ {
$baseActions = ['view', 'update', 'delete']; $baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) { if ($entity instanceof Chapter || $entity instanceof Book) {
$baseActions[] = 'page-create'; $baseActions[] = 'page-create';
} }
if ($entity->isA('book')) { if ($entity instanceof Book) {
$baseActions[] = 'chapter-create'; $baseActions[] = 'chapter-create';
} }
return $baseActions; return $baseActions;
@ -423,14 +382,8 @@ class PermissionService
/** /**
* Create entity permission data for an entity and role * Create entity permission data for an entity and role
* for a particular action. * for a particular action.
* @param Entity $entity
* @param Role $role
* @param string $action
* @param array $permissionMap
* @param array $rolePermissionMap
* @return array
*/ */
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap) protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
{ {
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action; $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']); $roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
@ -447,7 +400,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
} }
if ($entity->isA('book') || $entity->isA('bookshelf')) { if ($entity instanceof Book || $entity instanceof Bookshelf) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} }
@ -476,38 +429,27 @@ class PermissionService
/** /**
* Check for an active restriction in an entity map. * Check for an active restriction in an entity map.
* @param $entityMap
* @param Entity $entity
* @param Role $role
* @param $action
* @return bool
*/ */
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
{ {
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false; return $entityMap[$key] ?? false;
} }
/** /**
* Create an array of data with the information of an entity jointPermissions. * Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion. * Used to build data for bulk insertion.
* @param \BookStack\Entities\Models\Entity $entity
* @param Role $role
* @param $action
* @param $permissionAll
* @param $permissionOwn
* @return array
*/ */
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn) protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
{ {
return [ return [
'role_id' => $role->getRawAttribute('id'), 'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'), 'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entity->getMorphClass(), 'entity_type' => $entity->getMorphClass(),
'action' => $action, 'action' => $action,
'has_permission' => $permissionAll, 'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn, 'has_permission_own' => $permissionOwn,
'owned_by' => $entity->getRawAttribute('owned_by') 'owned_by' => $entity->getRawAttribute('owned_by'),
]; ];
} }
@ -521,38 +463,34 @@ class PermissionService
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id); $baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
$action = end($explodedPermission); $action = end($explodedPermission);
$this->currentAction = $action; $user = $this->currentUser();
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions // Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) { if (in_array($explodedPermission[0], $nonJointPermissions)) {
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all'); $allPermission = $user && $user->can($permission . '-all');
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own'); $ownPermission = $user && $user->can($permission . '-own');
$this->currentAction = 'view';
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by'; $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField; $isOwner = $user && $user->id === $ownable->$ownerField;
return ($allPermission || ($isOwner && $ownPermission)); return ($allPermission || ($isOwner && $ownPermission));
} }
// Handle abnormal create jointPermissions // Handle abnormal create jointPermissions
if ($action === 'create') { if ($action === 'create') {
$this->currentAction = $permission; $action = $permission;
} }
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0; $hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
$this->clean(); $this->clean();
return $q; return $hasAccess;
} }
/** /**
* Checks if a user has the given permission for any items in the system. * Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type. * Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/ */
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null) public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
{ {
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray(); $userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id; $userId = $this->currentUser()->id;
@ -578,37 +516,16 @@ class PermissionService
return $hasPermission; return $hasPermission;
} }
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param \BookStack\Entities\Models\Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/** /**
* The general query filter to remove all entities * The general query filter to remove all entities
* that the current user does not have access to. * that the current user does not have access to.
* @param $query
* @return mixed
*/ */
protected function entityRestrictionQuery($query) protected function entityRestrictionQuery(Builder $query, string $action): Builder
{ {
$q = $query->where(function ($parentQuery) { $q = $query->where(function ($parentQuery) use ($action) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) { $parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
$permissionQuery->whereIn('role_id', $this->getRoles()) $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $this->currentAction) ->where('action', '=', $action)
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true) $query->where('has_permission', '=', true)
->orWhere(function ($query) { ->orWhere(function ($query) {
@ -618,6 +535,7 @@ class PermissionService
}); });
}); });
}); });
$this->clean(); $this->clean();
return $q; return $q;
} }
@ -631,7 +549,7 @@ class PermissionService
$this->clean(); $this->clean();
return $query->where(function (Builder $parentQuery) use ($ability) { return $query->where(function (Builder $parentQuery) use ($ability) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
$permissionQuery->whereIn('role_id', $this->getRoles()) $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $ability) ->where('action', '=', $ability)
->where(function (Builder $query) { ->where(function (Builder $query) {
$query->where('has_permission', '=', true) $query->where('has_permission', '=', true)
@ -648,7 +566,7 @@ class PermissionService
* Extend the given page query to ensure draft items are not visible * Extend the given page query to ensure draft items are not visible
* unless created by the given user. * unless created by the given user.
*/ */
public function enforceDraftVisiblityOnQuery(Builder $query): Builder public function enforceDraftVisibilityOnQuery(Builder $query): Builder
{ {
return $query->where(function (Builder $query) { return $query->where(function (Builder $query) {
$query->where('draft', '=', false) $query->where('draft', '=', false)
@ -660,17 +578,13 @@ class PermissionService
} }
/** /**
* Add restrictions for a generic entity * Add restrictions for a generic entity.
* @param string $entityType
* @param Builder|\BookStack\Entities\Models\Entity $query
* @param string $action
* @return Builder
*/ */
public function enforceEntityRestrictions($entityType, $query, $action = 'view') public function enforceEntityRestrictions(string $entityType, Builder $query, string $action = 'view'): Builder
{ {
if (strtolower($entityType) === 'page') { if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others. // Prevent drafts being visible to others.
$query = $query->where(function ($query) { $query->where(function ($query) {
$query->where('draft', '=', false) $query->where('draft', '=', false)
->orWhere(function ($query) { ->orWhere(function ($query) {
$query->where('draft', '=', true) $query->where('draft', '=', true)
@ -679,32 +593,23 @@ class PermissionService
}); });
} }
$this->currentAction = $action; return $this->entityRestrictionQuery($query, $action);
return $this->entityRestrictionQuery($query);
} }
/** /**
* Filter items that have entities set as a polymorphic relation. * Filter items that have entities set as a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return QueryBuilder
*/ */
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
{ {
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) { $q = $query->where(function ($query) use ($tableDetails, $action) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
$permissionQuery->select('id')->from('joint_permissions') $permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $this->currentAction) ->where('action', '=', $action)
->whereIn('role_id', $this->getRoles()) ->whereIn('role_id', $this->getCurrentUserRoles())
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
@ -713,34 +618,28 @@ class PermissionService
}); });
}); });
}); });
$this->clean(); $this->clean();
return $q; return $q;
} }
/** /**
* Add conditions to a query to filter the selection to related entities * Add conditions to a query to filter the selection to related entities
* where permissions are granted. * where view permissions are granted.
* @param $entityType
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/ */
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn) public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
{ {
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$morphClass = app($entityClass)->getMorphClass();
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass(); $q = $query->where(function ($query) use ($tableDetails, $morphClass) {
$query->where(function ($query) use (&$tableDetails, $morphClass) {
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
$permissionQuery->select('id')->from('joint_permissions') $permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where('entity_type', '=', $pageMorphClass) ->where('entity_type', '=', $morphClass)
->where('action', '=', $this->currentAction) ->where('action', '=', 'view')
->whereIn('role_id', $this->getRoles()) ->whereIn('role_id', $this->getCurrentUserRoles())
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
@ -752,17 +651,15 @@ class PermissionService
}); });
$this->clean(); $this->clean();
return $q; return $q;
} }
/** /**
* Get the current user * Get the current user
* @return \BookStack\Auth\User
*/ */
private function currentUser() private function currentUser(): User
{ {
if ($this->currentUserModel === false) { if (is_null($this->currentUserModel)) {
$this->currentUserModel = user(); $this->currentUserModel = user();
} }
@ -772,10 +669,9 @@ class PermissionService
/** /**
* Clean the cached user elements. * Clean the cached user elements.
*/ */
private function clean() private function clean(): void
{ {
$this->currentUserModel = false; $this->currentUserModel = null;
$this->userRoles = false; $this->userRoles = null;
$this->isAdminUser = null;
} }
} }

View File

@ -40,7 +40,7 @@ class Page extends BookChild
*/ */
public function scopeVisible(Builder $query): Builder public function scopeVisible(Builder $query): Builder
{ {
$query = Permissions::enforceDraftVisiblityOnQuery($query); $query = Permissions::enforceDraftVisibilityOnQuery($query);
return parent::scopeVisible($query); return parent::scopeVisible($query);
} }

View File

@ -82,7 +82,7 @@ class ImageRepo
} }
// Filter by page access // Filter by page access
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); $imageQuery = $this->restrictionService->filterRelatedEntity(Page::class, $imageQuery, 'images', 'uploaded_to');
if ($whereClause !== null) { if ($whereClause !== null) {
$imageQuery = $imageQuery->where($whereClause); $imageQuery = $imageQuery->where($whereClause);