Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
27.18% |
53 / 195 |
|
27.78% |
5 / 18 |
CRAP | |
0.00% |
0 / 1 |
DataBaseAccess | |
27.18% |
53 / 195 |
|
27.78% |
5 / 18 |
826.96 | |
0.00% |
0 / 1 |
setWriteChunkSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPermissionsByUsersAndResources | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
changeAccess | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
132 | |||
getUsersWithPermissions | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getPermissions | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
3 | |||
getResourcesPermissions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addPermissions | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
getResourcePermissions | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
removePermissions | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
removeAllPermissions | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
checkPermissions | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
removeTables | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
createTables | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getEventManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPersistence | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
fetchQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
insertPermissions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
addEventValue | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 |
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) 2014-2023 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); |
19 | */ |
20 | |
21 | declare(strict_types=1); |
22 | |
23 | namespace oat\taoDacSimple\model; |
24 | |
25 | use common_persistence_SqlPersistence; |
26 | use oat\oatbox\event\EventManager; |
27 | use oat\oatbox\service\ConfigurableService; |
28 | use oat\taoDacSimple\model\Command\ChangeAccessCommand; |
29 | use oat\taoDacSimple\model\event\DacAddedEvent; |
30 | use oat\taoDacSimple\model\event\DacChangedEvent; |
31 | use oat\taoDacSimple\model\event\DacRemovedEvent; |
32 | use oat\generis\persistence\PersistenceManager; |
33 | use PDO; |
34 | use Throwable; |
35 | |
36 | /** |
37 | * Class to handle the storage and retrieval of permissions |
38 | * |
39 | * @author Antoine Robin <antoine.robin@vesperiagroup.com> |
40 | * @author Joel Bout <joel@taotesting.com> |
41 | */ |
42 | class DataBaseAccess extends ConfigurableService |
43 | { |
44 | public const SERVICE_ID = 'taoDacSimple/DataBaseAccess'; |
45 | |
46 | public const OPTION_PERSISTENCE = 'persistence'; |
47 | public const OPTION_FETCH_USER_PERMISSIONS_CHUNK_SIZE = 'fetch_user_permissions_chunk_size'; |
48 | |
49 | public const COLUMN_USER_ID = 'user_id'; |
50 | public const COLUMN_RESOURCE_ID = 'resource_id'; |
51 | public const COLUMN_PRIVILEGE = 'privilege'; |
52 | public const TABLE_PRIVILEGES_NAME = 'data_privileges'; |
53 | public const INDEX_RESOURCE_ID = 'data_privileges_resource_id_index'; |
54 | |
55 | private $writeChunkSize = 1000; |
56 | |
57 | private $persistence; |
58 | |
59 | public function setWriteChunkSize(int $size): void |
60 | { |
61 | $this->writeChunkSize = $size; |
62 | } |
63 | |
64 | /** |
65 | * @return array [ |
66 | * '{resourceId}' => [ |
67 | * '{userId}' => ['GRANT'], |
68 | * ] |
69 | * ] |
70 | */ |
71 | public function getPermissionsByUsersAndResources(array $userIds, array $resourceIds): array |
72 | { |
73 | if (empty($resourceIds) || empty($userIds)) { |
74 | return []; |
75 | } |
76 | |
77 | // phpcs:disable Generic.Files.LineLength |
78 | $results = $this->fetchQuery( |
79 | sprintf( |
80 | 'SELECT resource_id, user_id, privilege FROM data_privileges WHERE resource_id IN (%s) AND user_id IN (%s)', |
81 | implode(',', array_fill(0, count($resourceIds), '?')), |
82 | implode(',', array_fill(0, count($userIds), '?')) |
83 | ), |
84 | [ |
85 | ...$resourceIds, |
86 | ...$userIds |
87 | ] |
88 | ); |
89 | // phpcs:disable Generic.Files.LineLength |
90 | |
91 | $data = array_fill_keys($resourceIds, []); |
92 | |
93 | foreach ($results as $result) { |
94 | $data[$result[self::COLUMN_RESOURCE_ID]][$result[self::COLUMN_USER_ID]][] = $result[self::COLUMN_PRIVILEGE]; |
95 | } |
96 | |
97 | return $data; |
98 | } |
99 | |
100 | /** |
101 | * Allow to grant/revoke access for several users and resources |
102 | */ |
103 | public function changeAccess(ChangeAccessCommand $command): void |
104 | { |
105 | $persistence = $this->getPersistence(); |
106 | |
107 | $persistence->transactional(function () use ($command, $persistence): void { |
108 | $removed = []; |
109 | |
110 | foreach ($command->getUserIdsToRevokePermissions() as $userId) { |
111 | $permissions = $command->getUserPermissionsToRevoke($userId); |
112 | |
113 | foreach ($permissions as $permission) { |
114 | $resourceIds = $command->getResourceIdsByUserAndPermissionToRevoke($userId, $permission); |
115 | |
116 | if (!empty($resourceIds)) { |
117 | foreach (array_chunk($resourceIds, $this->writeChunkSize) as $batch) { |
118 | // phpcs:disable Generic.Files.LineLength |
119 | $persistence->exec( |
120 | sprintf( |
121 | 'DELETE FROM data_privileges WHERE user_id = ? AND privilege = ? AND resource_id IN (%s)', |
122 | implode(',', array_fill(0, count($batch), '?')), |
123 | ), |
124 | array_merge([$userId, $permission], $batch) |
125 | ); |
126 | // phpcs:enable Generic.Files.LineLength |
127 | } |
128 | |
129 | foreach ($resourceIds as $resourceId) { |
130 | $this->addEventValue($removed, $userId, $resourceId, $permission); |
131 | } |
132 | } |
133 | } |
134 | } |
135 | |
136 | $insert = []; |
137 | $added = []; |
138 | |
139 | foreach ($command->getResourceIdsToGrant() as $resourceId) { |
140 | foreach (PermissionProvider::ALLOWED_PERMISSIONS as $permission) { |
141 | $usersIds = $command->getUserIdsToGrant($resourceId, $permission); |
142 | |
143 | foreach ($usersIds as $userId) { |
144 | $insert[] = [ |
145 | 'user_id' => $userId, |
146 | 'resource_id' => $resourceId, |
147 | 'privilege' => $permission, |
148 | ]; |
149 | |
150 | $this->addEventValue($added, $userId, $resourceId, $permission); |
151 | } |
152 | } |
153 | } |
154 | |
155 | $this->insertPermissions($insert); |
156 | |
157 | if (!empty($added) || !empty($removed)) { |
158 | $this->getEventManager()->trigger(new DacChangedEvent($added, $removed)); |
159 | } |
160 | }); |
161 | } |
162 | |
163 | /** |
164 | * Retrieve info on users having privileges on a set of resources |
165 | * |
166 | * @return array [ |
167 | * [ |
168 | * '{resourceId}', |
169 | * '{userId}', |
170 | * '{privilege}' |
171 | * ] |
172 | * ] |
173 | */ |
174 | public function getUsersWithPermissions(array $resourceIds): array |
175 | { |
176 | $inQuery = implode(',', array_fill(0, count($resourceIds), '?')); |
177 | $query = sprintf( |
178 | 'SELECT %s, %s, %s FROM %s WHERE %s IN (%s)', |
179 | self::COLUMN_RESOURCE_ID, |
180 | self::COLUMN_USER_ID, |
181 | self::COLUMN_PRIVILEGE, |
182 | self::TABLE_PRIVILEGES_NAME, |
183 | self::COLUMN_RESOURCE_ID, |
184 | $inQuery |
185 | ); |
186 | |
187 | return $this->fetchQuery($query, $resourceIds); |
188 | } |
189 | |
190 | /** |
191 | * Get the permissions for a list of resources and users |
192 | * |
193 | * @return array [ |
194 | * '{resourceId}' => ['READ', 'WRITE'], |
195 | * ] |
196 | */ |
197 | public function getPermissions(array $userIds, array $resourceIds): array |
198 | { |
199 | // Permissions for an empty set of resources must be an empty array |
200 | if (!count($resourceIds)) { |
201 | return []; |
202 | } |
203 | |
204 | $inQueryResource = implode(',', array_fill(0, count($resourceIds), '?')); |
205 | $inQueryUser = implode(',', array_fill(0, count($userIds), '?')); |
206 | $query = sprintf( |
207 | 'SELECT %s, %s FROM %s WHERE %s IN (%s) AND %s IN (%s)', |
208 | self::COLUMN_RESOURCE_ID, |
209 | self::COLUMN_PRIVILEGE, |
210 | self::TABLE_PRIVILEGES_NAME, |
211 | self::COLUMN_RESOURCE_ID, |
212 | $inQueryResource, |
213 | self::COLUMN_USER_ID, |
214 | $inQueryUser |
215 | ); |
216 | |
217 | $params = array_merge(array_values($resourceIds), array_values($userIds)); |
218 | |
219 | //If resource doesn't have permission don't return null |
220 | $returnValue = array_fill_keys($resourceIds, []); |
221 | |
222 | $results = $this->fetchQuery($query, $params); |
223 | foreach ($results as $result) { |
224 | $returnValue[$result[self::COLUMN_RESOURCE_ID]][] = $result[self::COLUMN_PRIVILEGE]; |
225 | } |
226 | return $returnValue; |
227 | } |
228 | |
229 | /** |
230 | * @return array [ |
231 | * '{resourceId}' => [ |
232 | * '{userId}' => ['READ', 'WRITE'], |
233 | * ] |
234 | * ] |
235 | */ |
236 | public function getResourcesPermissions(array $resourceIds): array |
237 | { |
238 | $grants = array_fill_keys($resourceIds, []); |
239 | |
240 | foreach ($this->getUsersWithPermissions($resourceIds) as $entry) { |
241 | $grants[$entry[self::COLUMN_RESOURCE_ID]][$entry[self::COLUMN_USER_ID]][] |
242 | = $entry[self::COLUMN_PRIVILEGE]; |
243 | } |
244 | |
245 | return $grants; |
246 | } |
247 | |
248 | /** |
249 | * Add permissions of a user to a resource |
250 | * |
251 | * @deprecated Please use $this::changeAccess() |
252 | */ |
253 | public function addPermissions(string $user, string $resourceId, array $rights): void |
254 | { |
255 | foreach ($rights as $privilege) { |
256 | $this->getPersistence()->insert( |
257 | self::TABLE_PRIVILEGES_NAME, |
258 | [ |
259 | self::COLUMN_USER_ID => $user, |
260 | self::COLUMN_RESOURCE_ID => $resourceId, |
261 | self::COLUMN_PRIVILEGE => $privilege |
262 | ] |
263 | ); |
264 | } |
265 | |
266 | $this->getEventManager()->trigger(new DacAddedEvent( |
267 | $user, |
268 | $resourceId, |
269 | (array)$rights |
270 | )); |
271 | } |
272 | |
273 | /** |
274 | * Get the permissions to resource |
275 | * |
276 | * @return array [ |
277 | * '{userId}' => [ |
278 | * 'READ', |
279 | * 'WRITE', |
280 | * ] |
281 | * ] |
282 | */ |
283 | public function getResourcePermissions(string $resourceId): array |
284 | { |
285 | $grants = []; |
286 | $query = sprintf( |
287 | 'SELECT %s, %s FROM %s WHERE %s = ?', |
288 | self::COLUMN_USER_ID, |
289 | self::COLUMN_PRIVILEGE, |
290 | self::TABLE_PRIVILEGES_NAME, |
291 | self::COLUMN_RESOURCE_ID |
292 | ); |
293 | |
294 | foreach ($this->fetchQuery($query, [$resourceId]) as $entry) { |
295 | $grants[$entry[self::COLUMN_USER_ID]][] = $entry[self::COLUMN_PRIVILEGE]; |
296 | } |
297 | |
298 | return $grants; |
299 | } |
300 | |
301 | /** |
302 | * remove permissions to a resource for a user |
303 | * |
304 | * @deprecated Please use $this::changeAccess() |
305 | */ |
306 | public function removePermissions(string $user, string $resourceId, array $rights): void |
307 | { |
308 | //get all entries that match (user,resourceId) and remove them |
309 | $inQueryPrivilege = implode(',', array_fill(0, count($rights), ' ? ')); |
310 | $query = sprintf( |
311 | 'DELETE FROM %s WHERE %s = ? AND %s IN (%s) AND %s = ?', |
312 | self::TABLE_PRIVILEGES_NAME, |
313 | self::COLUMN_RESOURCE_ID, |
314 | self::COLUMN_PRIVILEGE, |
315 | $inQueryPrivilege, |
316 | self::COLUMN_USER_ID |
317 | ); |
318 | |
319 | $params = array_merge([$resourceId], array_values($rights), [$user]); |
320 | $this->getPersistence()->exec($query, $params); |
321 | |
322 | $this->getEventManager()->trigger(new DacRemovedEvent( |
323 | $user, |
324 | $resourceId, |
325 | $rights |
326 | )); |
327 | } |
328 | |
329 | /** |
330 | * Completely remove all permissions to any user for the resourceIds |
331 | * |
332 | * @deprecated Please use $this::changeAccess() |
333 | */ |
334 | public function removeAllPermissions(array $resourceIds): void |
335 | { |
336 | //get all entries that match (resourceId) and remove them |
337 | $inQuery = implode(',', array_fill(0, count($resourceIds), ' ? ')); |
338 | $query = sprintf( |
339 | 'DELETE FROM %s WHERE %s IN (%s)', |
340 | self::TABLE_PRIVILEGES_NAME, |
341 | self::COLUMN_RESOURCE_ID, |
342 | $inQuery |
343 | ); |
344 | |
345 | $this->getPersistence()->exec($query, $resourceIds); |
346 | |
347 | foreach ($resourceIds as $resourceId) { |
348 | $this->getEventManager()->trigger(new DacRemovedEvent('-', $resourceId, ['-'])); |
349 | } |
350 | } |
351 | |
352 | /** |
353 | * Filter users\roles that have permissions |
354 | * |
355 | * @return array [ |
356 | * '{userId}' => '{userId}', |
357 | * '{userId}' => '{userId}', |
358 | * ] |
359 | */ |
360 | public function checkPermissions(array $userIds): array |
361 | { |
362 | $chunks = array_chunk($userIds, $this->getOption(self::OPTION_FETCH_USER_PERMISSIONS_CHUNK_SIZE, 20)); |
363 | $existingUsers = []; |
364 | |
365 | foreach ($chunks as $chunkUserIds) { |
366 | $inQueryUser = implode(',', array_fill(0, count($chunkUserIds), ' ? ')); |
367 | $query = sprintf( |
368 | 'SELECT %s FROM %s WHERE %s IN (%s) GROUP BY %s', |
369 | self::COLUMN_USER_ID, |
370 | self::TABLE_PRIVILEGES_NAME, |
371 | self::COLUMN_USER_ID, |
372 | $inQueryUser, |
373 | self::COLUMN_USER_ID |
374 | ); |
375 | $results = $this->fetchQuery($query, array_values($chunkUserIds)); |
376 | foreach ($results as $result) { |
377 | $existingUsers[$result[self::COLUMN_USER_ID]] = $result[self::COLUMN_USER_ID]; |
378 | } |
379 | } |
380 | |
381 | return $existingUsers; |
382 | } |
383 | |
384 | public function removeTables(): void |
385 | { |
386 | $persistence = $this->getPersistence(); |
387 | $schema = $persistence->getDriver()->getSchemaManager()->createSchema(); |
388 | $fromSchema = clone $schema; |
389 | $schema->dropTable(self::TABLE_PRIVILEGES_NAME); |
390 | $queries = $persistence->getPlatform()->getMigrateSchemaSql($fromSchema, $schema); |
391 | foreach ($queries as $query) { |
392 | $persistence->exec($query); |
393 | } |
394 | } |
395 | |
396 | public function createTables(): void |
397 | { |
398 | $schemaManager = $this->getPersistence()->getDriver()->getSchemaManager(); |
399 | $schema = $schemaManager->createSchema(); |
400 | $fromSchema = clone $schema; |
401 | $table = $schema->createtable(self::TABLE_PRIVILEGES_NAME); |
402 | $table->addColumn(self::COLUMN_USER_ID, 'string', ['notnull' => null, 'length' => 255]); |
403 | $table->addColumn(self::COLUMN_RESOURCE_ID, 'string', ['notnull' => null, 'length' => 255]); |
404 | $table->addColumn(self::COLUMN_PRIVILEGE, 'string', ['notnull' => null, 'length' => 255]); |
405 | $table->setPrimaryKey([self::COLUMN_USER_ID, self::COLUMN_RESOURCE_ID, self::COLUMN_PRIVILEGE]); |
406 | $table->addIndex([self::COLUMN_RESOURCE_ID], self::INDEX_RESOURCE_ID); |
407 | |
408 | $queries = $this->getPersistence()->getPlatform()->getMigrateSchemaSql($fromSchema, $schema); |
409 | foreach ($queries as $query) { |
410 | $this->getPersistence()->exec($query); |
411 | } |
412 | } |
413 | |
414 | private function getEventManager(): EventManager |
415 | { |
416 | return $this->getServiceLocator()->get(EventManager::SERVICE_ID); |
417 | } |
418 | |
419 | /** |
420 | * @return common_persistence_SqlPersistence |
421 | */ |
422 | private function getPersistence() |
423 | { |
424 | if (!$this->persistence) { |
425 | $this->persistence = $this->getServiceLocator()->get(PersistenceManager::SERVICE_ID) |
426 | ->getPersistenceById($this->getOption(self::OPTION_PERSISTENCE)); |
427 | } |
428 | return $this->persistence; |
429 | } |
430 | |
431 | private function fetchQuery(string $query, array $params): array |
432 | { |
433 | return $this->getPersistence() |
434 | ->query($query, $params) |
435 | ->fetchAll(PDO::FETCH_ASSOC); |
436 | } |
437 | |
438 | /** |
439 | * @throws Throwable |
440 | */ |
441 | private function insertPermissions(array $insert): void |
442 | { |
443 | if (!empty($insert)) { |
444 | $persistence = $this->getPersistence(); |
445 | |
446 | foreach (array_chunk($insert, $this->writeChunkSize) as $batch) { |
447 | $persistence->insertMultiple(self::TABLE_PRIVILEGES_NAME, $batch); |
448 | } |
449 | } |
450 | } |
451 | |
452 | private function addEventValue(array &$eventData, string $userId, string $resourceId, string $permission): void |
453 | { |
454 | $key = $userId . $resourceId; |
455 | |
456 | if (array_key_exists($key, $eventData)) { |
457 | $eventData[$key]['privileges'][] = $permission; |
458 | |
459 | return; |
460 | } |
461 | |
462 | $eventData[$key] = [ |
463 | 'userId' => $userId, |
464 | 'resourceId' => $resourceId, |
465 | 'privileges' => [$permission], |
466 | ]; |
467 | } |
468 | } |