Updated userCan logic to meet expectations in tests
Updated with similar logic to that used in the user_permissions branch, but all extracted to a seperate class for doing all fetch and collapse work.
This commit is contained in:
parent
e2a72d16aa
commit
f3f2a0c1d5
|
@ -0,0 +1,131 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
|
use BookStack\Auth\Role;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class EntityPermissionEvaluator
|
||||||
|
{
|
||||||
|
protected Entity $entity;
|
||||||
|
protected array $userRoleIds;
|
||||||
|
protected string $action;
|
||||||
|
protected int $userId;
|
||||||
|
|
||||||
|
public function __construct(Entity $entity, int $userId, array $userRoleIds, string $action)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
$this->userId = $userId;
|
||||||
|
$this->userRoleIds = $userRoleIds;
|
||||||
|
$this->action = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function evaluate(): ?bool
|
||||||
|
{
|
||||||
|
if ($this->isUserSystemAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$typeIdChain = $this->gatherEntityChainTypeIds();
|
||||||
|
$relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain);
|
||||||
|
$permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
|
||||||
|
|
||||||
|
// Return grant or reject from role-level if exists
|
||||||
|
if (count($permitsByType['role']) > 0) {
|
||||||
|
return boolval(max($permitsByType['role']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return fallback permission if exists
|
||||||
|
if (count($permitsByType['fallback']) > 0) {
|
||||||
|
return boolval($permitsByType['fallback'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $typeIdChain
|
||||||
|
* @param array<string, EntityPermission[]> $permissionMapByTypeId
|
||||||
|
* @return array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
|
||||||
|
{
|
||||||
|
$permitsByType = ['fallback' => [], 'role' => []];
|
||||||
|
|
||||||
|
foreach ($typeIdChain as $typeId) {
|
||||||
|
$permissions = $permissionMapByTypeId[$typeId] ?? [];
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
$roleId = $permission->role_id;
|
||||||
|
$type = $roleId === 0 ? 'fallback' : 'role';
|
||||||
|
if (!isset($permitsByType[$type][$roleId])) {
|
||||||
|
$permitsByType[$type][$roleId] = $permission->{$this->action};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $permitsByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $typeIdChain
|
||||||
|
* @return array<string, EntityPermission[]>
|
||||||
|
*/
|
||||||
|
protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array
|
||||||
|
{
|
||||||
|
$relevantPermissions = EntityPermission::query()
|
||||||
|
->where(function (Builder $query) use ($typeIdChain) {
|
||||||
|
foreach ($typeIdChain as $typeId) {
|
||||||
|
$query->orWhere(function (Builder $query) use ($typeId) {
|
||||||
|
[$type, $id] = explode(':', $typeId);
|
||||||
|
$query->where('entity_type', '=', $type)
|
||||||
|
->where('entity_id', '=', $id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})->where(function (Builder $query) {
|
||||||
|
$query->whereIn('role_id', [...$this->userRoleIds, 0]);
|
||||||
|
})->get(['entity_id', 'entity_type', 'role_id', $this->action])
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($relevantPermissions as $permission) {
|
||||||
|
$key = $permission->entity_type . ':' . $permission->entity_id;
|
||||||
|
if (!isset($map[$key])) {
|
||||||
|
$map[$key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[$key][] = $permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
protected function gatherEntityChainTypeIds(): array
|
||||||
|
{
|
||||||
|
// The array order here is very important due to the fact we walk up the chain
|
||||||
|
// elsewhere in the class. Earlier items in the chain have higher priority.
|
||||||
|
|
||||||
|
$chain = [$this->entity->getMorphClass() . ':' . $this->entity->id];
|
||||||
|
|
||||||
|
if ($this->entity instanceof Page && $this->entity->chapter_id) {
|
||||||
|
$chain[] = 'chapter:' . $this->entity->chapter_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->entity instanceof Page || $this->entity instanceof Chapter) {
|
||||||
|
$chain[] = 'book:' . $this->entity->book_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isUserSystemAdmin(): bool
|
||||||
|
{
|
||||||
|
$adminRoleId = Role::getSystemRole('admin')->id;
|
||||||
|
return in_array($adminRoleId, $this->userRoleIds);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Models\Chapter;
|
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
@ -48,7 +47,7 @@ class PermissionApplicator
|
||||||
return $hasRolePermission;
|
return $hasRolePermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
|
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $user->id, $userRoleIds, $action);
|
||||||
|
|
||||||
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
|
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
|
||||||
}
|
}
|
||||||
|
@ -57,50 +56,11 @@ class PermissionApplicator
|
||||||
* Check if there are permissions that are applicable for the given entity item, action and roles.
|
* Check if there are permissions that are applicable for the given entity item, action and roles.
|
||||||
* Returns null when no entity permissions are in force.
|
* Returns null when no entity permissions are in force.
|
||||||
*/
|
*/
|
||||||
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool
|
protected function hasEntityPermission(Entity $entity, int $userId, array $userRoleIds, string $action): ?bool
|
||||||
{
|
{
|
||||||
$this->ensureValidEntityAction($action);
|
$this->ensureValidEntityAction($action);
|
||||||
|
|
||||||
$adminRoleId = Role::getSystemRole('admin')->id;
|
return (new EntityPermissionEvaluator($entity, $userId, $userRoleIds, $action))->evaluate();
|
||||||
if (in_array($adminRoleId, $userRoleIds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The chain order here is very important due to the fact we walk up the chain
|
|
||||||
// in the loop below. Earlier items in the chain have higher priority.
|
|
||||||
$chain = [$entity];
|
|
||||||
if ($entity instanceof Page && $entity->chapter_id) {
|
|
||||||
$chain[] = $entity->chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($entity instanceof Page || $entity instanceof Chapter) {
|
|
||||||
$chain[] = $entity->book;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($chain as $currentEntity) {
|
|
||||||
$allowedByRoleId = $currentEntity->permissions()
|
|
||||||
->whereIn('role_id', [0, ...$userRoleIds])
|
|
||||||
->pluck($action, 'role_id');
|
|
||||||
|
|
||||||
// Continue up the chain if no applicable entity permission overrides.
|
|
||||||
if ($allowedByRoleId->isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have user-role-specific permissions set, allow if any of those
|
|
||||||
// role permissions allow access.
|
|
||||||
$hasDefault = $allowedByRoleId->has(0);
|
|
||||||
if (!$hasDefault || $allowedByRoleId->count() > 1) {
|
|
||||||
return $allowedByRoleId->search(function (bool $allowed, int $roleId) {
|
|
||||||
return $roleId !== 0 && $allowed;
|
|
||||||
}) !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return the default "Other roles" fallback value.
|
|
||||||
return $allowedByRoleId->get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue