isGrantedToUpdate()   C
last analyzed

Complexity

Conditions 13
Paths 25

Size

Total Lines 92
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 13.217

Importance

Changes 0
Metric Value
cc 13
eloc 42
nc 25
nop 3
dl 0
loc 92
ccs 41
cts 46
cp 0.8913
crap 13.217
rs 6.6166
c 0
b 0
f 0

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
namespace App\Module\User\Update\Service;
4
5
use App\Application\Data\UserNetworkSessionData;
6
use App\Module\Authorization\Repository\AuthorizationUserRoleFinderRepository;
0 ignored issues
show
Bug introduced by
The type App\Module\Authorization...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\Module\User\AssignRole\Service\UserAssignRoleAuthorizationChecker;
8
use App\Module\User\Enum\UserRole;
9
use App\Module\User\Update\Repository\UserUpdateAuthorizationRoleFinderRepository;
0 ignored issues
show
Bug introduced by
The type App\Module\User\Update\R...ionRoleFinderRepository 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...
10
use Psr\Log\LoggerInterface;
11
12
/**
13
 * Check if authenticated user is permitted to do actions
14
 * Roles: newcomer < advisor < managing_advisor < administrator.
15
 */
16
final class UserUpdateAuthorizationChecker
17
{
18
    private ?int $loggedInUserId = null;
19
20 27
    public function __construct(
21
        private readonly UserNetworkSessionData $userNetworkSessionData,
22
        private readonly AuthorizationUserRoleFinderRepository $authorizationUserRoleFinderRepository,
23
        private readonly UserUpdateAuthorizationRoleFinderRepository $userAuthorizationRoleFinderRepository,
24
        private readonly UserAssignRoleAuthorizationChecker $userAssignRoleAuthorizationChecker,
25
        private readonly LoggerInterface $logger,
26
    ) {
27
        // Fix error $userId must not be accessed before initialization
28 27
        $this->loggedInUserId = $this->userNetworkSessionData->userId ?? null;
29
    }
30
31
    /**
32
     * Logic to check if logged-in user is granted to update user.
33
     * This function has a high cyclomatic complexity due to the many if-statements,
34
     * but for now I find it more readable than splitting it up into multiple functions.
35
     *
36
     * @param array $userDataToUpdate validated array with as key the column to
37
     * update and value the new value. There may be one or multiple entries,
38
     * depending on what the user wants to update
39
     * @param string|int $userIdToUpdate
40
     * @param bool $log log if forbidden (expected false when function is called for privilege setting)
41
     *
42
     * @return bool
43
     */
44 21
    public function isGrantedToUpdate(array $userDataToUpdate, string|int $userIdToUpdate, bool $log = true): bool
45
    {
46
        // Unset key id from data to update as is present in the array without the intention of being changed
47 21
        unset($userDataToUpdate['id']);
48 21
        if ($this->loggedInUserId === null) {
49
            $this->logger->error(
50
                'loggedInUserId not while user update authorization check' .
51
                json_encode($userDataToUpdate, JSON_PARTIAL_OUTPUT_ON_ERROR)
52
            );
53
54
            return false;
55
        }
56 21
        $grantedUpdateKeys = [];
57
58 21
        $authenticatedUserRoleHierarchy = $this->authorizationUserRoleFinderRepository->getRoleHierarchyByUserId(
59 21
            $this->loggedInUserId
60 21
        );
61 21
        $userToUpdateRoleData = $this->userAuthorizationRoleFinderRepository->getUserRoleDataFromUser((int)$userIdToUpdate);
62
        // Returns array with role name as key and hierarchy as value ['role_name' => hierarchy_int]
63
        // * Lower hierarchy number means higher privileged role
64 21
        $userRoleHierarchies = $this->authorizationUserRoleFinderRepository->getUserRolesHierarchies();
65
66
        // Only managing advisor or higher privileged can change users
67 21
        if ((($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]
68
                    // but only if user to change is advisor or lower
69 12
                    && $userToUpdateRoleData->hierarchy >= $userRoleHierarchies[UserRole::ADVISOR->value])
70
                // if user role is higher privileged than managing advisor (admin) -> authorized
71 18
                || $authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::ADMIN->value])
72
            // or if the user edits his own profile, also authorized to the next section
73 21
            || $this->loggedInUserId === (int)$userIdToUpdate
74
        ) {
75
            // Things that managing advisor and owner user are allowed to change
76
            // Personal info are values such as first name, last name and email
77 15
            $grantedUpdateKeys[] = 'personal_info';
78 15
            $grantedUpdateKeys[] = 'first_name';
79 15
            $grantedUpdateKeys[] = 'last_name';
80 15
            $grantedUpdateKeys[] = 'email';
81 15
            $grantedUpdateKeys[] = 'password_hash';
82 15
            $grantedUpdateKeys[] = 'theme';
83 15
            $grantedUpdateKeys[] = 'language';
84
            // If a new basic data field is added, it has to be added to provider userUpdateAuthorizationCases()
85
            // $basicDataChanges variable and invalid value to provider invalidUserUpdateCases()
86
87
            // Things that only managing_advisor and higher privileged are allowed to change
88
            // If the user is managing advisor we know by the parent if-statement
89
            // that the user to change has not higher role than advisor
90 15
            if ($authenticatedUserRoleHierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]) {
91 8
                $grantedUpdateKeys[] = 'status';
92
93
                // Check if the authenticated user is granted to attribute role if that's requested
94 8
                if (array_key_exists('user_role_id', $userDataToUpdate)
95 8
                    && $this->userAssignRoleAuthorizationChecker->userRoleIsGranted(
96 8
                        $userDataToUpdate['user_role_id'],
97 8
                        $userToUpdateRoleData->id,
98 8
                        $authenticatedUserRoleHierarchy,
99 8
                        $userRoleHierarchies
100 8
                    ) === true) {
101 1
                    $grantedUpdateKeys[] = 'user_role_id';
102
                }
103
104
                // There is a special case with passwords where the user can change his own password, but he needs to
105
                // provide the old password. If password_without_verification is added to $grantedUpdateKeys it means
106
                // that the authenticated user can change the password without the old password.
107
                // But if the user wants to change his own password, the old password is required regardless of role
108
                // so that nobody can change his password if the computer is left unattended and logged-in
109
                // https://security.stackexchange.com/a/24292 - to change other passwords it would be best if
110
                // the authenticated managing_advisor / admin password is asked instead of the old user password,
111
                // but this is too much complexity for this project.
112 8
                if ($this->loggedInUserId !== (int)$userIdToUpdate) {
113 6
                    $grantedUpdateKeys[] = 'password_without_verification';
114
                }
115
            }
116
            // Owner user (profile edit) is not allowed to change its user role or status
117
        }
118
119
        // Check that the data that the user wanted to update is in $grantedUpdateKeys array
120 21
        foreach ($userDataToUpdate as $key => $value) {
121
            // If at least one array key doesn't exist in $grantedUpdateKeys it means that user is not permitted
122 21
            if (!in_array($key, $grantedUpdateKeys, true)) {
123 13
                if ($log === true) {
124 8
                    $this->logger->notice(
125 8
                        'User ' . $this->loggedInUserId . ' tried to update user but isn\'t allowed to change' .
126 8
                        $key . ' to "' . $value . '".'
127 8
                    );
128
                }
129
130 13
                return false;
131
            }
132
        }
133
134
        // All keys in $userDataToUpdate are in $grantedUpdateKeys
135 10
        return true;
136
    }
137
}
138