2023-01-23 12:40:11 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace BookStack\Auth\Permissions;
|
|
|
|
|
|
|
|
use BookStack\Auth\Role;
|
|
|
|
use BookStack\Entities\Models\Entity;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
|
|
|
|
class EntityPermissionEvaluator
|
|
|
|
{
|
|
|
|
protected string $action;
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
public function __construct(string $action)
|
2023-01-23 12:40:11 +00:00
|
|
|
{
|
|
|
|
$this->action = $action;
|
|
|
|
}
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
|
2023-01-23 12:40:11 +00:00
|
|
|
{
|
2023-01-23 15:09:03 +00:00
|
|
|
if ($this->isUserSystemAdmin($userRoleIds)) {
|
2023-01-23 12:40:11 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
$typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
|
|
|
|
$relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
|
2023-01-23 12:40:11 +00:00
|
|
|
$permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
|
|
|
|
|
2023-01-24 20:42:20 +00:00
|
|
|
$status = $this->evaluatePermitsByType($permitsByType);
|
|
|
|
|
|
|
|
return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
|
2023-01-23 15:09:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<string, array<string, int>> $permitsByType
|
|
|
|
*/
|
2023-01-24 20:42:20 +00:00
|
|
|
protected function evaluatePermitsByType(array $permitsByType): ?int
|
2023-01-23 15:09:03 +00:00
|
|
|
{
|
2023-01-23 12:40:11 +00:00
|
|
|
// Return grant or reject from role-level if exists
|
|
|
|
if (count($permitsByType['role']) > 0) {
|
2023-01-24 20:42:20 +00:00
|
|
|
return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return fallback permission if exists
|
|
|
|
if (count($permitsByType['fallback']) > 0) {
|
2023-01-24 20:42:20 +00:00
|
|
|
return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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};
|
|
|
|
}
|
|
|
|
}
|
2023-01-24 21:26:41 +00:00
|
|
|
|
|
|
|
if (isset($permitsByType['fallback'][0])) {
|
|
|
|
break;
|
|
|
|
}
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $permitsByType;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string[] $typeIdChain
|
|
|
|
* @return array<string, EntityPermission[]>
|
|
|
|
*/
|
2023-01-23 15:09:03 +00:00
|
|
|
protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
|
2023-01-23 12:40:11 +00:00
|
|
|
{
|
2023-01-23 15:09:03 +00:00
|
|
|
$query = 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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!empty($filterRoleIds)) {
|
|
|
|
$query->where(function (Builder $query) use ($filterRoleIds) {
|
|
|
|
$query->whereIn('role_id', [...$filterRoleIds, 0]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
$relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
|
2023-01-23 12:40:11 +00:00
|
|
|
|
|
|
|
$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[]
|
|
|
|
*/
|
2023-01-23 15:09:03 +00:00
|
|
|
protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
|
2023-01-23 12:40:11 +00:00
|
|
|
{
|
|
|
|
// 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.
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
$chain = [$entity->type . ':' . $entity->id];
|
2023-01-23 12:40:11 +00:00
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
if ($entity->type === 'page' && $entity->chapter_id) {
|
|
|
|
$chain[] = 'chapter:' . $entity->chapter_id;
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
if ($entity->type === 'page' || $entity->type === 'chapter') {
|
|
|
|
$chain[] = 'book:' . $entity->book_id;
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $chain;
|
|
|
|
}
|
|
|
|
|
2023-01-23 15:09:03 +00:00
|
|
|
protected function isUserSystemAdmin($userRoleIds): bool
|
2023-01-23 12:40:11 +00:00
|
|
|
{
|
|
|
|
$adminRoleId = Role::getSystemRole('admin')->id;
|
2023-01-23 15:09:03 +00:00
|
|
|
return in_array($adminRoleId, $userRoleIds);
|
2023-01-23 12:40:11 +00:00
|
|
|
}
|
|
|
|
}
|