|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace BookStack\Auth\Permissions; |
| 4 | + |
| 5 | +use BookStack\Auth\Role; |
| 6 | +use BookStack\Entities\Models\Entity; |
| 7 | +use Illuminate\Database\Eloquent\Builder; |
| 8 | + |
| 9 | +class EntityPermissionEvaluator |
| 10 | +{ |
| 11 | + protected string $action; |
| 12 | + |
| 13 | + public function __construct(string $action) |
| 14 | + { |
| 15 | + $this->action = $action; |
| 16 | + } |
| 17 | + |
| 18 | + public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool |
| 19 | + { |
| 20 | + if ($this->isUserSystemAdmin($userRoleIds)) { |
| 21 | + return true; |
| 22 | + } |
| 23 | + |
| 24 | + $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity)); |
| 25 | + $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]); |
| 26 | + $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions); |
| 27 | + |
| 28 | + $status = $this->evaluatePermitsByType($permitsByType); |
| 29 | + |
| 30 | + return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW; |
| 31 | + } |
| 32 | + |
| 33 | + /** |
| 34 | + * @param array<string, array<string, int>> $permitsByType |
| 35 | + */ |
| 36 | + protected function evaluatePermitsByType(array $permitsByType): ?int |
| 37 | + { |
| 38 | + // Return grant or reject from role-level if exists |
| 39 | + if (count($permitsByType['role']) > 0) { |
| 40 | + return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY; |
| 41 | + } |
| 42 | + |
| 43 | + // Return fallback permission if exists |
| 44 | + if (count($permitsByType['fallback']) > 0) { |
| 45 | + return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY; |
| 46 | + } |
| 47 | + |
| 48 | + return null; |
| 49 | + } |
| 50 | + |
| 51 | + /** |
| 52 | + * @param string[] $typeIdChain |
| 53 | + * @param array<string, EntityPermission[]> $permissionMapByTypeId |
| 54 | + * @return array<string, array<string, int>> |
| 55 | + */ |
| 56 | + protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array |
| 57 | + { |
| 58 | + $permitsByType = ['fallback' => [], 'role' => []]; |
| 59 | + |
| 60 | + foreach ($typeIdChain as $typeId) { |
| 61 | + $permissions = $permissionMapByTypeId[$typeId] ?? []; |
| 62 | + foreach ($permissions as $permission) { |
| 63 | + $roleId = $permission->role_id; |
| 64 | + $type = $roleId === 0 ? 'fallback' : 'role'; |
| 65 | + if (!isset($permitsByType[$type][$roleId])) { |
| 66 | + $permitsByType[$type][$roleId] = $permission->{$this->action}; |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + if (isset($permitsByType['fallback'][0])) { |
| 71 | + break; |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + return $permitsByType; |
| 76 | + } |
| 77 | + |
| 78 | + /** |
| 79 | + * @param string[] $typeIdChain |
| 80 | + * @return array<string, EntityPermission[]> |
| 81 | + */ |
| 82 | + protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array |
| 83 | + { |
| 84 | + $query = EntityPermission::query()->where(function (Builder $query) use ($typeIdChain) { |
| 85 | + foreach ($typeIdChain as $typeId) { |
| 86 | + $query->orWhere(function (Builder $query) use ($typeId) { |
| 87 | + [$type, $id] = explode(':', $typeId); |
| 88 | + $query->where('entity_type', '=', $type) |
| 89 | + ->where('entity_id', '=', $id); |
| 90 | + }); |
| 91 | + } |
| 92 | + }); |
| 93 | + |
| 94 | + if (!empty($filterRoleIds)) { |
| 95 | + $query->where(function (Builder $query) use ($filterRoleIds) { |
| 96 | + $query->whereIn('role_id', [...$filterRoleIds, 0]); |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + $relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all(); |
| 101 | + |
| 102 | + $map = []; |
| 103 | + foreach ($relevantPermissions as $permission) { |
| 104 | + $key = $permission->entity_type . ':' . $permission->entity_id; |
| 105 | + if (!isset($map[$key])) { |
| 106 | + $map[$key] = []; |
| 107 | + } |
| 108 | + |
| 109 | + $map[$key][] = $permission; |
| 110 | + } |
| 111 | + |
| 112 | + return $map; |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * @return string[] |
| 117 | + */ |
| 118 | + protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array |
| 119 | + { |
| 120 | + // The array order here is very important due to the fact we walk up the chain |
| 121 | + // elsewhere in the class. Earlier items in the chain have higher priority. |
| 122 | + |
| 123 | + $chain = [$entity->type . ':' . $entity->id]; |
| 124 | + |
| 125 | + if ($entity->type === 'page' && $entity->chapter_id) { |
| 126 | + $chain[] = 'chapter:' . $entity->chapter_id; |
| 127 | + } |
| 128 | + |
| 129 | + if ($entity->type === 'page' || $entity->type === 'chapter') { |
| 130 | + $chain[] = 'book:' . $entity->book_id; |
| 131 | + } |
| 132 | + |
| 133 | + return $chain; |
| 134 | + } |
| 135 | + |
| 136 | + protected function isUserSystemAdmin($userRoleIds): bool |
| 137 | + { |
| 138 | + $adminRoleId = Role::getSystemRole('admin')->id; |
| 139 | + return in_array($adminRoleId, $userRoleIds); |
| 140 | + } |
| 141 | +} |
0 commit comments