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