Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.38% |
84 / 89 |
|
71.43% |
10 / 14 |
CRAP | |
0.00% |
0 / 1 |
SecureResourceService | |
94.38% |
84 / 89 |
|
71.43% |
10 / 14 |
35.22 | |
0.00% |
0 / 1 |
getAllChildren | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
getInstances | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
6 | |||
hasAccess | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
validateResourceUriPermissions | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
validatePermissions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
validatePermission | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
3.01 | |||
getClass | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
getParentUris | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getPermissionProvider | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
throwResourceAccessDeniedException | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getAdvancedLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFeatureFlagChecker | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOntology | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2013-2021 (original work) Open Assessment Technologies SA; |
19 | */ |
20 | |
21 | declare(strict_types=1); |
22 | |
23 | namespace oat\tao\model\resources; |
24 | |
25 | use core_kernel_classes_Property; |
26 | use oat\generis\model\data\Ontology; |
27 | use oat\oatbox\user\User; |
28 | use common_exception_Error; |
29 | use oat\tao\model\featureFlag\FeatureFlagChecker; |
30 | use oat\tao\model\featureFlag\FeatureFlagCheckerInterface; |
31 | use oat\tao\model\TaoOntology; |
32 | use Psr\Log\LoggerInterface; |
33 | use core_kernel_classes_Class; |
34 | use core_kernel_classes_Resource; |
35 | use oat\oatbox\session\SessionService; |
36 | use oat\oatbox\log\logger\AdvancedLogger; |
37 | use oat\oatbox\service\ConfigurableService; |
38 | use oat\generis\model\data\permission\PermissionInterface; |
39 | use oat\oatbox\log\logger\extender\ContextExtenderInterface; |
40 | |
41 | class SecureResourceService extends ConfigurableService implements SecureResourceServiceInterface |
42 | { |
43 | private const HIGHEST_PARENT_URI = 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject'; |
44 | |
45 | /** @var User */ |
46 | private $user; |
47 | |
48 | /** @var ?bool */ |
49 | private $ignoreTranslations; |
50 | |
51 | /** @var ?core_kernel_classes_Property */ |
52 | private $originalUriProperty; |
53 | |
54 | /** |
55 | * @inheritDoc |
56 | * |
57 | * @throws common_exception_Error |
58 | */ |
59 | public function getAllChildren(core_kernel_classes_Class $resource): array |
60 | { |
61 | $subClasses = $resource->getSubClasses(false); |
62 | $accessibleInstances = [[]]; |
63 | $permissionService = $this->getPermissionProvider(); |
64 | |
65 | if ($this->ignoreTranslations === null) { |
66 | $this->ignoreTranslations = $this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_TRANSLATION_ENABLED'); |
67 | $this->originalUriProperty = $this->getOntology() |
68 | ->getProperty(TaoOntology::PROPERTY_TRANSLATION_ORIGINAL_RESOURCE_URI); |
69 | } |
70 | |
71 | if ($subClasses) { |
72 | foreach ($subClasses as $subClass) { |
73 | $classUri = $subClass->getUri(); |
74 | $classPermissions = $permissionService->getPermissions($this->getUser(), [$classUri]); |
75 | |
76 | if ($this->hasAccess($classPermissions[$classUri])) { |
77 | $accessibleInstances[] = $this->getAllChildren($subClass); |
78 | } |
79 | } |
80 | } |
81 | |
82 | return array_merge( |
83 | $this->getInstances($resource), |
84 | ...$accessibleInstances |
85 | ); |
86 | } |
87 | |
88 | /** |
89 | * @return core_kernel_classes_Resource[] |
90 | * @throws common_exception_Error |
91 | */ |
92 | private function getInstances(core_kernel_classes_Class $class): array |
93 | { |
94 | $instances = $class->getInstances(false); |
95 | |
96 | if (!$instances) { |
97 | return []; |
98 | } |
99 | |
100 | $childrenUris = array_map( |
101 | static function (core_kernel_classes_Resource $child) { |
102 | return $child->getUri(); |
103 | }, |
104 | $instances |
105 | ); |
106 | |
107 | $permissions = $this->getPermissionProvider()->getPermissions( |
108 | $this->getUser(), |
109 | $childrenUris |
110 | ); |
111 | |
112 | $accessibleInstances = []; |
113 | |
114 | foreach ($instances as $child) { |
115 | if ($this->ignoreTranslations && !empty($child->getOnePropertyValue($this->originalUriProperty))) { |
116 | continue; |
117 | } |
118 | |
119 | $uri = $child->getUri(); |
120 | |
121 | if ($this->hasAccess($permissions[$uri])) { |
122 | $accessibleInstances[$uri] = $child; |
123 | } |
124 | } |
125 | |
126 | return $accessibleInstances; |
127 | } |
128 | |
129 | private function hasAccess(array $permissions, array $permissionsToCheck = ['READ']): bool |
130 | { |
131 | return $permissions === [PermissionInterface::RIGHT_UNSUPPORTED] |
132 | || empty(array_diff($permissionsToCheck, $permissions)); |
133 | } |
134 | |
135 | /** |
136 | * @param string[] $resourceUris |
137 | * @param string[] $permissionsToCheck |
138 | * |
139 | * @throws common_exception_Error |
140 | */ |
141 | private function validateResourceUriPermissions(array $resourceUris, array $permissionsToCheck): void |
142 | { |
143 | $permissionService = $this->getPermissionProvider(); |
144 | |
145 | $permissions = $permissionService->getPermissions( |
146 | $this->getUser(), |
147 | $resourceUris |
148 | ); |
149 | |
150 | foreach ($permissions as $uri => $permission) { |
151 | if (empty($permission) || !$this->hasAccess($permission, $permissionsToCheck)) { |
152 | $this->throwResourceAccessDeniedException($uri); |
153 | } |
154 | } |
155 | } |
156 | |
157 | /** |
158 | * @param core_kernel_classes_Resource[] $resources |
159 | * @param string[] $permissionsToCheck |
160 | * |
161 | * @throws common_exception_Error |
162 | */ |
163 | public function validatePermissions(iterable $resources, array $permissionsToCheck): void |
164 | { |
165 | foreach ($resources as $resource) { |
166 | $this->validatePermission($resource, $permissionsToCheck); |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * @param core_kernel_classes_Resource|string $resource |
172 | * @param array $permissionsToCheck |
173 | * |
174 | * @throws common_exception_Error |
175 | */ |
176 | public function validatePermission($resource, array $permissionsToCheck): void |
177 | { |
178 | $permissionService = $this->getPermissionProvider(); |
179 | |
180 | if (is_string($resource)) { |
181 | $resource = new core_kernel_classes_Resource($resource); |
182 | } |
183 | |
184 | $resourceUri = $resource->getUri(); |
185 | $permissions = $permissionService->getPermissions($this->getUser(), [$resourceUri]); |
186 | |
187 | if (!$this->hasAccess($permissions[$resourceUri], $permissionsToCheck)) { |
188 | $this->throwResourceAccessDeniedException($resourceUri); |
189 | } |
190 | |
191 | $parentUris = $this->getParentUris( |
192 | $this->getClass($resource) |
193 | ); |
194 | |
195 | $this->validateResourceUriPermissions($parentUris, $permissionsToCheck); |
196 | } |
197 | |
198 | private function getClass(core_kernel_classes_Resource $resource): core_kernel_classes_Class |
199 | { |
200 | if ($resource instanceof core_kernel_classes_Class) { |
201 | return $resource; |
202 | } |
203 | |
204 | // fetch parent class |
205 | if (!$resource->isClass()) { |
206 | return current($resource->getTypes()); |
207 | } |
208 | |
209 | // the last chance to fetch class form DB |
210 | return $resource->getClass($resource->getUri()); |
211 | } |
212 | |
213 | private function getParentUris(core_kernel_classes_Class $parent): array |
214 | { |
215 | $parentUris = [$parent->getUri()]; |
216 | |
217 | while ($parentList = $parent->getParentClasses(false)) { |
218 | $parent = current($parentList); |
219 | if ($parent->getUri() === self::HIGHEST_PARENT_URI) { |
220 | break; |
221 | } |
222 | $parentUris[] = $parent->getUri(); |
223 | } |
224 | |
225 | return $parentUris; |
226 | } |
227 | |
228 | private function getPermissionProvider(): PermissionInterface |
229 | { |
230 | /** @noinspection PhpIncompatibleReturnTypeInspection */ |
231 | return $this->getServiceManager()->get(PermissionInterface::SERVICE_ID); |
232 | } |
233 | |
234 | /** |
235 | * @return User |
236 | * |
237 | * @throws common_exception_Error |
238 | */ |
239 | private function getUser(): User |
240 | { |
241 | if ($this->user === null) { |
242 | $this->user = $this |
243 | ->getServiceManager() |
244 | ->get(SessionService::SERVICE_ID) |
245 | ->getCurrentUser(); |
246 | } |
247 | |
248 | return $this->user; |
249 | } |
250 | |
251 | private function throwResourceAccessDeniedException(string $uri): void |
252 | { |
253 | $exception = new ResourceAccessDeniedException($uri); |
254 | $this->getAdvancedLogger()->error( |
255 | $exception->getMessage(), |
256 | [ContextExtenderInterface::CONTEXT_EXCEPTION => $exception] |
257 | ); |
258 | |
259 | throw $exception; |
260 | } |
261 | |
262 | private function getAdvancedLogger(): LoggerInterface |
263 | { |
264 | return $this->getServiceManager()->getContainer()->get(AdvancedLogger::ACL_SERVICE_ID); |
265 | } |
266 | |
267 | private function getFeatureFlagChecker(): FeatureFlagCheckerInterface |
268 | { |
269 | return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class); |
270 | } |
271 | |
272 | private function getOntology(): Ontology |
273 | { |
274 | return $this->getServiceManager()->getContainer()->get(Ontology::SERVICE_ID); |
275 | } |
276 | } |