3 namespace BookStack\Auth\Permissions;
5 use BookStack\Auth\Role;
6 use BookStack\Entities\Models\Entity;
7 use Illuminate\Database\Eloquent\Builder;
9 class EntityPermissionEvaluator
11 protected string $action;
13 public function __construct(string $action)
15 $this->action = $action;
18 public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
20 if ($this->isUserSystemAdmin($userRoleIds)) {
24 $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
25 $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
26 $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
28 $status = $this->evaluatePermitsByType($permitsByType);
30 return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
34 * @param array<string, array<string, int>> $permitsByType
36 protected function evaluatePermitsByType(array $permitsByType): ?int
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;
43 // Return fallback permission if exists
44 if (count($permitsByType['fallback']) > 0) {
45 return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
52 * @param string[] $typeIdChain
53 * @param array<string, EntityPermission[]> $permissionMapByTypeId
54 * @return array<string, array<string, int>>
56 protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
58 $permitsByType = ['fallback' => [], 'role' => []];
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};
70 if (isset($permitsByType['fallback'][0])) {
75 return $permitsByType;
79 * @param string[] $typeIdChain
80 * @return array<string, EntityPermission[]>
82 protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
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);
94 if (!empty($filterRoleIds)) {
95 $query->where(function (Builder $query) use ($filterRoleIds) {
96 $query->whereIn('role_id', [...$filterRoleIds, 0]);
100 $relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
103 foreach ($relevantPermissions as $permission) {
104 $key = $permission->entity_type . ':' . $permission->entity_id;
105 if (!isset($map[$key])) {
109 $map[$key][] = $permission;
118 protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
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.
123 $chain = [$entity->type . ':' . $entity->id];
125 if ($entity->type === 'page' && $entity->chapter_id) {
126 $chain[] = 'chapter:' . $entity->chapter_id;
129 if ($entity->type === 'page' || $entity->type === 'chapter') {
130 $chain[] = 'book:' . $entity->book_id;
136 protected function isUserSystemAdmin($userRoleIds): bool
138 $adminRoleId = Role::getSystemRole('admin')->id;
139 return in_array($adminRoleId, $userRoleIds);