Completed
Push — EZP-26057-permission-api ( e8d739...b86601 )
by
unknown
21:34
created

PermissionService   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 4
Bugs 0 Features 1
Metric Value
dl 0
loc 243
rs 8.8
c 4
b 0
f 1
wmc 36
lcom 1
cbo 9

6 Methods

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