3 namespace BookStack\Auth\Permissions;
5 use BookStack\Auth\Role;
6 use BookStack\Entities\Models\Chapter;
7 use BookStack\Entities\Models\Entity;
8 use BookStack\Entities\Models\Page;
9 use Illuminate\Database\Eloquent\Builder;
11 class EntityPermissionEvaluator
13 protected Entity $entity;
14 protected array $userRoleIds;
15 protected string $action;
16 protected int $userId;
18 public function __construct(Entity $entity, int $userId, array $userRoleIds, string $action)
20 $this->entity = $entity;
21 $this->userId = $userId;
22 $this->userRoleIds = $userRoleIds;
23 $this->action = $action;
26 public function evaluate(): ?bool
28 if ($this->isUserSystemAdmin()) {
32 $typeIdChain = $this->gatherEntityChainTypeIds();
33 $relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain);
34 $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
36 // Return grant or reject from role-level if exists
37 if (count($permitsByType['role']) > 0) {
38 return boolval(max($permitsByType['role']));
41 // Return fallback permission if exists
42 if (count($permitsByType['fallback']) > 0) {
43 return boolval($permitsByType['fallback'][0]);
50 * @param string[] $typeIdChain
51 * @param array<string, EntityPermission[]> $permissionMapByTypeId
52 * @return array<string, array<string, int>>
54 protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
56 $permitsByType = ['fallback' => [], 'role' => []];
58 foreach ($typeIdChain as $typeId) {
59 $permissions = $permissionMapByTypeId[$typeId] ?? [];
60 foreach ($permissions as $permission) {
61 $roleId = $permission->role_id;
62 $type = $roleId === 0 ? 'fallback' : 'role';
63 if (!isset($permitsByType[$type][$roleId])) {
64 $permitsByType[$type][$roleId] = $permission->{$this->action};
69 return $permitsByType;
73 * @param string[] $typeIdChain
74 * @return array<string, EntityPermission[]>
76 protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array
78 $relevantPermissions = EntityPermission::query()
79 ->where(function (Builder $query) use ($typeIdChain) {
80 foreach ($typeIdChain as $typeId) {
81 $query->orWhere(function (Builder $query) use ($typeId) {
82 [$type, $id] = explode(':', $typeId);
83 $query->where('entity_type', '=', $type)
84 ->where('entity_id', '=', $id);
87 })->where(function (Builder $query) {
88 $query->whereIn('role_id', [...$this->userRoleIds, 0]);
89 })->get(['entity_id', 'entity_type', 'role_id', $this->action])
93 foreach ($relevantPermissions as $permission) {
94 $key = $permission->entity_type . ':' . $permission->entity_id;
95 if (!isset($map[$key])) {
99 $map[$key][] = $permission;
108 protected function gatherEntityChainTypeIds(): array
110 // The array order here is very important due to the fact we walk up the chain
111 // elsewhere in the class. Earlier items in the chain have higher priority.
113 $chain = [$this->entity->getMorphClass() . ':' . $this->entity->id];
115 if ($this->entity instanceof Page && $this->entity->chapter_id) {
116 $chain[] = 'chapter:' . $this->entity->chapter_id;
119 if ($this->entity instanceof Page || $this->entity instanceof Chapter) {
120 $chain[] = 'book:' . $this->entity->book_id;
126 protected function isUserSystemAdmin(): bool
128 $adminRoleId = Role::getSystemRole('admin')->id;
129 return in_array($adminRoleId, $this->userRoleIds);