Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.38% covered (success)
94.38%
84 / 89
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SecureResourceService
94.38% covered (success)
94.38%
84 / 89
71.43% covered (warning)
71.43%
10 / 14
35.22
0.00% covered (danger)
0.00%
0 / 1
 getAllChildren
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 getInstances
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 hasAccess
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 validateResourceUriPermissions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 validatePermissions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 validatePermission
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 getClass
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 getParentUris
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getPermissionProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 throwResourceAccessDeniedException
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getAdvancedLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFeatureFlagChecker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOntology
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
21declare(strict_types=1);
22
23namespace oat\tao\model\resources;
24
25use core_kernel_classes_Property;
26use oat\generis\model\data\Ontology;
27use oat\oatbox\user\User;
28use common_exception_Error;
29use oat\tao\model\featureFlag\FeatureFlagChecker;
30use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
31use oat\tao\model\TaoOntology;
32use Psr\Log\LoggerInterface;
33use core_kernel_classes_Class;
34use core_kernel_classes_Resource;
35use oat\oatbox\session\SessionService;
36use oat\oatbox\log\logger\AdvancedLogger;
37use oat\oatbox\service\ConfigurableService;
38use oat\generis\model\data\permission\PermissionInterface;
39use oat\oatbox\log\logger\extender\ContextExtenderInterface;
40
41class 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}