Completed
Push — master ( 83fb2b...0f847f )
by
unknown
42:12 queued 20:32
created

PermissionResolver   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 423
c 0
b 0
f 0
rs 3.6
wmc 60
lcom 1
cbo 12

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A getCurrentUserReference() 0 4 1
A setCurrentUserReference() 0 9 2
D hasAccess() 0 65 20
C canUser() 0 87 12
C lookupLimitations() 0 66 13
A isGrantedByLimitation() 0 11 1
A isDeniedByRoleLimitation() 0 15 2
A sudo() 0 14 2
B prepareTargetsForType() 0 26 6

How to fix   Complexity   

Complex Class

Complex classes like PermissionResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PermissionResolver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Repository\Permission;
8
9
use eZ\Publish\API\Repository\PermissionResolver as PermissionResolverInterface;
10
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
11
use eZ\Publish\API\Repository\Values\User\Limitation;
12
use eZ\Publish\API\Repository\Values\User\LookupLimitationResult;
13
use eZ\Publish\API\Repository\Values\User\LookupPolicyLimitations;
14
use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
15
use eZ\Publish\API\Repository\Values\ValueObject;
16
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
17
use eZ\Publish\Core\Repository\Helper\LimitationService;
18
use eZ\Publish\Core\Repository\Helper\RoleDomainMapper;
19
use eZ\Publish\SPI\Limitation\Target;
20
use eZ\Publish\SPI\Limitation\TargetAwareType;
21
use eZ\Publish\SPI\Limitation\Type as LimitationType;
22
use eZ\Publish\SPI\Persistence\User\Handler as UserHandler;
23
use Exception;
24
25
/**
26
 * Core implementation of PermissionResolver interface.
27
 */
28
class PermissionResolver implements PermissionResolverInterface
29
{
30
    /**
31
     * Counter for the current sudo nesting level {@see sudo()}.
32
     *
33
     * @var int
34
     */
35
    private $sudoNestingLevel = 0;
36
37
    /**
38
     * @var \eZ\Publish\Core\Repository\Helper\RoleDomainMapper
39
     */
40
    private $roleDomainMapper;
41
42
    /**
43
     * @var \eZ\Publish\Core\Repository\Helper\LimitationService
44
     */
45
    private $limitationService;
46
47
    /**
48
     * @var \eZ\Publish\SPI\Persistence\User\Handler
49
     */
50
    private $userHandler;
51
52
    /**
53
     * Currently logged in user reference for permission purposes.
54
     *
55
     * @var \eZ\Publish\API\Repository\Values\User\UserReference
56
     */
57
    private $currentUserRef;
58
59
    /**
60
     * Map of system configured policies, for validation usage.
61
     *
62
     * @var array
63
     */
64
    private $policyMap;
65
66
    /**
67
     * @param \eZ\Publish\Core\Repository\Helper\RoleDomainMapper $roleDomainMapper
68
     * @param \eZ\Publish\Core\Repository\Helper\LimitationService $limitationService
69
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
70
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $userReference
71
     * @param array $policyMap Map of system configured policies, for validation usage.
72
     */
73
    public function __construct(
74
        RoleDomainMapper $roleDomainMapper,
75
        LimitationService $limitationService,
76
        UserHandler $userHandler,
77
        APIUserReference $userReference,
78
        array $policyMap = []
79
    ) {
80
        $this->roleDomainMapper = $roleDomainMapper;
81
        $this->limitationService = $limitationService;
82
        $this->userHandler = $userHandler;
83
        $this->currentUserRef = $userReference;
84
        $this->policyMap = $policyMap;
85
    }
86
87
    public function getCurrentUserReference()
88
    {
89
        return $this->currentUserRef;
90
    }
91
92
    public function setCurrentUserReference(APIUserReference $userReference)
93
    {
94
        $id = $userReference->getUserId();
95
        if (!$id) {
96
            throw new InvalidArgumentValue('$user->getUserId()', $id);
97
        }
98
99
        $this->currentUserRef = $userReference;
100
    }
101
102
    public function hasAccess($module, $function, APIUserReference $userReference = null)
103
    {
104
        if (!isset($this->policyMap[$module])) {
105
            throw new InvalidArgumentValue('module', "module: {$module}/ function: {$function}");
106
        } elseif (!array_key_exists($function, $this->policyMap[$module])) {
107
            throw new InvalidArgumentValue('function', "module: {$module}/ function: {$function}");
108
        }
109
110
        // Full access if sudo nesting level is set by {@see sudo()}
111
        if ($this->sudoNestingLevel > 0) {
112
            return true;
113
        }
114
115
        if ($userReference === null) {
116
            $userReference = $this->getCurrentUserReference();
117
        }
118
119
        // Uses SPI to avoid triggering permission checks in Role/User service
120
        $permissionSets = array();
121
        $spiRoleAssignments = $this->userHandler->loadRoleAssignmentsByGroupId($userReference->getUserId(), true);
122
        foreach ($spiRoleAssignments as $spiRoleAssignment) {
123
            $permissionSet = array('limitation' => null, 'policies' => array());
124
125
            $spiRole = $this->userHandler->loadRole($spiRoleAssignment->roleId);
126
            foreach ($spiRole->policies as $spiPolicy) {
127
                if ($spiPolicy->module === '*' && $spiRoleAssignment->limitationIdentifier === null) {
128
                    return true;
129
                }
130
131
                if ($spiPolicy->module !== $module && $spiPolicy->module !== '*') {
132
                    continue;
133
                }
134
135
                if ($spiPolicy->function === '*' && $spiRoleAssignment->limitationIdentifier === null) {
136
                    return true;
137
                }
138
139
                if ($spiPolicy->function !== $function && $spiPolicy->function !== '*') {
140
                    continue;
141
                }
142
143
                if ($spiPolicy->limitations === '*' && $spiRoleAssignment->limitationIdentifier === null) {
144
                    return true;
145
                }
146
147
                $permissionSet['policies'][] = $this->roleDomainMapper->buildDomainPolicyObject($spiPolicy);
148
            }
149
150
            if (!empty($permissionSet['policies'])) {
151
                if ($spiRoleAssignment->limitationIdentifier !== null) {
152
                    $permissionSet['limitation'] = $this->limitationService
153
                        ->getLimitationType($spiRoleAssignment->limitationIdentifier)
154
                        ->buildValue($spiRoleAssignment->values);
0 ignored issues
show
Bug introduced by
It seems like $spiRoleAssignment->values can also be of type null; however, eZ\Publish\SPI\Limitation\Type::buildValue() does only seem to accept array<integer,*>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
155
                }
156
157
                $permissionSets[] = $permissionSet;
158
            }
159
        }
160
161
        if (!empty($permissionSets)) {
162
            return $permissionSets;
163
        }
164
165
        return false; // No policies matching $module and $function, or they contained limitations
166
    }
167
168
    public function canUser($module, $function, ValueObject $object, array $targets = [])
169
    {
170
        $permissionSets = $this->hasAccess($module, $function);
171
        if ($permissionSets === false || $permissionSets === true) {
172
            return $permissionSets;
173
        }
174
175
        if (empty($targets)) {
176
            $targets = null;
177
        }
178
179
        $currentUserRef = $this->getCurrentUserReference();
180
        foreach ($permissionSets as $permissionSet) {
0 ignored issues
show
Bug introduced by
The expression $permissionSets of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
181
            /**
182
             * First deal with Role limitation if any.
183
             *
184
             * Here we accept ACCESS_GRANTED and ACCESS_ABSTAIN, the latter in cases where $object and $targets
185
             * are not supported by limitation.
186
             *
187
             * @var \eZ\Publish\API\Repository\Values\User\Limitation[]
188
             */
189
            if (
190
                $permissionSet['limitation'] instanceof Limitation
191
                && $this->isDeniedByRoleLimitation(
192
                    $permissionSet['limitation'],
193
                    $currentUserRef,
194
                    $object,
195
                    $targets
196
                )
197
            ) {
198
                continue;
199
            }
200
201
            /**
202
             * Loop over all policies.
203
             *
204
             * These are already filtered by hasAccess and given hasAccess did not return boolean
205
             * there must be some, so only return true if one of them says yes.
206
             *
207
             * @var \eZ\Publish\API\Repository\Values\User\Policy
208
             */
209
            foreach ($permissionSet['policies'] as $policy) {
210
                $limitations = $policy->getLimitations();
211
212
                /*
213
                 * Return true if policy gives full access (aka no limitations)
214
                 */
215
                if ($limitations === '*') {
216
                    return true;
217
                }
218
219
                /*
220
                 * Loop over limitations, all must return ACCESS_GRANTED for policy to pass.
221
                 * If limitations was empty array this means same as '*'
222
                 */
223
                $limitationsPass = true;
224
                foreach ($limitations as $limitation) {
225
                    $type = $this->limitationService->getLimitationType($limitation->getIdentifier());
226
                    $accessVote = $type->evaluate(
227
                        $limitation,
228
                        $currentUserRef,
229
                        $object,
230
                        $this->prepareTargetsForType($targets, $type)
0 ignored issues
show
Bug introduced by
It seems like $this->prepareTargetsForType($targets, $type) targeting eZ\Publish\Core\Reposito...prepareTargetsForType() can also be of type array; however, eZ\Publish\SPI\Limitation\Type::evaluate() does only seem to accept null|array<integer,objec...ry\Values\ValueObject>>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
231
                    );
232
                    /*
233
                     * For policy limitation atm only support ACCESS_GRANTED
234
                     *
235
                     * Reasoning: Right now, use of a policy limitation not valid for a policy is per definition a
236
                     * BadState. To reach this you would have to configure the "policyMap" wrongly, like using
237
                     * Node (Location) limitation on state/assign. So in this case Role Limitations will return
238
                     * ACCESS_ABSTAIN (== no access here), and other limitations will throw InvalidArgument above,
239
                     * both cases forcing dev to investigate to find miss configuration. This might be relaxed in
240
                     * the future if valid use cases for ACCESS_ABSTAIN on policy limitations becomes known.
241
                     */
242
                    if ($accessVote !== LimitationType::ACCESS_GRANTED) {
243
                        $limitationsPass = false;
244
                        break; // Break to next policy, all limitations must pass
245
                    }
246
                }
247
                if ($limitationsPass) {
248
                    return true;
249
                }
250
            }
251
        }
252
253
        return false; // None of the limitation sets wanted to let you in, sorry!
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function lookupLimitations(
260
        string $module,
261
        string $function,
262
        ValueObject $object,
263
        array $targets = [],
264
        array $limitationsIdentifiers = []
265
    ): LookupLimitationResult {
266
        $permissionSets = $this->hasAccess($module, $function);
267
268
        if (is_bool($permissionSets)) {
269
            return new LookupLimitationResult($permissionSets);
270
        }
271
272
        if (empty($targets)) {
273
            $targets = null;
274
        }
275
276
        $currentUserReference = $this->getCurrentUserReference();
277
278
        $passedLimitations = [];
279
        $passedRoleLimitations = [];
280
281
        foreach ($permissionSets as $permissionSet) {
282
            if ($this->isDeniedByRoleLimitation($permissionSet['limitation'], $currentUserReference, $object, $targets)) {
283
                continue;
284
            }
285
286
            /** @var \eZ\Publish\API\Repository\Values\User\Policy $policy */
287
            foreach ($permissionSet['policies'] as $policy) {
288
                $policyLimitations = $policy->getLimitations();
289
290
                /** Return empty array if policy gives full access (aka no limitations) */
291
                if ($policyLimitations === '*') {
292
                    return new LookupLimitationResult(true);
293
                }
294
295
                $limitationsPass = true;
296
                $possibleLimitations = [];
297
                foreach ($policyLimitations as $limitation) {
298
                    $limitationsPass = $this->isGrantedByLimitation($limitation, $currentUserReference, $object, $targets);
299
                    if (!$limitationsPass) {
300
                        break;
301
                    }
302
303
                    $possibleLimitations[] = $limitation;
304
                }
305
306
                $limitationFilter = function (Limitation $limitation) use ($limitationsIdentifiers) {
307
                    return \in_array($limitation->getIdentifier(), $limitationsIdentifiers, true);
308
                };
309
310
                if (!empty($limitationsIdentifiers)) {
311
                    $possibleLimitations = array_filter($possibleLimitations, $limitationFilter);
312
                }
313
314
                if ($limitationsPass && !empty($possibleLimitations)) {
315
                    $passedLimitations[] = new LookupPolicyLimitations($policy, $possibleLimitations);
316
                    if (null !== $permissionSet['limitation']) {
317
                        $passedRoleLimitations[] = $permissionSet['limitation'];
318
                    }
319
                }
320
            }
321
        }
322
323
        return new LookupLimitationResult(!empty($passedLimitations), $passedRoleLimitations, $passedLimitations);
324
    }
325
326
    /**
327
     * @param \eZ\Publish\API\Repository\Values\User\Limitation $limitation
328
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $currentUserReference
329
     * @param \eZ\Publish\API\Repository\Values\ValueObject $object
330
     * @param array|null $targets
331
     *
332
     * @return bool
333
     *
334
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
335
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
336
     */
337
    private function isGrantedByLimitation(
338
        Limitation $limitation,
339
        APIUserReference $currentUserReference,
340
        ValueObject $object,
341
        ?array $targets
342
    ): bool {
343
        $type = $this->limitationService->getLimitationType($limitation->getIdentifier());
344
        $accessVote = $type->evaluate($limitation, $currentUserReference, $object, $targets);
0 ignored issues
show
Bug introduced by
It seems like $targets defined by parameter $targets on line 341 can also be of type array; however, eZ\Publish\SPI\Limitation\Type::evaluate() does only seem to accept null|array<integer,objec...ry\Values\ValueObject>>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
345
346
        return $accessVote === LimitationType::ACCESS_GRANTED;
347
    }
348
349
    /**
350
     * @param \eZ\Publish\API\Repository\Values\User\Limitation|null $limitation
351
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $currentUserReference
352
     * @param \eZ\Publish\API\Repository\Values\ValueObject $object
353
     * @param array|null $targets
354
     *
355
     * @return bool
356
     *
357
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
358
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
359
     */
360
    private function isDeniedByRoleLimitation(
361
        ?Limitation $limitation,
362
        APIUserReference $currentUserReference,
363
        ValueObject $object,
364
        ?array $targets
365
    ): bool {
366
        if (null === $limitation) {
367
            return false;
368
        }
369
370
        $type = $this->limitationService->getLimitationType($limitation->getIdentifier());
371
        $accessVote = $type->evaluate($limitation, $currentUserReference, $object, $targets);
0 ignored issues
show
Bug introduced by
It seems like $targets defined by parameter $targets on line 364 can also be of type array; however, eZ\Publish\SPI\Limitation\Type::evaluate() does only seem to accept null|array<integer,objec...ry\Values\ValueObject>>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
372
373
        return $accessVote === LimitationType::ACCESS_DENIED;
374
    }
375
376
    /**
377
     * @internal For internal use only, do not depend on this method.
378
     *
379
     * Allows API execution to be performed with full access sand-boxed.
380
     *
381
     * The closure sandbox will do a catch all on exceptions and rethrow after
382
     * re-setting the sudo flag.
383
     *
384
     * Example use:
385
     *     $location = $repository->sudo(
386
     *         function ( Repository $repo ) use ( $locationId )
387
     *         {
388
     *             return $repo->getLocationService()->loadLocation( $locationId )
389
     *         }
390
     *     );
391
     *
392
     *
393
     * @param \Closure $callback
394
     * @param \eZ\Publish\API\Repository\Repository $outerRepository
395
     *
396
     * @throws \RuntimeException Thrown on recursive sudo() use.
397
     * @throws \Exception Re throws exceptions thrown inside $callback
398
     *
399
     * @return mixed
400
     */
401
    public function sudo(callable $callback, RepositoryInterface $outerRepository)
402
    {
403
        ++$this->sudoNestingLevel;
404
        try {
405
            $returnValue = $callback($outerRepository);
406
        } catch (Exception $e) {
407
            --$this->sudoNestingLevel;
408
            throw $e;
409
        }
410
411
        --$this->sudoNestingLevel;
412
413
        return $returnValue;
414
    }
415
416
    /**
417
     * Prepare list of targets for the given Type keeping BC.
418
     *
419
     * @param array|null $targets
420
     * @param \eZ\Publish\SPI\Limitation\Type $type
421
     *
422
     * @return array|null
423
     */
424
    private function prepareTargetsForType(?array $targets, LimitationType $type): ?array
425
    {
426
        $isTargetAware = $type instanceof TargetAwareType;
427
428
        // BC: null for empty targets is still expected by some Limitations, so needs to be preserved
429
        if (null === $targets) {
430
            return $isTargetAware ? [] : null;
431
        }
432
433
        // BC: for TargetAware Limitations return only instances of Target, for others return only non-Target instances
434
        $targets = array_filter(
435
            $targets,
436
            function ($target) use ($isTargetAware) {
437
                $isTarget = $target instanceof Target;
438
439
                return $isTargetAware ? $isTarget : !$isTarget;
440
            }
441
        );
442
443
        // BC: treat empty targets after filtering as if they were empty the whole time
444
        if (!$isTargetAware && empty($targets)) {
445
            return null;
446
        }
447
448
        return $targets;
449
    }
450
}
451