]> BookStack Code Mirror - bookstack/blob - app/Auth/Permissions/EntityPermissionEvaluator.php
91596d02a2472b3d71a26001f5f825d61449abe4
[bookstack] / app / Auth / Permissions / EntityPermissionEvaluator.php
1 <?php
2
3 namespace BookStack\Auth\Permissions;
4
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;
10
11 class EntityPermissionEvaluator
12 {
13     protected Entity $entity;
14     protected array $userRoleIds;
15     protected string $action;
16     protected int $userId;
17
18     public function __construct(Entity $entity, int $userId, array $userRoleIds, string $action)
19     {
20         $this->entity = $entity;
21         $this->userId = $userId;
22         $this->userRoleIds = $userRoleIds;
23         $this->action = $action;
24     }
25
26     public function evaluate(): ?bool
27     {
28         if ($this->isUserSystemAdmin()) {
29             return true;
30         }
31
32         $typeIdChain = $this->gatherEntityChainTypeIds();
33         $relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain);
34         $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
35
36         // Return grant or reject from role-level if exists
37         if (count($permitsByType['role']) > 0) {
38             return boolval(max($permitsByType['role']));
39         }
40
41         // Return fallback permission if exists
42         if (count($permitsByType['fallback']) > 0) {
43             return boolval($permitsByType['fallback'][0]);
44         }
45
46         return null;
47     }
48
49     /**
50      * @param string[] $typeIdChain
51      * @param array<string, EntityPermission[]> $permissionMapByTypeId
52      * @return array<string, array<string, int>>
53      */
54     protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
55     {
56         $permitsByType = ['fallback' => [], 'role' => []];
57
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};
65                 }
66             }
67         }
68
69         return $permitsByType;
70     }
71
72     /**
73      * @param string[] $typeIdChain
74      * @return array<string, EntityPermission[]>
75      */
76     protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array
77     {
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);
85                     });
86                 }
87             })->where(function (Builder $query) {
88                 $query->whereIn('role_id', [...$this->userRoleIds, 0]);
89             })->get(['entity_id', 'entity_type', 'role_id', $this->action])
90             ->all();
91
92         $map = [];
93         foreach ($relevantPermissions as $permission) {
94             $key = $permission->entity_type . ':' . $permission->entity_id;
95             if (!isset($map[$key])) {
96                 $map[$key] = [];
97             }
98
99             $map[$key][] = $permission;
100         }
101
102         return $map;
103     }
104
105     /**
106      * @return string[]
107      */
108     protected function gatherEntityChainTypeIds(): array
109     {
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.
112
113         $chain = [$this->entity->getMorphClass() . ':' . $this->entity->id];
114
115         if ($this->entity instanceof Page && $this->entity->chapter_id) {
116             $chain[] = 'chapter:' . $this->entity->chapter_id;
117         }
118
119         if ($this->entity instanceof Page || $this->entity instanceof Chapter) {
120             $chain[] = 'book:' . $this->entity->book_id;
121         }
122
123         return $chain;
124     }
125
126     protected function isUserSystemAdmin(): bool
127     {
128         $adminRoleId = Role::getSystemRole('admin')->id;
129         return in_array($adminRoleId, $this->userRoleIds);
130     }
131 }