3 namespace BookStack\Permissions;
5 use BookStack\Entities\Models\Entity;
6 use BookStack\Permissions\Models\EntityPermission;
7 use BookStack\Users\Models\Role;
8 use Illuminate\Database\Eloquent\Builder;
10 class EntityPermissionEvaluator
12 protected string $action;
14 public function __construct(string $action)
16 $this->action = $action;
19 public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
21 if ($this->isUserSystemAdmin($userRoleIds)) {
25 $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
26 $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
27 $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
29 $status = $this->evaluatePermitsByType($permitsByType);
31 return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
35 * @param array<string, array<string, int>> $permitsByType
37 protected function evaluatePermitsByType(array $permitsByType): ?int
39 // Return grant or reject from role-level if exists
40 if (count($permitsByType['role']) > 0) {
41 return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;
44 // Return fallback permission if exists
45 if (count($permitsByType['fallback']) > 0) {
46 return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
53 * @param string[] $typeIdChain
54 * @param array<string, EntityPermission[]> $permissionMapByTypeId
55 * @return array<string, array<string, int>>
57 protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
59 $permitsByType = ['fallback' => [], 'role' => []];
61 foreach ($typeIdChain as $typeId) {
62 $permissions = $permissionMapByTypeId[$typeId] ?? [];
63 foreach ($permissions as $permission) {
64 $roleId = $permission->role_id;
65 $type = $roleId === 0 ? 'fallback' : 'role';
66 if (!isset($permitsByType[$type][$roleId])) {
67 $permitsByType[$type][$roleId] = $permission->{$this->action};
71 if (isset($permitsByType['fallback'][0])) {
76 return $permitsByType;
80 * @param string[] $typeIdChain
81 * @return array<string, EntityPermission[]>
83 protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
85 $query = EntityPermission::query()->where(function (Builder $query) use ($typeIdChain) {
86 foreach ($typeIdChain as $typeId) {
87 $query->orWhere(function (Builder $query) use ($typeId) {
88 [$type, $id] = explode(':', $typeId);
89 $query->where('entity_type', '=', $type)
90 ->where('entity_id', '=', $id);
95 if (!empty($filterRoleIds)) {
96 $query->where(function (Builder $query) use ($filterRoleIds) {
97 $query->whereIn('role_id', [...$filterRoleIds, 0]);
101 $relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
104 foreach ($relevantPermissions as $permission) {
105 $key = $permission->entity_type . ':' . $permission->entity_id;
106 if (!isset($map[$key])) {
110 $map[$key][] = $permission;
119 protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
121 // The array order here is very important due to the fact we walk up the chain
122 // elsewhere in the class. Earlier items in the chain have higher priority.
124 $chain = [$entity->type . ':' . $entity->id];
126 if ($entity->type === 'page' && $entity->chapter_id) {
127 $chain[] = 'chapter:' . $entity->chapter_id;
130 if ($entity->type === 'page' || $entity->type === 'chapter') {
131 $chain[] = 'book:' . $entity->book_id;
137 protected function isUserSystemAdmin($userRoleIds): bool
139 $adminRoleId = Role::getSystemRole('admin')->id;
140 return in_array($adminRoleId, $userRoleIds);