Completed
Push — impl-EZP-26000-permission-look... ( 500ba3 )
by
unknown
26:49
created

PermissionService::getPermissionInfo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
cc 2
eloc 4
nc 2
nop 2
rs 9.4285
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\InvalidArgumentValue;
15
use eZ\Publish\Core\Repository\Helper\LimitationService;
16
use eZ\Publish\Core\Repository\Helper\RoleDomainMapper;
17
use eZ\Publish\Core\Repository\PermissionResolver\PermissionInfoMapper\Content as ContentPermissionInfoMapper;
18
use eZ\Publish\Core\Repository\PermissionResolver\PermissionInfoMapper\Aggregate as PermissionInfoMapper;
19
use eZ\Publish\Core\Repository\PermissionResolver\PermissionResolver;
20
use eZ\Publish\SPI\Limitation\Type as LimitationType;
21
use eZ\Publish\SPI\Persistence\User\Handler as UserHandler;
22
use Exception;
23
24
/**
25
 * Core implementation of PermissionsService interface.
26
 */
27
class PermissionService implements PermissionServiceInterface
28
{
29
    /**
30
     * Counter for the current sudo nesting level {@see sudo()}.
31
     *
32
     * @var int
33
     */
34
    private $sudoNestingLevel = 0;
35
36
    /**
37
     * @var \eZ\Publish\Core\Repository\Helper\RoleDomainMapper
38
     */
39
    private $roleDomainMapper;
40
41
    /**
42
     * @var \eZ\Publish\Core\Repository\Helper\LimitationService
43
     */
44
    private $limitationService;
45
46
    /**
47
     * @var \eZ\Publish\SPI\Persistence\User\Handler
48
     */
49
    private $userHandler;
50
51
    /**
52
     * Currently logged in user reference for permission purposes.
53
     *
54
     * @var \eZ\Publish\API\Repository\Values\User\UserReference
55
     */
56
    private $currentUserRef;
57
58
    /**
59
     * @var \eZ\Publish\Core\Repository\PermissionResolver\PermissionInfoMapper
60
     */
61
    private $permissionInfoMapper;
62
63
    /**
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
        RoleDomainMapper $roleDomainMapper,
71
        LimitationService $limitationService,
72
        UserHandler $userHandler,
73
        APIUserReference $userReference
74
    ) {
75
        $this->roleDomainMapper = $roleDomainMapper;
76
        $this->limitationService = $limitationService;
77
        $this->userHandler = $userHandler;
78
        $this->currentUserRef = $userReference;
79
80
        // TODO: inject
81
        $permissionResolver = new PermissionResolver(
82
            $this->roleDomainMapper,
83
            $this->limitationService,
84
            $this->userHandler
85
        );
86
        $this->permissionInfoMapper = new PermissionInfoMapper();
87
        $contentPermissionInfoMapper = new ContentPermissionInfoMapper($permissionResolver);
88
        $this->permissionInfoMapper->addMapper($contentPermissionInfoMapper);
89
    }
90
91
    public function getCurrentUserReference()
92
    {
93
        return $this->currentUserRef;
94
    }
95
96
    public function setCurrentUserReference(APIUserReference $userReference)
97
    {
98
        $id = $userReference->getUserId();
99
        if (!$id) {
100
            throw new InvalidArgumentValue('$user->getUserId()', $id);
101
        }
102
103
        $this->currentUserRef = $userReference;
104
    }
105
106
    public function hasAccess($module, $function, APIUserReference $userReference = null)
107
    {
108
        // Full access if sudo nesting level is set by {@see sudo()}
109
        if ($this->sudoNestingLevel > 0) {
110
            return true;
111
        }
112
113
        if ($userReference === null) {
114
            $userReference = $this->getCurrentUserReference();
115
        }
116
117
        // Uses SPI to avoid triggering permission checks in Role/User service
118
        $permissionSets = array();
119
        $spiRoleAssignments = $this->userHandler->loadRoleAssignmentsByGroupId($userReference->getUserId(), true);
120
        foreach ($spiRoleAssignments as $spiRoleAssignment) {
121
            $permissionSet = array('limitation' => null, 'policies' => array());
122
123
            $spiRole = $this->userHandler->loadRole($spiRoleAssignment->roleId);
124
            foreach ($spiRole->policies as $spiPolicy) {
125
                if ($spiPolicy->module === '*' && $spiRoleAssignment->limitationIdentifier === null) {
126
                    return true;
127
                }
128
129
                if ($spiPolicy->module !== $module && $spiPolicy->module !== '*') {
130
                    continue;
131
                }
132
133
                if ($spiPolicy->function === '*' && $spiRoleAssignment->limitationIdentifier === null) {
134
                    return true;
135
                }
136
137
                if ($spiPolicy->function !== $function && $spiPolicy->function !== '*') {
138
                    continue;
139
                }
140
141
                if ($spiPolicy->limitations === '*' && $spiRoleAssignment->limitationIdentifier === null) {
142
                    return true;
143
                }
144
145
                $permissionSet['policies'][] = $this->roleDomainMapper->buildDomainPolicyObject($spiPolicy);
146
            }
147
148 View Code Duplication
            if (!empty($permissionSet['policies'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
                if ($spiRoleAssignment->limitationIdentifier !== null) {
150
                    $permissionSet['limitation'] = $this->limitationService
151
                        ->getLimitationType($spiRoleAssignment->limitationIdentifier)
152
                        ->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...
153
                }
154
155
                $permissionSets[] = $permissionSet;
156
            }
157
        }
158
159
        if (!empty($permissionSets)) {
160
            return $permissionSets;
161
        }
162
163
        return false;// No policies matching $module and $function, or they contained limitations
164
    }
165
166
    public function canUser($module, $function, ValueObject $object, array $targets = [])
167
    {
168
        $permissionSets = $this->hasAccess($module, $function);
169
        if ($permissionSets === false || $permissionSets === true) {
170
            return $permissionSets;
171
        }
172
173
        if (empty($targets)) {
174
            $targets = null;
175
        }
176
177
        $currentUserRef = $this->getCurrentUserReference();
178
        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...
179
            /**
180
             * First deal with Role limitation if any.
181
             *
182
             * Here we accept ACCESS_GRANTED and ACCESS_ABSTAIN, the latter in cases where $object and $targets
183
             * are not supported by limitation.
184
             *
185
             * @var \eZ\Publish\API\Repository\Values\User\Limitation[]
186
             */
187
            if ($permissionSet['limitation'] instanceof Limitation) {
188
                $type = $this->limitationService->getLimitationType($permissionSet['limitation']->getIdentifier());
189
                $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 166 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...
190
                if ($accessVote === LimitationType::ACCESS_DENIED) {
191
                    continue;
192
                }
193
            }
194
195
            /**
196
             * Loop over all policies.
197
             *
198
             * These are already filtered by hasAccess and given hasAccess did not return boolean
199
             * there must be some, so only return true if one of them says yes.
200
             *
201
             * @var \eZ\Publish\API\Repository\Values\User\Policy
202
             */
203
            foreach ($permissionSet['policies'] as $policy) {
204
                $limitations = $policy->getLimitations();
205
206
                /*
207
                 * Return true if policy gives full access (aka no limitations)
208
                 */
209
                if ($limitations === '*') {
210
                    return true;
211
                }
212
213
                /*
214
                 * Loop over limitations, all must return ACCESS_GRANTED for policy to pass.
215
                 * If limitations was empty array this means same as '*'
216
                 */
217
                $limitationsPass = true;
218 View Code Duplication
                foreach ($limitations as $limitation) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
                    $type = $this->limitationService->getLimitationType($limitation->getIdentifier());
220
                    $accessVote = $type->evaluate($limitation, $currentUserRef, $object, $targets);
0 ignored issues
show
Bug introduced by
It seems like $targets defined by parameter $targets on line 166 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...
221
                    /*
222
                     * For policy limitation atm only support ACCESS_GRANTED
223
                     *
224
                     * Reasoning: Right now, use of a policy limitation not valid for a policy is per definition a
225
                     * BadState. To reach this you would have to configure the "policyMap" wrongly, like using
226
                     * Node (Location) limitation on state/assign. So in this case Role Limitations will return
227
                     * ACCESS_ABSTAIN (== no access here), and other limitations will throw InvalidArgument above,
228
                     * both cases forcing dev to investigate to find miss configuration. This might be relaxed in
229
                     * the future if valid use cases for ACCESS_ABSTAIN on policy limitations becomes known.
230
                     */
231
                    if ($accessVote !== LimitationType::ACCESS_GRANTED) {
232
                        $limitationsPass = false;
233
                        break;// Break to next policy, all limitations must pass
234
                    }
235
                }
236
                if ($limitationsPass) {
237
                    return true;
238
                }
239
            }
240
        }
241
242
        return false;// None of the limitation sets wanted to let you in, sorry!
243
    }
244
245
    public function getPermissionInfo(ValueObject $object, APIUserReference $userReference = null)
246
    {
247
        if ($userReference === null) {
248
            $userReference = $this->getCurrentUserReference();
249
        }
250
251
        return $this->permissionInfoMapper->map($object, $userReference);
252
    }
253
254
    /**
255
     * @internal For internal use only, do not depend on this method.
256
     *
257
     * Allows API execution to be performed with full access sand-boxed.
258
     *
259
     * The closure sandbox will do a catch all on exceptions and rethrow after
260
     * re-setting the sudo flag.
261
     *
262
     * Example use:
263
     *     $location = $repository->sudo(
264
     *         function ( Repository $repo ) use ( $locationId )
265
     *         {
266
     *             return $repo->getLocationService()->loadLocation( $locationId )
267
     *         }
268
     *     );
269
     *
270
     *
271
     * @param \Closure $callback
272
     * @param \eZ\Publish\API\Repository\Repository $outerRepository
273
     *
274
     * @throws \RuntimeException Thrown on recursive sudo() use.
275
     * @throws \Exception Re throws exceptions thrown inside $callback
276
     *
277
     * @return mixed
278
     */
279
    public function sudo(\Closure $callback, RepositoryInterface $outerRepository)
280
    {
281
        ++$this->sudoNestingLevel;
282
        try {
283
            $returnValue = $callback($outerRepository);
284
        } catch (Exception $e) {
285
            --$this->sudoNestingLevel;
286
            throw $e;
287
        }
288
289
        --$this->sudoNestingLevel;
290
291
        return $returnValue;
292
    }
293
}
294