2021-06-26 17:23:15 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace BookStack\Auth\Permissions;
|
2018-09-25 13:30:50 +02:00
|
|
|
|
2018-09-25 17:58:03 +02:00
|
|
|
use BookStack\Auth\Role;
|
2021-03-14 20:52:07 +01:00
|
|
|
use BookStack\Auth\User;
|
2020-11-22 01:17:45 +01:00
|
|
|
use BookStack\Entities\Models\Entity;
|
2021-03-13 16:18:37 +01:00
|
|
|
use BookStack\Entities\Models\Page;
|
2020-12-30 19:25:35 +01:00
|
|
|
use BookStack\Model;
|
|
|
|
use BookStack\Traits\HasCreatorAndUpdater;
|
|
|
|
use BookStack\Traits\HasOwner;
|
2017-01-01 17:05:44 +01:00
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2017-04-30 12:38:58 +02:00
|
|
|
use Illuminate\Database\Query\Builder as QueryBuilder;
|
2016-02-29 21:31:21 +01:00
|
|
|
|
2022-07-12 21:15:41 +02:00
|
|
|
class PermissionApplicator
|
2016-02-28 20:03:04 +01:00
|
|
|
{
|
2018-09-25 19:00:40 +02:00
|
|
|
/**
|
2022-07-12 21:15:41 +02:00
|
|
|
* @var ?array<int>
|
2018-09-25 19:00:40 +02:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
protected $userRoles = null;
|
2017-01-01 22:21:11 +01:00
|
|
|
|
2018-09-25 19:00:40 +02:00
|
|
|
/**
|
2021-03-14 20:52:07 +01:00
|
|
|
* @var ?User
|
2018-09-25 19:00:40 +02:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
protected $currentUserModel = null;
|
2018-09-25 19:00:40 +02:00
|
|
|
|
2016-05-01 20:36:53 +02:00
|
|
|
/**
|
2021-03-14 20:52:07 +01:00
|
|
|
* Get the roles for the current logged in user.
|
2016-05-01 20:36:53 +02:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
protected function getCurrentUserRoles(): array
|
2016-05-01 20:36:53 +02:00
|
|
|
{
|
2021-03-14 20:52:07 +01:00
|
|
|
if (!is_null($this->userRoles)) {
|
2018-01-28 17:58:52 +01:00
|
|
|
return $this->userRoles;
|
|
|
|
}
|
2016-05-01 20:36:53 +02:00
|
|
|
|
|
|
|
if (auth()->guest()) {
|
2021-03-14 20:52:07 +01:00
|
|
|
$this->userRoles = [Role::getSystemRole('public')->id];
|
|
|
|
} else {
|
|
|
|
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
|
2016-05-01 20:36:53 +02:00
|
|
|
}
|
|
|
|
|
2021-03-14 20:52:07 +01:00
|
|
|
return $this->userRoles;
|
2016-05-01 20:36:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-05 19:09:21 +01:00
|
|
|
/**
|
|
|
|
* Checks if an entity has a restriction set upon it.
|
2021-06-26 17:23:15 +02:00
|
|
|
*
|
2020-12-30 19:25:35 +01:00
|
|
|
* @param HasCreatorAndUpdater|HasOwner $ownable
|
2016-03-05 19:09:21 +01:00
|
|
|
*/
|
2020-12-30 19:25:35 +01:00
|
|
|
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
2016-02-29 21:31:21 +01:00
|
|
|
{
|
2016-04-24 17:54:20 +02:00
|
|
|
$explodedPermission = explode('-', $permission);
|
|
|
|
|
2020-12-30 19:25:35 +01:00
|
|
|
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
|
2016-04-26 22:48:17 +02:00
|
|
|
$action = end($explodedPermission);
|
2021-03-14 20:52:07 +01:00
|
|
|
$user = $this->currentUser();
|
2016-04-24 17:54:20 +02:00
|
|
|
|
2020-12-30 23:18:28 +01:00
|
|
|
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
|
|
|
|
2016-05-01 22:20:50 +02:00
|
|
|
// Handle non entity specific jointPermissions
|
2020-12-30 23:18:28 +01:00
|
|
|
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
2021-03-14 20:52:07 +01:00
|
|
|
$allPermission = $user && $user->can($permission . '-all');
|
|
|
|
$ownPermission = $user && $user->can($permission . '-own');
|
2021-01-04 19:07:39 +01:00
|
|
|
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
2021-03-14 20:52:07 +01:00
|
|
|
$isOwner = $user && $user->id === $ownable->$ownerField;
|
2021-06-26 17:23:15 +02:00
|
|
|
|
|
|
|
return $allPermission || ($isOwner && $ownPermission);
|
2016-02-29 21:31:21 +01:00
|
|
|
}
|
2016-04-24 17:54:20 +02:00
|
|
|
|
2016-05-01 22:20:50 +02:00
|
|
|
// Handle abnormal create jointPermissions
|
2016-04-26 22:48:17 +02:00
|
|
|
if ($action === 'create') {
|
2021-03-14 20:52:07 +01:00
|
|
|
$action = $permission;
|
2016-04-26 22:48:17 +02:00
|
|
|
}
|
|
|
|
|
2022-07-13 16:23:03 +02:00
|
|
|
// TODO - Use a non-query based check
|
2021-03-14 20:52:07 +01:00
|
|
|
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
2016-09-17 19:22:04 +02:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2021-03-14 20:52:07 +01:00
|
|
|
return $hasAccess;
|
2016-02-28 20:03:04 +01:00
|
|
|
}
|
|
|
|
|
2019-01-02 06:55:28 +01:00
|
|
|
/**
|
2019-03-09 17:50:22 +01:00
|
|
|
* Checks if a user has the given permission for any items in the system.
|
2019-03-09 22:15:45 +01:00
|
|
|
* Can be passed an entity instance to filter on a specific type.
|
2019-01-02 06:55:28 +01:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
|
2019-01-02 06:55:28 +01:00
|
|
|
{
|
2019-03-09 17:50:22 +01:00
|
|
|
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
2019-01-02 06:55:28 +01:00
|
|
|
$userId = $this->currentUser()->id;
|
|
|
|
|
2021-03-14 21:32:33 +01:00
|
|
|
$permissionQuery = JointPermission::query()
|
2019-03-09 17:50:22 +01:00
|
|
|
->where('action', '=', $permission)
|
2019-01-02 06:55:28 +01:00
|
|
|
->whereIn('role_id', $userRoleIds)
|
2021-03-14 21:32:33 +01:00
|
|
|
->where(function (Builder $query) use ($userId) {
|
|
|
|
$this->addJointHasPermissionCheck($query, $userId);
|
2019-04-03 11:26:24 +02:00
|
|
|
});
|
2019-03-09 22:15:45 +01:00
|
|
|
|
|
|
|
if (!is_null($entityClass)) {
|
2021-03-14 21:32:33 +01:00
|
|
|
$entityInstance = app($entityClass);
|
2019-03-09 22:15:45 +01:00
|
|
|
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
|
|
|
}
|
2019-01-02 06:55:28 +01:00
|
|
|
|
2019-03-09 22:15:45 +01:00
|
|
|
$hasPermission = $permissionQuery->count() > 0;
|
2019-03-09 17:50:22 +01:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2019-03-09 22:15:45 +01:00
|
|
|
return $hasPermission;
|
2019-01-02 06:55:28 +01:00
|
|
|
}
|
|
|
|
|
2016-02-28 20:03:04 +01:00
|
|
|
/**
|
2016-04-23 19:14:26 +02:00
|
|
|
* The general query filter to remove all entities
|
|
|
|
* that the current user does not have access to.
|
2016-02-28 20:03:04 +01:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
protected function entityRestrictionQuery(Builder $query, string $action): Builder
|
2016-02-28 20:03:04 +01:00
|
|
|
{
|
2021-03-14 20:52:07 +01:00
|
|
|
$q = $query->where(function ($parentQuery) use ($action) {
|
|
|
|
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
|
|
|
|
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
|
|
|
->where('action', '=', $action)
|
2021-03-14 21:32:33 +01:00
|
|
|
->where(function (Builder $query) {
|
|
|
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
2016-02-28 20:03:04 +01:00
|
|
|
});
|
2016-04-23 19:14:26 +02:00
|
|
|
});
|
2016-02-28 20:03:04 +01:00
|
|
|
});
|
2021-03-14 20:52:07 +01:00
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
return $q;
|
2016-02-28 20:03:04 +01:00
|
|
|
}
|
|
|
|
|
2017-01-15 16:00:29 +01:00
|
|
|
/**
|
2019-10-05 13:55:01 +02:00
|
|
|
* Limited the given entity query so that the query will only
|
|
|
|
* return items that the user has permission for the given ability.
|
2017-01-15 16:00:29 +01:00
|
|
|
*/
|
2019-10-05 13:55:01 +02:00
|
|
|
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
|
2018-01-28 17:58:52 +01:00
|
|
|
{
|
2019-10-05 13:55:01 +02:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2019-10-05 13:55:01 +02:00
|
|
|
return $query->where(function (Builder $parentQuery) use ($ability) {
|
|
|
|
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
2021-03-14 20:52:07 +01:00
|
|
|
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
2019-10-05 13:55:01 +02:00
|
|
|
->where('action', '=', $ability)
|
|
|
|
->where(function (Builder $query) {
|
2021-03-14 21:32:33 +01:00
|
|
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
2019-01-27 11:29:23 +01:00
|
|
|
});
|
2018-09-20 20:48:08 +02:00
|
|
|
});
|
2019-10-05 13:55:01 +02:00
|
|
|
});
|
|
|
|
}
|
2017-01-22 13:02:30 +01:00
|
|
|
|
2019-10-05 13:55:01 +02:00
|
|
|
/**
|
|
|
|
* Extend the given page query to ensure draft items are not visible
|
|
|
|
* unless created by the given user.
|
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
|
2019-10-05 13:55:01 +02:00
|
|
|
{
|
|
|
|
return $query->where(function (Builder $query) {
|
|
|
|
$query->where('draft', '=', false)
|
|
|
|
->orWhere(function (Builder $query) {
|
|
|
|
$query->where('draft', '=', true)
|
2020-12-30 19:25:35 +01:00
|
|
|
->where('owned_by', '=', $this->currentUser()->id);
|
2019-10-05 13:55:01 +02:00
|
|
|
});
|
|
|
|
});
|
2017-01-01 22:21:11 +01:00
|
|
|
}
|
|
|
|
|
2016-05-06 21:33:08 +02:00
|
|
|
/**
|
2021-03-14 20:52:07 +01:00
|
|
|
* Add restrictions for a generic entity.
|
2016-05-06 21:33:08 +02:00
|
|
|
*/
|
2022-07-13 16:23:03 +02:00
|
|
|
public function enforceEntityRestrictions(Entity $entity, Builder $query): Builder
|
2016-02-28 20:03:04 +01:00
|
|
|
{
|
2021-03-14 21:32:33 +01:00
|
|
|
if ($entity instanceof Page) {
|
2017-01-01 17:05:44 +01:00
|
|
|
// Prevent drafts being visible to others.
|
2021-03-14 21:32:33 +01:00
|
|
|
$this->enforceDraftVisibilityOnQuery($query);
|
2017-01-01 17:05:44 +01:00
|
|
|
}
|
|
|
|
|
2022-07-13 16:23:03 +02:00
|
|
|
return $this->entityRestrictionQuery($query, 'view');
|
2016-02-28 20:03:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-03-19 13:48:44 +01:00
|
|
|
* Filter items that have entities set as a polymorphic relation.
|
2021-11-30 01:06:17 +01:00
|
|
|
* For simplicity, this will not return results attached to draft pages.
|
|
|
|
* Draft pages should never really have related items though.
|
2021-06-26 17:23:15 +02:00
|
|
|
*
|
2021-09-29 19:41:11 +02:00
|
|
|
* @param Builder|QueryBuilder $query
|
2016-02-28 20:03:04 +01:00
|
|
|
*/
|
2021-05-20 00:37:23 +02:00
|
|
|
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
2016-02-28 20:03:04 +01:00
|
|
|
{
|
2021-09-29 19:41:11 +02:00
|
|
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
2021-11-30 01:06:17 +01:00
|
|
|
$pageMorphClass = (new Page())->getMorphClass();
|
|
|
|
|
|
|
|
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
|
|
|
/** @var Builder $permissionQuery */
|
|
|
|
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
|
|
|
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
|
|
|
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
|
|
|
->where('joint_permissions.action', '=', $action)
|
|
|
|
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
|
|
|
->where(function (QueryBuilder $query) {
|
|
|
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
|
|
|
});
|
|
|
|
})->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
|
|
|
/** @var Builder $query */
|
|
|
|
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
|
2021-11-30 15:25:09 +01:00
|
|
|
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
|
2021-11-30 01:06:17 +01:00
|
|
|
$query->select('id')->from('pages')
|
|
|
|
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
|
|
|
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
|
|
|
|
->where('pages.draft', '=', false);
|
|
|
|
});
|
2016-02-28 20:03:04 +01:00
|
|
|
});
|
2021-03-14 20:52:07 +01:00
|
|
|
|
2017-01-21 17:16:27 +01:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
return $q;
|
2016-02-28 20:03:04 +01:00
|
|
|
}
|
|
|
|
|
2016-03-13 14:30:47 +01:00
|
|
|
/**
|
2019-04-27 15:18:00 +02:00
|
|
|
* Add conditions to a query to filter the selection to related entities
|
2021-03-14 20:52:07 +01:00
|
|
|
* where view permissions are granted.
|
2016-03-13 14:30:47 +01:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
2016-03-13 14:30:47 +01:00
|
|
|
{
|
2021-11-30 01:06:17 +01:00
|
|
|
$fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
|
2021-11-30 15:25:09 +01:00
|
|
|
$instance = new $entityClass();
|
2021-11-30 01:06:17 +01:00
|
|
|
$morphClass = $instance->getMorphClass();
|
|
|
|
|
2021-11-30 15:25:09 +01:00
|
|
|
$existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
|
2021-11-30 01:06:17 +01:00
|
|
|
/** @var Builder $permissionQuery */
|
|
|
|
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
|
|
|
|
->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
|
|
|
|
->where('joint_permissions.entity_type', '=', $morphClass)
|
|
|
|
->where('joint_permissions.action', '=', 'view')
|
|
|
|
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
|
|
|
->where(function (QueryBuilder $query) {
|
|
|
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
2016-04-23 19:14:26 +02:00
|
|
|
});
|
2021-11-30 01:06:17 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
$q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
|
|
|
|
$query->whereExists($existsQuery)
|
|
|
|
->orWhere($fullEntityIdColumn, '=', 0);
|
2016-03-13 14:30:47 +01:00
|
|
|
});
|
2019-04-27 15:18:00 +02:00
|
|
|
|
2021-11-30 01:06:17 +01:00
|
|
|
if ($instance instanceof Page) {
|
|
|
|
// Prevent visibility of non-owned draft pages
|
2021-11-30 15:25:09 +01:00
|
|
|
$q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
|
2021-11-30 01:06:17 +01:00
|
|
|
$query->select('id')->from('pages')
|
|
|
|
->whereColumn('pages.id', '=', $fullEntityIdColumn)
|
|
|
|
->where(function (QueryBuilder $query) {
|
|
|
|
$query->where('pages.draft', '=', false)
|
|
|
|
->orWhere('pages.owned_by', '=', $this->currentUser()->id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
$this->clean();
|
2021-06-26 17:23:15 +02:00
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
return $q;
|
|
|
|
}
|
|
|
|
|
2021-03-14 21:32:33 +01:00
|
|
|
/**
|
|
|
|
* Add the query for checking the given user id has permission
|
|
|
|
* within the join_permissions table.
|
2021-06-26 17:23:15 +02:00
|
|
|
*
|
2021-03-14 21:32:33 +01:00
|
|
|
* @param QueryBuilder|Builder $query
|
|
|
|
*/
|
|
|
|
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
|
|
|
{
|
2021-11-30 01:06:17 +01:00
|
|
|
$query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
|
|
|
$query->where('joint_permissions.has_permission_own', '=', true)
|
|
|
|
->where('joint_permissions.owned_by', '=', $userIdToCheck);
|
2021-03-14 21:32:33 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-09-17 19:22:04 +02:00
|
|
|
/**
|
2021-06-26 17:23:15 +02:00
|
|
|
* Get the current user.
|
2016-09-17 19:22:04 +02:00
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
private function currentUser(): User
|
2016-09-17 19:22:04 +02:00
|
|
|
{
|
2021-03-14 20:52:07 +01:00
|
|
|
if (is_null($this->currentUserModel)) {
|
2016-09-29 13:43:46 +02:00
|
|
|
$this->currentUserModel = user();
|
2016-09-17 19:22:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $this->currentUserModel;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean the cached user elements.
|
|
|
|
*/
|
2021-03-14 20:52:07 +01:00
|
|
|
private function clean(): void
|
2016-09-17 19:22:04 +02:00
|
|
|
{
|
2021-03-14 20:52:07 +01:00
|
|
|
$this->currentUserModel = null;
|
|
|
|
$this->userRoles = null;
|
2016-03-13 14:30:47 +01:00
|
|
|
}
|
2018-01-28 17:58:52 +01:00
|
|
|
}
|