Completed
Push — EZP-26057-permission-api ( b3fc4f )
by
unknown
25:48
created

PermissionService::canUser()   D

Complexity

Conditions 14
Paths 26

Size

Total Lines 84
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 14
eloc 32
nc 26
nop 4
dl 0
loc 84
rs 4.9516
c 1
b 0
f 1

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