Passed
Push — multiproject/local-access ( 5353e5...767924 )
by Simon
03:31
created

SecurityManager::userIsIdentified()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 14
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Security;
10
11
use Waca\DataObjects\Domain;
12
use Waca\DataObjects\User;
13
use Waca\DataObjects\UserRole;
14
use Waca\IIdentificationVerifier;
15
16
final class SecurityManager implements ISecurityManager
17
{
18
    private IIdentificationVerifier $identificationVerifier;
19
    private RoleConfigurationBase $roleConfiguration;
20
21
    private array $cache = [];
22
    private IUserAccessLoader $userAccessLoader;
23
24
    public function __construct(
25
        IIdentificationVerifier $identificationVerifier,
26
        RoleConfigurationBase $roleConfiguration,
27
        IUserAccessLoader $userAccessLoader
28
    ) {
29
        $this->identificationVerifier = $identificationVerifier;
30
        $this->roleConfiguration = $roleConfiguration;
31
        $this->userAccessLoader = $userAccessLoader;
32
    }
33
34
    /**
35
     * Tests if a user is allowed to perform an action.
36
     *
37
     * This method should form a hard, deterministic security barrier, and only return true if it is absolutely sure
38
     * that a user should have access to something.
39
     *
40
     * @category Security-Critical
41
     */
42
    public function allows(string $page, string $route, User $user): int
43
    {
44
        $this->getCachedActiveRoles($user, $activeRoles, $inactiveRoles);
45
46
        $availableRights = $this->roleConfiguration->getResultantRole($activeRoles);
47
        $testResult = $this->findResult($availableRights, $page, $route);
48
49
        if ($testResult !== null) {
50
            // We got a firm result here, so just return it.
51
            return $testResult;
52
        }
53
54
        // No firm result yet, so continue testing the inactive roles so we can give a better error.
55
        $inactiveRights = $this->roleConfiguration->getResultantRole($inactiveRoles);
56
        $testResult = $this->findResult($inactiveRights, $page, $route);
57
58
        if ($testResult === self::ALLOWED) {
59
            // The user is allowed to access this, but their role is inactive.
60
            return self::ERROR_NOT_IDENTIFIED;
61
        }
62
63
        // Other options from the secondary test are denied and inconclusive, which at this point defaults to denied.
64
        return self::ERROR_DENIED;
65
    }
66
67
    public function getActiveRoles(User $user, ?array &$activeRoles, ?array &$inactiveRoles)
68
    {
69
        // Default to the community user here, because the main user is logged out
70
        $identified = false;
71
        $userRoles = array('public');
72
73
        // if we're not the community user, get our real rights.
74
        if (!$user->isCommunityUser()) {
75
            // Check the user's status - only active users are allowed the effects of roles
76
77
            $userRoles[] = 'loggedIn';
78
79
            if ($user->isActive()) {
80
                // All active users get +user
81
                $userRoles[] = 'user';
82
83
                $loadedRoles = $this->userAccessLoader->loadRolesForUser($user);
84
85
                // NOTE: public is still in this array.
86
                $userRoles = array_merge($userRoles, $loadedRoles);
87
88
                $identified = $this->userIsIdentified($user);
89
            }
90
        }
91
92
        $activeRoles = array();
93
        $inactiveRoles = array();
94
95
        foreach ($userRoles as $v) {
96
            if ($this->roleConfiguration->roleNeedsIdentification($v)) {
97
                if ($identified) {
98
                    $activeRoles[] = $v;
99
                }
100
                else {
101
                    $inactiveRoles[] = $v;
102
                }
103
            }
104
            else {
105
                $activeRoles[] = $v;
106
            }
107
        }
108
    }
109
110
    public function getCachedActiveRoles(User $user, ?array &$activeRoles, ?array &$inactiveRoles): void
111
    {
112
        if (!array_key_exists($user->getId(), $this->cache)) {
113
            $this->getActiveRoles($user, $retrievedActiveRoles, $retrievedInactiveRoles);
114
            $this->cache[$user->getId()] = ['active' => $retrievedActiveRoles, 'inactive' => $retrievedInactiveRoles];
115
        }
116
117
        $activeRoles = $this->cache[$user->getId()]['active'];
118
        $inactiveRoles = $this->cache[$user->getId()]['inactive'];
119
    }
120
121
    public function getAvailableRoles(): array
122
    {
123
        return $this->roleConfiguration->getAvailableRoles();
124
    }
125
126
    /**
127
     * Tests a role for an ACL decision on a specific page/route
128
     *
129
     * @param array  $pseudoRole The role (flattened) to check
130
     * @param string $page       The page class to check
131
     * @param string $route      The page route to check
132
     *
133
     * @return int|null
134
     */
135
    private function findResult($pseudoRole, $page, $route)
136
    {
137
        if (isset($pseudoRole[$page])) {
138
            // check for deny on catch-all route
139
            if (isset($pseudoRole[$page][RoleConfigurationBase::ALL])) {
140
                if ($pseudoRole[$page][RoleConfigurationBase::ALL] === RoleConfigurationBase::ACCESS_DENY) {
141
                    return self::ERROR_DENIED;
142
                }
143
            }
144
145
            // check normal route
146
            if (isset($pseudoRole[$page][$route])) {
147
                if ($pseudoRole[$page][$route] === RoleConfigurationBase::ACCESS_DENY) {
148
                    return self::ERROR_DENIED;
149
                }
150
151
                if ($pseudoRole[$page][$route] === RoleConfigurationBase::ACCESS_ALLOW) {
152
                    return self::ALLOWED;
153
                }
154
            }
155
156
            // check for allowed on catch-all route
157
            if (isset($pseudoRole[$page][RoleConfigurationBase::ALL])) {
158
                if ($pseudoRole[$page][RoleConfigurationBase::ALL] === RoleConfigurationBase::ACCESS_ALLOW) {
159
                    return self::ALLOWED;
160
                }
161
            }
162
        }
163
164
        // return indeterminate result
165
        return null;
166
    }
167
168
    private function userIsIdentified(User $user): bool
169
    {
170
        if ($user->getForceIdentified() === false) {
171
            // User forced to be unidentified in the database.
172
            return false;
173
        }
174
175
        if ($user->getForceIdentified() === true) {
0 ignored issues
show
introduced by
The condition $user->getForceIdentified() === true is always true.
Loading history...
176
            // User forced to be identified in the database.
177
            return true;
178
        }
179
180
        // User not forced to any particular identified status; consult IdentificationVerifier
181
        return $this->identificationVerifier->isUserIdentified($user->getOnWikiName());
182
    }
183
}
184