Passed
Pull Request — master (#99)
by Damien
03:55
created

AuditReader::checkRoles()   B

Complexity

Conditions 8
Paths 20

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 12
c 0
b 0
f 0
nc 20
nop 1
dl 0
loc 28
rs 8.4444
1
<?php
2
3
namespace DH\DoctrineAuditBundle\Reader;
4
5
use DH\DoctrineAuditBundle\AuditConfiguration;
6
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
7
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
8
use DH\DoctrineAuditBundle\User\UserInterface;
9
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
10
use Doctrine\DBAL\Query\QueryBuilder;
11
use Doctrine\DBAL\Statement;
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
14
use Pagerfanta\Adapter\DoctrineDbalSingleTableAdapter;
15
use Pagerfanta\Pagerfanta;
16
use Symfony\Component\Security\Core\Security;
17
18
class AuditReader
19
{
20
    public const UPDATE = 'update';
21
    public const ASSOCIATE = 'associate';
22
    public const DISSOCIATE = 'dissociate';
23
    public const INSERT = 'insert';
24
    public const REMOVE = 'remove';
25
26
    public const PAGE_SIZE = 50;
27
28
    /**
29
     * @var AuditConfiguration
30
     */
31
    private $configuration;
32
33
    /**
34
     * @var EntityManagerInterface
35
     */
36
    private $entityManager;
37
38
    /**
39
     * @var ?string
40
     */
41
    private $filter;
42
43
    /**
44
     * AuditReader constructor.
45
     *
46
     * @param AuditConfiguration     $configuration
47
     * @param EntityManagerInterface $entityManager
48
     */
49
    public function __construct(
50
        AuditConfiguration $configuration,
51
        EntityManagerInterface $entityManager
52
    ) {
53
        $this->configuration = $configuration;
54
        $this->entityManager = $entityManager;
55
    }
56
57
    /**
58
     * @return AuditConfiguration
59
     */
60
    public function getConfiguration(): AuditConfiguration
61
    {
62
        return $this->configuration;
63
    }
64
65
    /**
66
     * Set the filter for AuditEntry retrieving.
67
     *
68
     * @param string $filter
69
     *
70
     * @return AuditReader
71
     */
72
    public function filterBy(string $filter): self
73
    {
74
        if (!\in_array($filter, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true)) {
75
            $this->filter = null;
76
        } else {
77
            $this->filter = $filter;
78
        }
79
80
        return $this;
81
    }
82
83
    /**
84
     * Returns current filter.
85
     *
86
     * @return null|string
87
     */
88
    public function getFilter(): ?string
89
    {
90
        return $this->filter;
91
    }
92
93
    /**
94
     * Returns an array of audit table names indexed by entity FQN.
95
     *
96
     * @throws \Doctrine\ORM\ORMException
97
     *
98
     * @return array
99
     */
100
    public function getEntities(): array
101
    {
102
        $metadataDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl();
103
        $entities = [];
104
        if (null !== $metadataDriver) {
105
            $entities = $metadataDriver->getAllClassNames();
106
        }
107
        $audited = [];
108
        foreach ($entities as $entity) {
109
            if ($this->configuration->isAuditable($entity)) {
110
                $audited[$entity] = $this->getEntityTableName($entity);
111
            }
112
        }
113
        ksort($audited);
114
115
        return $audited;
116
    }
117
118
    /**
119
     * Returns an array of audited entries/operations.
120
     *
121
     * @param object|string $entity
122
     * @param int|string    $id
123
     * @param null|int      $page
124
     * @param null|int      $pageSize
125
     * @param null|string   $transactionHash
126
     * @param null|bool     $strict
127
     *
128
     * @throws AccessDeniedException
129
     * @throws InvalidArgumentException
130
     *
131
     * @return array
132
     */
133
    public function getAudits($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, ?bool $strict = true): array
134
    {
135
        $this->checkAuditable($entity);
136
        $this->checkRoles($entity);
137
138
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type integer and string; however, parameter $id of DH\DoctrineAuditBundle\R...getAuditsQueryBuilder() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
        $queryBuilder = $this->getAuditsQueryBuilder($entity, /** @scrutinizer ignore-type */ $id, $page, $pageSize, $transactionHash, $strict);
Loading history...
Bug introduced by
It seems like $strict can also be of type null; however, parameter $strict of DH\DoctrineAuditBundle\R...getAuditsQueryBuilder() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, /** @scrutinizer ignore-type */ $strict);
Loading history...
139
140
        /** @var Statement $statement */
141
        $statement = $queryBuilder->execute();
142
        $statement->setFetchMode(\PDO::FETCH_CLASS, AuditEntry::class);
143
144
        return $statement->fetchAll();
145
    }
146
147
    /**
148
     * Returns an array of all audited entries/operations for a given transaction hash
149
     * indexed by entity FQCN.
150
     *
151
     * @param string $transactionHash
152
     *
153
     * @throws InvalidArgumentException
154
     * @throws \Doctrine\ORM\ORMException
155
     *
156
     * @return array
157
     */
158
    public function getAuditsByTransactionHash(string $transactionHash): array
159
    {
160
        $results = [];
161
162
        $entities = $this->getEntities();
163
        foreach ($entities as $entity => $tablename) {
164
            try {
165
                $audits = $this->getAudits($entity, null, null, null, $transactionHash);
166
                if (\count($audits) > 0) {
167
                    $results[$entity] = $audits;
168
                }
169
            } catch (AccessDeniedException $e) {
170
                // acces denied
171
            }
172
        }
173
174
        return $results;
175
    }
176
177
    /**
178
     * Returns an array of audited entries/operations.
179
     *
180
     * @param object|string $entity
181
     * @param int|string    $id
182
     * @param int           $page
183
     * @param int           $pageSize
184
     *
185
     * @return Pagerfanta
186
     */
187
    public function getAuditsPager($entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): Pagerfanta
188
    {
189
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type integer and string; however, parameter $id of DH\DoctrineAuditBundle\R...getAuditsQueryBuilder() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

189
        $queryBuilder = $this->getAuditsQueryBuilder($entity, /** @scrutinizer ignore-type */ $id);
Loading history...
190
191
        $adapter = new DoctrineDbalSingleTableAdapter($queryBuilder, 'at.id');
192
193
        $pagerfanta = new Pagerfanta($adapter);
194
        $pagerfanta
195
            ->setMaxPerPage($pageSize)
196
            ->setCurrentPage($page)
197
        ;
198
199
        return $pagerfanta;
200
    }
201
202
    /**
203
     * Returns the amount of audited entries/operations.
204
     *
205
     * @param object|string $entity
206
     * @param int|string    $id
207
     *
208
     * @return int
209
     */
210
    public function getAuditsCount($entity, $id = null): int
211
    {
212
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type integer and string; however, parameter $id of DH\DoctrineAuditBundle\R...getAuditsQueryBuilder() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
        $queryBuilder = $this->getAuditsQueryBuilder($entity, /** @scrutinizer ignore-type */ $id);
Loading history...
213
214
        $result = $queryBuilder
215
            ->resetQueryPart('select')
216
            ->resetQueryPart('orderBy')
217
            ->select('COUNT(id)')
218
            ->execute()
219
            ->fetchColumn(0)
220
        ;
221
222
        return false === $result ? 0 : $result;
223
    }
224
225
    /**
226
     * Returns an array of audited entries/operations.
227
     *
228
     * @param $entity
229
     * @param null        $id
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
230
     * @param null|int    $page
231
     * @param null|int    $pageSize
232
     * @param null|string $transactionHash
233
     * @param null|bool   $strict
234
     *
235
     * @throws AccessDeniedException
236
     * @throws InvalidArgumentException
237
     *
238
     * @return QueryBuilder
239
     */
240
    private function getAuditsQueryBuilder($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): QueryBuilder
241
    {
242
        $this->checkAuditable($entity);
243
        $this->checkRoles($entity);
244
245
        if (null !== $page && $page < 1) {
246
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
247
        }
248
249
        if (null !== $pageSize && $pageSize < 1) {
250
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
251
        }
252
253
        $storage = $this->selectStorage();
254
        $connection = $storage->getConnection();
255
256
        $queryBuilder = $connection->createQueryBuilder();
257
        $queryBuilder
258
            ->select('*')
259
            ->from($this->getEntityAuditTableName($entity), 'at')
260
            ->orderBy('created_at', 'DESC')
261
            ->addOrderBy('id', 'DESC')
262
        ;
263
264
        $metadata = $this->getClassMetadata($entity);
265
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
266
            $queryBuilder
267
                ->andWhere('discriminator = :discriminator')
268
                ->setParameter('discriminator', \is_object($entity) ? \get_class($entity) : $entity)
269
            ;
270
        }
271
272
        if (null !== $pageSize) {
273
            $queryBuilder
274
                ->setFirstResult(($page - 1) * $pageSize)
275
                ->setMaxResults($pageSize)
276
            ;
277
        }
278
279
        if (null !== $id) {
0 ignored issues
show
introduced by
The condition null !== $id is always false.
Loading history...
280
            $queryBuilder
281
                ->andWhere('object_id = :object_id')
282
                ->setParameter('object_id', $id)
283
            ;
284
        }
285
286
        if (null !== $this->filter) {
287
            $queryBuilder
288
                ->andWhere('type = :filter')
289
                ->setParameter('filter', $this->filter)
290
            ;
291
        }
292
293
        if (null !== $transactionHash) {
294
            $queryBuilder
295
                ->andWhere('transaction_hash = :transaction_hash')
296
                ->setParameter('transaction_hash', $transactionHash)
297
            ;
298
        }
299
300
        return $queryBuilder;
301
    }
302
303
    /**
304
     * @param $entity
305
     * @param $id
306
     *
307
     * @throws AccessDeniedException
308
     * @throws InvalidArgumentException
309
     *
310
     * @return mixed[]
311
     */
312
    public function getAudit($entity, $id)
313
    {
314
        $this->checkAuditable($entity);
315
        $this->checkRoles($entity);
316
317
        $connection = $this->entityManager->getConnection();
318
319
        /**
320
         * @var \Doctrine\DBAL\Query\QueryBuilder
321
         */
322
        $queryBuilder = $connection->createQueryBuilder();
323
        $queryBuilder
324
            ->select('*')
325
            ->from($this->getEntityAuditTableName($entity))
326
            ->where('id = :id')
327
            ->setParameter('id', $id)
328
        ;
329
330
        if (null !== $this->filter) {
331
            $queryBuilder
332
                ->andWhere('type = :filter')
333
                ->setParameter('filter', $this->filter)
334
            ;
335
        }
336
337
        /** @var Statement $statement */
338
        $statement = $queryBuilder->execute();
339
        $statement->setFetchMode(\PDO::FETCH_CLASS, AuditEntry::class);
340
341
        return $statement->fetchAll();
342
    }
343
344
    /**
345
     * @param $entity
346
     *
347
     * @return ClassMetadata
348
     */
349
    private function getClassMetadata($entity): ClassMetadata
350
    {
351
        return $this
352
            ->entityManager
353
            ->getClassMetadata($entity)
354
        ;
355
    }
356
357
    /**
358
     * Returns the table name of $entity.
359
     *
360
     * @param object|string $entity
361
     *
362
     * @return string
363
     */
364
    public function getEntityTableName($entity): string
365
    {
366
        return $this->getClassMetadata($entity)
367
            ->getTableName()
368
        ;
369
    }
370
371
    /**
372
     * Returns the audit table name for $entity.
373
     *
374
     * @param mixed $entity
375
     *
376
     * @return string
377
     */
378
    public function getEntityAuditTableName($entity): string
379
    {
380
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
381
        $schema = $this->getClassMetadata($entityName)->getSchemaName() ? $this->getClassMetadata($entityName)->getSchemaName().'.' : '';
382
383
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entityName), $this->configuration->getTableSuffix());
384
    }
385
386
    /**
387
     * @return EntityManagerInterface
388
     */
389
    private function selectStorage(): EntityManagerInterface
390
    {
391
        return $this->configuration->getEntityManager() ?? $this->entityManager;
392
    }
393
394
    /**
395
     * @return EntityManagerInterface
396
     */
397
    public function getEntityManager(): EntityManagerInterface
398
    {
399
        return $this->entityManager;
400
    }
401
402
    /**
403
     * Throws an InvalidArgumentException if given entity is not auditable.
404
     *
405
     * @param $entity
406
     *
407
     * @throws InvalidArgumentException
408
     */
409
    private function checkAuditable($entity): void
410
    {
411
        if (!$this->configuration->isAuditable($entity)) {
412
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
413
        }
414
    }
415
416
    /**
417
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
418
     *
419
     * @param $entity
420
     *
421
     * @throws AccessDeniedException
422
     */
423
    private function checkRoles($entity): void
424
    {
425
        $userProvider = $this->configuration->getUserProvider();
426
        $user = null === $userProvider ? null : $userProvider->getUser();
427
        $security = null === $userProvider ? null : $userProvider->getSecurity();
0 ignored issues
show
Bug introduced by
The method getSecurity() does not exist on DH\DoctrineAuditBundle\User\UserProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\DoctrineAuditBundle\User\UserProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

427
        $security = null === $userProvider ? null : $userProvider->/** @scrutinizer ignore-call */ getSecurity();
Loading history...
428
429
        if (!($user instanceof UserInterface) || !($security instanceof Security)) {
430
            // If no security defined or no user identified, consider access granted
431
            return;
432
        }
433
434
        $entities = $this->configuration->getEntities();
435
436
        if (null === $entities[$entity]['roles']) {
437
            // If no roles are configured, consider access granted
438
            return;
439
        }
440
441
        // roles are defined
442
        foreach ($entities[$entity]['roles'] as $role) {
443
            if ($security->isGranted($role)) {
444
                // role granted => access granted
445
                return;
446
            }
447
        }
448
449
        // access denied
450
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
451
    }
452
}
453