Completed
Push — EZP-26057-permission-api ( b4c475...de9e2b )
by
unknown
23:03
created

PermissionService::hasAccess()   C

Complexity

Conditions 18
Paths 59

Size

Total Lines 59
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 59
cc 18
eloc 31
nc 59
nop 3
rs 6.3239

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
8
9
use eZ\Publish\API\Repository\PermissionService as PermissionServiceInterface;
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\UserReference as APIUserReference;
13
use eZ\Publish\API\Repository\Values\ValueObject;
14
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
15
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
16
use eZ\Publish\Core\Repository\Helper\LimitationService;
17
use eZ\Publish\Core\Repository\Helper\RoleDomainMapper;
18
use eZ\Publish\Core\Repository\Values\User\UserReference;
19
use eZ\Publish\SPI\Limitation\Type as LimitationType;
20
use eZ\Publish\SPI\Persistence\User\Handler as UserHandler;
21
use Exception;
22
23
/**
24
 * Core implementation of PermissionsService interface.
25
 */
26
class PermissionService implements PermissionServiceInterface
27
{
28
    /**
29
     * Counter for the current sudo nesting level {@see sudo()}.
30
     *
31
     * @var int
32
     */
33
    private $sudoNestingLevel = 0;
34
35
    /**
36
     * @var \eZ\Publish\API\Repository\Repository
37
     */
38
    private $repository;
39
40
    /**
41
     * @var \eZ\Publish\Core\Repository\Helper\RoleDomainMapper
42
     */
43
    private $roleDomainMapper;
44
45
    /**
46
     * @var \eZ\Publish\Core\Repository\Helper\LimitationService
47
     */
48
    private $limitationService;
49
50
    /**
51
     * @var \eZ\Publish\SPI\Persistence\User\Handler
52
     */
53
    private $userHandler;
54
55
    /**
56
     * Currently logged in user reference for permission purposes.
57
     *
58
     * @var \eZ\Publish\API\Repository\Values\User\UserReference
59
     */
60
    private $currentUserRef;
61
62
    /**
63
     * @param \eZ\Publish\API\Repository\Repository $repository
64
     * @param \eZ\Publish\Core\Repository\Helper\RoleDomainMapper $roleDomainMapper
65
     * @param \eZ\Publish\Core\Repository\Helper\LimitationService $limitationService
66
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
67
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $userReference
68
     */
69
    public function __construct(
70
        RepositoryInterface $repository,
71
        RoleDomainMapper $roleDomainMapper,
72
        LimitationService $limitationService,
73
        UserHandler $userHandler,
74
        APIUserReference $userReference
75
    ) {
76
        $this->repository = $repository;
77
        $this->roleDomainMapper = $roleDomainMapper;
78
        $this->limitationService = $limitationService;
79
        $this->userHandler = $userHandler;
80
        $this->currentUserRef = $userReference;
81
    }
82
83
    public function getCurrentUserReference()
84
    {
85
        return $this->currentUserRef;
86
    }
87
88
    public function setCurrentUserReference(APIUserReference $userReference)
89
    {
90
        $id = $userReference->getUserId();
91
        if (!$id) {
92
            throw new InvalidArgumentValue('$user->getUserId()', $id);
93
        }
94
95
        $this->currentUserRef = $userReference;
96
    }
97
98
    public function hasAccess($module, $function, APIUserReference $userReference = null)
99
    {
100
        // Full access if sudo nesting level is set by {@see sudo()}
101
        if ($this->sudoNestingLevel > 0) {
102
            return true;
103
        }
104
105
        if ($userReference === null) {
106
            $userReference = $this->getCurrentUserReference();
107
        }
108
109
        // Uses SPI to avoid triggering permission checks in Role/User service
110
        $permissionSets = array();
111
        $spiRoleAssignments = $this->userHandler->loadRoleAssignmentsByGroupId($userReference->getUserId(), true);
112
        foreach ($spiRoleAssignments as $spiRoleAssignment) {
113
            $permissionSet = array('limitation' => null, 'policies' => array());
114
115
            $spiRole = $this->userHandler->loadRole($spiRoleAssignment->roleId);
116
            foreach ($spiRole->policies as $spiPolicy) {
117
                if ($spiPolicy->module === '*' && $spiRoleAssignment->limitationIdentifier === null) {
118
                    return true;
119
                }
120
121
                if ($spiPolicy->module !== $module && $spiPolicy->module !== '*') {
122
                    continue;
123
                }
124
125
                if ($spiPolicy->function === '*' && $spiRoleAssignment->limitationIdentifier === null) {
126
                    return true;
127
                }
128
129
                if ($spiPolicy->function !== $function && $spiPolicy->function !== '*') {
130
                    continue;
131
                }
132
133
                if ($spiPolicy->limitations === '*' && $spiRoleAssignment->limitationIdentifier === null) {
134
                    return true;
135
                }
136
137
                $permissionSet['policies'][] = $this->roleDomainMapper->buildDomainPolicyObject($spiPolicy);
138
            }
139
140
            if (!empty($permissionSet['policies'])) {
141
                if ($spiRoleAssignment->limitationIdentifier !== null) {
142
                    $permissionSet['limitation'] = $this->limitationService
143
                        ->getLimitationType($spiRoleAssignment->limitationIdentifier)
144
                        ->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...
145
                }
146
147
                $permissionSets[] = $permissionSet;
148
            }
149
        }
150
151
        if (!empty($permissionSets)) {
152
            return $permissionSets;
153
        }
154
155
        return false;// No policies matching $module and $function, or they contained limitations
156
    }
157
158
    public function canUser($module, $function, ValueObject $object, $targets = null)
159
    {
160
        $permissionSets = $this->hasAccess($module, $function);
161
        if ($permissionSets === false || $permissionSets === true) {
162
            return $permissionSets;
163
        }
164
165
        if ($targets instanceof ValueObject) {
166
            $targets = array($targets);
167
        } elseif ($targets !== null && !is_array($targets)) {
168
            throw new InvalidArgumentType(
169
                '$targets',
170
                'null|\\eZ\\Publish\\API\\Repository\\Values\\ValueObject|\\eZ\\Publish\\API\\Repository\\Values\\ValueObject[]',
171
                $targets
172
            );
173
        }
174
175
        $currentUserRef = $this->getCurrentUserReference();
176
        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...
177
            /**
178
             * First deal with Role limitation if any.
179
             *
180
             * Here we accept ACCESS_GRANTED and ACCESS_ABSTAIN, the latter in cases where $object and $targets
181
             * are not supported by limitation.
182
             *
183
             * @var \eZ\Publish\API\Repository\Values\User\Limitation[]
184
             */
185
            if ($permissionSet['limitation'] instanceof Limitation) {
186
                $type = $this->limitationService->getLimitationType($permissionSet['limitation']->getIdentifier());
187
                $accessVote = $type->evaluate($permissionSet['limitation'], $currentUserRef, $object, $targets);
0 ignored issues
show
Bug introduced by
It seems like $targets defined by parameter $targets on line 158 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...
188
                if ($accessVote === LimitationType::ACCESS_DENIED) {
189
                    continue;
190
                }
191
            }
192
193
            /**
194
             * Loop over all policies.
195
             *
196
             * These are already filtered by hasAccess and given hasAccess did not return boolean
197
             * there must be some, so only return true if one of them says yes.
198
             *
199
             * @var \eZ\Publish\API\Repository\Values\User\Policy
200
             */
201
            foreach ($permissionSet['policies'] as $policy) {
202
                $limitations = $policy->getLimitations();
203
204
                /*
205
                 * Return true if policy gives full access (aka no limitations)
206
                 */
207
                if ($limitations === '*') {
208
                    return true;
209
                }
210
211
                /*
212
                 * Loop over limitations, all must return ACCESS_GRANTED for policy to pass.
213
                 * If limitations was empty array this means same as '*'
214
                 */
215
                $limitationsPass = true;
216
                foreach ($limitations as $limitation) {
217
                    $type = $this->limitationService->getLimitationType($limitation->getIdentifier());
218
                    $accessVote = $type->evaluate($limitation, $currentUserRef, $object, $targets);
0 ignored issues
show
Bug introduced by
It seems like $targets defined by parameter $targets on line 158 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...
219
                    /*
220
                     * For policy limitation atm only support ACCESS_GRANTED
221
                     *
222
                     * Reasoning: Right now, use of a policy limitation not valid for a policy is per definition a
223
                     * BadState. To reach this you would have to configure the "policyMap" wrongly, like using
224
                     * Node (Location) limitation on state/assign. So in this case Role Limitations will return
225
                     * ACCESS_ABSTAIN (== no access here), and other limitations will throw InvalidArgument above,
226
                     * both cases forcing dev to investigate to find miss configuration. This might be relaxed in
227
                     * the future if valid use cases for ACCESS_ABSTAIN on policy limitations becomes known.
228
                     */
229
                    if ($accessVote !== LimitationType::ACCESS_GRANTED) {
230
                        $limitationsPass = false;
231
                        break;// Break to next policy, all limitations must pass
232
                    }
233
                }
234
                if ($limitationsPass) {
235
                    return true;
236
                }
237
            }
238
        }
239
240
        return false;// None of the limitation sets wanted to let you in, sorry!
241
    }
242
243
    /**
244
     * Allows API execution to be performed with full access sand-boxed.
245
     *
246
     * The closure sandbox will do a catch all on exceptions and rethrow after
247
     * re-setting the sudo flag.
248
     *
249
     * Example use:
250
     *     $location = $repository->sudo(
251
     *         function ( Repository $repo ) use ( $locationId )
252
     *         {
253
     *             return $repo->getLocationService()->loadLocation( $locationId )
254
     *         }
255
     *     );
256
     *
257
     *
258
     * @param \Closure $callback
259
     * @param \eZ\Publish\API\Repository\Repository $outerRepository
260
     *
261
     * @throws \RuntimeException Thrown on recursive sudo() use.
262
     * @throws \Exception Re throws exceptions thrown inside $callback
263
     *
264
     * @return mixed
265
     */
266
    public function sudo(\Closure $callback, RepositoryInterface $outerRepository = null)
267
    {
268
        ++$this->sudoNestingLevel;
269
        try {
270
            $returnValue = $callback($outerRepository !== null ? $outerRepository : $this->repository);
271
        } catch (Exception $e) {
272
            --$this->sudoNestingLevel;
273
            throw $e;
274
        }
275
276
        --$this->sudoNestingLevel;
277
278
        return $returnValue;
279
    }
280
}
281