Passed
Push — master ( 492756...90c7de )
by Samuel
02:59
created

UserPermissionVerifier   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Test Coverage

Coverage 83.54%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 48
eloc 138
c 1
b 0
f 0
dl 0
loc 368
ccs 132
cts 158
cp 0.8354
rs 8.5599

7 Methods

Rating   Name   Duplication   Size   Complexity  
B userRoleIsGranted() 0 46 10
A __construct() 0 7 1
C isGrantedToUpdate() 0 92 13
B isGrantedToDelete() 0 37 7
A isGrantedToRead() 0 31 5
A isGrantedToCreate() 0 44 5
B isGrantedToReadUserActivity() 0 39 7

How to fix   Complexity   

Complex Class

Complex classes like UserPermissionVerifier 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.

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 UserPermissionVerifier, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Domain\User\Service\Authorization;
4
5
use App\Application\Data\UserNetworkSessionData;
6
use App\Domain\Authentication\Repository\UserRoleFinderRepository;
0 ignored issues
show
Bug introduced by
The type App\Domain\Authenticatio...serRoleFinderRepository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use App\Domain\User\Enum\UserRole;
8
use Psr\Log\LoggerInterface;
9
10
/**
11
 * Check if authenticated user is permitted to do actions
12
 * Roles: newcomer < advisor < managing_advisor < administrator.
13
 */
14
final class UserPermissionVerifier
15
{
16
    private ?int $loggedInUserId = null;
17
18 203
    public function __construct(
19
        private readonly UserNetworkSessionData $userNetworkSessionData,
20
        private readonly UserRoleFinderRepository $userRoleFinderRepository,
21
        private readonly LoggerInterface $logger,
22
    ) {
23
        // Fix error $userId must not be accessed before initialization
24 203
        $this->loggedInUserId = $this->userNetworkSessionData->userId ?? null;
25
    }
26
27
    /**
28
     * Check if the authenticated user is allowed to create
29
     * Important to have user role in the object.
30
     *
31
     * @param array $userValues
32
     *
33
     * @return bool
34
     */
35 9
    public function isGrantedToCreate(array $userValues): bool
36
    {
37 9
        if ($this->loggedInUserId === null) {
38
            $this->logger->error(
39
                'loggedInUserId not set while authorization check isGrantedToCreate: '
40
                . json_encode($userValues, JSON_PARTIAL_OUTPUT_ON_ERROR)
41
            );
42
43
            return false;
44
        }
45 9
        $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
46 9
            $this->loggedInUserId
47 9
        );
48
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
49
        // * Lower hierarchy number means higher privileged role
50 9
        $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
51
52
        // Newcomer and advisor are not allowed to do anything from other users - only user edit his own profile
53
        // Managing advisor may change users
54 9
        if ($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]) {
55
            // Managing advisors can do everything with users except setting a role higher than advisor
56 8
            if ($this->userRoleIsGranted(
57 8
                $userValues['user_role_id'] ?? null,
58 8
                null,
59 8
                $authenticatedUserRoleHierarchy,
60 8
                $userRoleHierarchies
61 8
            ) === true
62
            ) {
63 7
                return true;
64
            }
65
66
            // If the user role of the user managing advisors or higher wants to change is empty, allowed
67
            // It's the validation's job to check if the value is valid
68 1
            if ($userValues['user_role_id'] === null) {
69
                return true;
70
            }
71
        }
72
        // There is no need to check if user wants to create his own user as he can't be logged in if the user doesn't exist
73
74 2
        $this->logger->notice(
75 2
            'User ' . $this->loggedInUserId . ' tried to create user but isn\'t allowed.'
76 2
        );
77
78 2
        return false;
79
    }
80
81
    /**
82
     * Check if the authenticated user is allowed to assign a user role.
83
     *
84
     * @param string|int|null $newUserRoleId (New) user role id to be assigned. Nullable as admins are authorized to
85
     * set any role, validation should check if the value is valid.
86
     * @param string|int|null $userRoleIdOfUserToMutate (Existing) user role of user to be changed
87
     * @param int|null $authenticatedUserRoleHierarchy optional so that it can be called outside this class
88
     * @param array|null $userRoleHierarchies optional so that it can be called outside this class
89
     *
90
     * @return bool
91
     */
92 21
    public function userRoleIsGranted(
93
        string|int|null $newUserRoleId,
94
        string|int|null $userRoleIdOfUserToMutate,
95
        ?int $authenticatedUserRoleHierarchy = null,
96
        ?array $userRoleHierarchies = null,
97
    ): bool {
98 21
        if ($this->loggedInUserId === null) {
99
            $this->logger->error(
100
                'loggedInUserId not set while authorization check that user role is granted $userRoleIdOfUserToMutate: '
101
                . $userRoleIdOfUserToMutate
102
            );
103
104
            return false;
105
        }
106
        // $authenticatedUserRoleData and $userRoleHierarchies passed as arguments if called inside this class
107 21
        if ($authenticatedUserRoleHierarchy === null) {
108 10
            $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
109 10
                $this->loggedInUserId
110 10
            );
111
        }
112 21
        if ($userRoleHierarchies === null) {
113
            // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
114
            // * Lower hierarchy number means higher privileged role
115 10
            $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
116
        }
117
118 21
        $userRoleHierarchiesById = $this->userRoleFinderRepository->getUserRolesHierarchies(true);
119
120
        // Role higher (lower hierarchy number) than managing advisor may assign any role (admin)
121 21
        if ($authenticatedUserRoleHierarchy < $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]) {
122 8
            return true;
123
        }
124
125
        if (// Managing advisor can only attribute roles with lower or equal privilege than advisor
126 13
            !empty($newUserRoleId)
127 13
            && $authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
128 13
            && $userRoleHierarchiesById[$newUserRoleId] >= $userRoleHierarchies[UserRole::ADVISOR->value]
129
            // And managing advisor may only change advisors or newcomers
130 13
            && ($userRoleIdOfUserToMutate === null
131 13
                || $userRoleHierarchiesById[$userRoleIdOfUserToMutate] >=
132 13
                $userRoleHierarchies[UserRole::ADVISOR->value])
133
        ) {
134 4
            return true;
135
        }
136
137 11
        return false;
138
    }
139
140
    /**
141
     * Logic to check if logged-in user is granted to update user.
142
     * This function has a high cyclomatic complexity due to the many if-statements,
143
     * but for now I find it more readable than splitting it up into multiple functions.
144
     *
145
     * @param array $userDataToUpdate validated array with as key the column to
146
     * update and value the new value. There may be one or multiple entries,
147
     * depending on what the user wants to update
148
     * @param string|int $userIdToUpdate
149
     * @param bool $log log if forbidden (expected false when function is called for privilege setting)
150
     *
151
     * @return bool
152
     */
153 21
    public function isGrantedToUpdate(array $userDataToUpdate, string|int $userIdToUpdate, bool $log = true): bool
154
    {
155
        // Unset key id from data to update as is present in the array without the intention of being changed
156 21
        unset($userDataToUpdate['id']);
157 21
        if ($this->loggedInUserId === null) {
158
            $this->logger->error(
159
                'loggedInUserId not while user update authorization check' .
160
                json_encode($userDataToUpdate, JSON_PARTIAL_OUTPUT_ON_ERROR)
161
            );
162
163
            return false;
164
        }
165 21
        $grantedUpdateKeys = [];
166
167 21
        $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
168 21
            $this->loggedInUserId
169 21
        );
170 21
        $userToUpdateRoleData = $this->userRoleFinderRepository->getUserRoleDataFromUser((int)$userIdToUpdate);
171
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
172
        // * Lower hierarchy number means higher privileged role
173 21
        $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
174
175
        // Only managing advisor or higher privileged can change users
176 21
        if ((($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
177
                    // but only if user to change is advisor or lower
178 12
                    && $userToUpdateRoleData->hierarchy >= $userRoleHierarchies[UserRole::ADVISOR->value])
179
                // if user role is higher privileged than managing advisor (admin) -> authorized
180 18
                || $authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::ADMIN->value])
181
            // or if the user edits his own profile, also authorized to the next section
182 21
            || $this->loggedInUserId === (int)$userIdToUpdate
183
        ) {
184
            // Things that managing advisor and owner user are allowed to change
185
            // Personal info are values such as first name, last name and email
186 15
            $grantedUpdateKeys[] = 'personal_info';
187 15
            $grantedUpdateKeys[] = 'first_name';
188 15
            $grantedUpdateKeys[] = 'surname';
189 15
            $grantedUpdateKeys[] = 'email';
190 15
            $grantedUpdateKeys[] = 'password_hash';
191 15
            $grantedUpdateKeys[] = 'theme';
192 15
            $grantedUpdateKeys[] = 'language';
193
            // If a new basic data field is added, it has to be added to provider userUpdateAuthorizationCases()
194
            // $basicDataChanges variable and invalid value to provider invalidUserUpdateCases()
195
196
            // Things that only managing_advisor and higher privileged are allowed to change
197
            // If the user is managing advisor we know by the parent if-statement
198
            // that the user to change has not higher role than advisor
199 15
            if ($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]) {
200 8
                $grantedUpdateKeys[] = 'status';
201
202
                // Check if the authenticated user is granted to attribute role if that's requested
203 8
                if (array_key_exists('user_role_id', $userDataToUpdate)
204 8
                    && $this->userRoleIsGranted(
205 8
                        $userDataToUpdate['user_role_id'],
206 8
                        $userToUpdateRoleData->id,
207 8
                        $authenticatedUserRoleHierarchy,
208 8
                        $userRoleHierarchies
209 8
                    ) === true) {
210 1
                    $grantedUpdateKeys[] = 'user_role_id';
211
                }
212
213
                // There is a special case with passwords where the user can change his own password, but he needs to
214
                // provide the old password. If password_without_verification is added to $grantedUpdateKeys it means
215
                // that the authenticated user can change the password without the old password.
216
                // But if the user wants to change his own password, the old password is required regardless of role
217
                // so that nobody can change his password if the computer is left unattended and logged-in
218
                // https://security.stackexchange.com/a/24292 - to change other passwords it would be best if
219
                // the authenticated managing_advisor / admin password is asked instead of the old user password,
220
                // but this is too much complexity for this project.
221 8
                if ($this->loggedInUserId !== (int)$userIdToUpdate) {
222 6
                    $grantedUpdateKeys[] = 'password_without_verification';
223
                }
224
            }
225
            // Owner user (profile edit) is not allowed to change its user role or status
226
        }
227
228
        // Check that the data that the user wanted to update is in $grantedUpdateKeys array
229 21
        foreach ($userDataToUpdate as $key => $value) {
230
            // If at least one array key doesn't exist in $grantedUpdateKeys it means that user is not permitted
231 21
            if (!in_array($key, $grantedUpdateKeys, true)) {
232 13
                if ($log === true) {
233 8
                    $this->logger->notice(
234 8
                        'User ' . $this->loggedInUserId . ' tried to update user but isn\'t allowed to change' .
235 8
                        $key . ' to "' . $value . '".'
236 8
                    );
237
                }
238
239 13
                return false;
240
            }
241
        }
242
243
        // All keys in $userDataToUpdate are in $grantedUpdateKeys
244 10
        return true;
245
    }
246
247
    /**
248
     * Check if authenticated user is allowed to delete user.
249
     *
250
     * @param int $userIdToDelete
251
     * @param bool $log log if forbidden (expected false when function is called for privilege setting)
252
     *
253
     * @return bool
254
     */
255 9
    public function isGrantedToDelete(
256
        int $userIdToDelete,
257
        bool $log = true
258
    ): bool {
259 9
        if ($this->loggedInUserId === null) {
260
            $this->logger->error(
261
                'loggedInUserId not set while authorization check isGrantedToDelete $userIdToDelete: '
262
                . $userIdToDelete
263
            );
264
265
            return false;
266
        }
267 9
        $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
268 9
            $this->loggedInUserId
269 9
        );
270 9
        $userToDeleteRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId($userIdToDelete);
271
272
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
273
        // * Lower hierarchy number means higher privileged role
274 9
        $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
275
276
        // Only managing_advisor and higher are allowed to delete user and only if the user is advisor or lower or their own
277 9
        if (($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($authenticatedUserRoleH...UserRole::ADMIN->value], Probably Intended Meaning: $authenticatedUserRoleHi...serRole::ADMIN->value])
Loading history...
278 7
                && ($userToDeleteRoleHierarchy >= $userRoleHierarchies[UserRole::ADVISOR->value]
279 7
                    || $userIdToDelete === $this->loggedInUserId))
280
            // or authenticated user is admin
281 9
            || $authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::ADMIN->value]) {
282 6
            return true;
283
        }
284
285 3
        if ($log === true) {
286 2
            $this->logger->notice(
287 2
                'User ' . $this->loggedInUserId . ' tried to delete user but isn\'t allowed.'
288 2
            );
289
        }
290
291 3
        return false;
292
    }
293
294
    /**
295
     * Check if authenticated user is allowed to read user.
296
     *
297
     * @param int|null $userIdToRead null when check for all users
298
     * @param bool $log log if forbidden (expected false when function is called for privilege setting)
299
     *
300
     * @return bool
301
     */
302 149
    public function isGrantedToRead(?int $userIdToRead = null, bool $log = true): bool
303
    {
304 149
        if ($this->loggedInUserId === null) {
305 1
            $this->logger->error(
306 1
                'loggedInUserId not set while authorization check isGrantedToRead $userIdToRead: '
307 1
                . $userIdToRead
308 1
            );
309
310 1
            return false;
311
        }
312 148
        $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
313 148
            $this->loggedInUserId
314 148
        );
315
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
316
        // * Lower hierarchy number means higher privileged role
317 148
        $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
318
319
        // Only managing advisor and higher privileged are allowed to see other users
320
        // If the user role hierarchy of the authenticated user is lower or equal
321
        // than the one from the managing advisor -> authorized
322 148
        if ($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
323
            // or user wants to view his own profile in which case also -> authorized
324 148
            || $this->loggedInUserId === $userIdToRead) {
325 78
            return true;
326
        }
327
328 72
        if ($log === true) {
329 3
            $this->logger->notice('User ' . $this->loggedInUserId . ' tried to read user but isn\'t allowed.');
330
        }
331
332 72
        return false;
333
    }
334
335
    /**
336
     * Check if the authenticated user is allowed to read user activity.
337
     *
338
     * @param int $userIdToRead
339
     * @param bool $log log if forbidden
340
     *
341
     * @return bool
342
     */
343 2
    public function isGrantedToReadUserActivity(
344
        int $userIdToRead,
345
        bool $log = true
346
    ): bool {
347 2
        if ($this->loggedInUserId === null) {
348
            $this->logger->error(
349
                'loggedInUserId not set while authorization check isGrantedToReadUserActivity $userIdToRead: '
350
                . $userIdToRead
351
            );
352
353
            return false;
354
        }
355
356 2
        $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
357 2
            $this->loggedInUserId
358 2
        );
359
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
360
        // * Lower hierarchy number means higher privileged role
361 2
        $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
362
363 2
        $userToReadRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId($userIdToRead);
364
365
        // Only managing advisors are allowed to see user activity, but only if target user role is not higher than also managing advisor
366 2
        if (($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
367 1
                && $userToReadRoleHierarchy >= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value])
368
            // or authenticated user is admin
369 2
            || $authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::ADMIN->value]
370
            // or user wants to view his own activity
371 2
            || $this->loggedInUserId === $userIdToRead) {
372 2
            return true;
373
        }
374
375 1
        if ($log === true) {
376 1
            $this->logger->notice(
377 1
                "User $this->loggedInUserId tried to read activity of user $userIdToRead but isn't allowed."
378 1
            );
379
        }
380
381 1
        return false;
382
    }
383
}
384