]> BookStack Code Mirror - bookstack/blob - app/Permissions/EntityPermissionEvaluator.php
06f0126ad290908ffdc86a2d3c1b73583cc3f89e
[bookstack] / app / Permissions / EntityPermissionEvaluator.php
1 <?php
2
3 namespace BookStack\Permissions;
4
5 use BookStack\Entities\Models\Entity;
6 use BookStack\Permissions\Models\EntityPermission;
7 use BookStack\Users\Models\Role;
8 use Illuminate\Database\Eloquent\Builder;
9
10 class EntityPermissionEvaluator
11 {
12     protected string $action;
13
14     public function __construct(string $action)
15     {
16         $this->action = $action;
17     }
18
19     public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
20     {
21         if ($this->isUserSystemAdmin($userRoleIds)) {
22             return true;
23         }
24
25         $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
26         $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
27         $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
28
29         $status = $this->evaluatePermitsByType($permitsByType);
30
31         return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
32     }
33
34     /**
35      * @param array<string, array<string, int>> $permitsByType
36      */
37     protected function evaluatePermitsByType(array $permitsByType): ?int
38     {
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;
42         }
43
44         // Return fallback permission if exists
45         if (count($permitsByType['fallback']) > 0) {
46             return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
47         }
48
49         return null;
50     }
51
52     /**
53      * @param string[] $typeIdChain
54      * @param array<string, EntityPermission[]> $permissionMapByTypeId
55      * @return array<string, array<string, int>>
56      */
57     protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
58     {
59         $permitsByType = ['fallback' => [], 'role' => []];
60
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};
68                 }
69             }
70
71             if (isset($permitsByType['fallback'][0])) {
72                 break;
73             }
74         }
75
76         return $permitsByType;
77     }
78
79     /**
80      * @param string[] $typeIdChain
81      * @return array<string, EntityPermission[]>
82      */
83     protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
84     {
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);
91                 });
92             }
93         });
94
95         if (!empty($filterRoleIds)) {
96             $query->where(function (Builder $query) use ($filterRoleIds) {
97                 $query->whereIn('role_id', [...$filterRoleIds, 0]);
98             });
99         }
100
101         $relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
102
103         $map = [];
104         foreach ($relevantPermissions as $permission) {
105             $key = $permission->entity_type . ':' . $permission->entity_id;
106             if (!isset($map[$key])) {
107                 $map[$key] = [];
108             }
109
110             $map[$key][] = $permission;
111         }
112
113         return $map;
114     }
115
116     /**
117      * @return string[]
118      */
119     protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
120     {
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.
123
124         $chain = [$entity->type . ':' . $entity->id];
125
126         if ($entity->type === 'page' && $entity->chapter_id) {
127             $chain[] = 'chapter:' . $entity->chapter_id;
128         }
129
130         if ($entity->type === 'page' || $entity->type === 'chapter') {
131             $chain[] = 'book:' . $entity->book_id;
132         }
133
134         return $chain;
135     }
136
137     protected function isUserSystemAdmin($userRoleIds): bool
138     {
139         $adminRoleId = Role::getSystemRole('admin')->id;
140         return in_array($adminRoleId, $userRoleIds);
141     }
142 }