Passed
Push — performance ( a79a56 )
by Simon
03:51
created

SecurityManager::getCachedActiveRoles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 10
c 1
b 0
f 0
cc 2
nc 2
nop 3
crap 2
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\User;
12
use Waca\DataObjects\UserRole;
13
use Waca\IdentificationVerifier;
14
15
final class SecurityManager
16
{
17
    const ALLOWED = 1;
18
    const ERROR_NOT_IDENTIFIED = 2;
19
    const ERROR_DENIED = 3;
20
    /** @var IdentificationVerifier */
21
    private $identificationVerifier;
22
    /**
23
     * @var RoleConfiguration
24
     */
25
    private $roleConfiguration;
26
27
    private $cache = [];
28
29
    /**
30
     * SecurityManager constructor.
31
     *
32
     * @param IdentificationVerifier $identificationVerifier
33
     * @param RoleConfiguration      $roleConfiguration
34
     */
35 2
    public function __construct(
36
        IdentificationVerifier $identificationVerifier,
37
        RoleConfiguration $roleConfiguration
38
    ) {
39 2
        $this->identificationVerifier = $identificationVerifier;
40 2
        $this->roleConfiguration = $roleConfiguration;
41 2
    }
42
43
    /**
44
     * Tests if a user is allowed to perform an action.
45
     *
46
     * This method should form a hard, deterministic security barrier, and only return true if it is absolutely sure
47
     * that a user should have access to something.
48
     *
49
     * @param string $page
50
     * @param string $route
51
     * @param User   $user
52
     *
53
     * @return int
54
     *
55
     * @category Security-Critical
56
     */
57 2
    public function allows($page, $route, User $user)
58
    {
59 2
        $this->getCachedActiveRoles($user, $activeRoles, $inactiveRoles);
60
61 2
        $availableRights = $this->flattenRoles($activeRoles);
62 2
        $testResult = $this->findResult($availableRights, $page, $route);
63
64 2
        if ($testResult !== null) {
65
            // We got a firm result here, so just return it.
66 2
            return $testResult;
67
        }
68
69
        // No firm result yet, so continue testing the inactive roles so we can give a better error.
70
        $inactiveRights = $this->flattenRoles($inactiveRoles);
71
        $testResult = $this->findResult($inactiveRights, $page, $route);
72
73
        if ($testResult === self::ALLOWED) {
74
            // The user is allowed to access this, but their role is inactive.
75
            return self::ERROR_NOT_IDENTIFIED;
76
        }
77
78
        // Other options from the secondary test are denied and inconclusive, which at this point defaults to denied.
79
        return self::ERROR_DENIED;
80
    }
81
82
    /**
83
     * @param array  $pseudoRole The role (flattened) to check
84
     * @param string $page       The page class to check
85
     * @param string $route      The page route to check
86
     *
87
     * @return int|null
88
     */
89 2
    private function findResult($pseudoRole, $page, $route)
90
    {
91 2
        if (isset($pseudoRole[$page])) {
92
            // check for deny on catch-all route
93 2
            if (isset($pseudoRole[$page][RoleConfiguration::ALL])) {
94 2
                if ($pseudoRole[$page][RoleConfiguration::ALL] === RoleConfiguration::ACCESS_DENY) {
95 1
                    return self::ERROR_DENIED;
96
                }
97
            }
98
99
            // check normal route
100 1
            if (isset($pseudoRole[$page][$route])) {
101
                if ($pseudoRole[$page][$route] === RoleConfiguration::ACCESS_DENY) {
102
                    return self::ERROR_DENIED;
103
                }
104
105
                if ($pseudoRole[$page][$route] === RoleConfiguration::ACCESS_ALLOW) {
106
                    return self::ALLOWED;
107
                }
108
            }
109
110
            // check for allowed on catch-all route
111 1
            if (isset($pseudoRole[$page][RoleConfiguration::ALL])) {
112 1
                if ($pseudoRole[$page][RoleConfiguration::ALL] === RoleConfiguration::ACCESS_ALLOW) {
113 1
                    return self::ALLOWED;
114
                }
115
            }
116
        }
117
118
        // return indeterminate result
119
        return null;
120
    }
121
122
    /**
123
     * Takes an array of roles and flattens the values to a single set.
124
     *
125
     * @param array $activeRoles
126
     *
127
     * @return array
128
     */
129 2
    private function flattenRoles($activeRoles)
130
    {
131 2
        $result = array();
132
133 2
        $roleConfig = $this->roleConfiguration->getApplicableRoles($activeRoles);
134
135
        // Iterate over every page in every role
136 2
        foreach ($roleConfig as $role) {
137 2
            foreach ($role as $page => $pageRights) {
138
                // Create holder in result for this page
139 2
                if (!isset($result[$page])) {
140 2
                    $result[$page] = array();
141
                }
142
143 2
                foreach ($pageRights as $action => $permission) {
144
                    // Deny takes precedence, so if it's set, don't change it.
145 2
                    if (isset($result[$page][$action])) {
146
                        if ($result[$page][$action] === RoleConfiguration::ACCESS_DENY) {
147
                            continue;
148
                        }
149
                    }
150
151 2
                    if ($permission === RoleConfiguration::ACCESS_DEFAULT) {
152
                        // Configured to do precisely nothing.
153
                        continue;
154
                    }
155
156 2
                    $result[$page][$action] = $permission;
157
                }
158
            }
159
        }
160
161 2
        return $result;
162
    }
163
164
    /**
165
     * @param User  $user
166
     * @param array $activeRoles
167
     * @param array $inactiveRoles
168
     */
169 2
    public function getActiveRoles(User $user, &$activeRoles, &$inactiveRoles)
170
    {
171
        // Default to the community user here, because the main user is logged out
172 2
        $identified = false;
173 2
        $userRoles = array('public');
174
175
        // if we're not the community user, get our real rights.
176 2
        if (!$user->isCommunityUser()) {
177
            // Check the user's status - only active users are allowed the effects of roles
178
179
            $userRoles[] = 'loggedIn';
180
181
            if ($user->isActive()) {
182
                $ur = UserRole::getForUser($user->getId(), $user->getDatabase());
183
184
                // NOTE: public is still in this array.
185
                foreach ($ur as $r) {
186
                    $userRoles[] = $r->getRole();
187
                }
188
189
                $identified = $user->isIdentified($this->identificationVerifier);
190
            }
191
        }
192
193 2
        $activeRoles = array();
194 2
        $inactiveRoles = array();
195
196 2
        foreach ($userRoles as $v) {
197 2
            if ($this->roleConfiguration->roleNeedsIdentification($v)) {
198
                if ($identified) {
199
                    $activeRoles[] = $v;
200
                }
201
                else {
202
                    $inactiveRoles[] = $v;
203
                }
204
            }
205
            else {
206 2
                $activeRoles[] = $v;
207
            }
208
        }
209 2
    }
210
211
    /**
212
     * @param User  $user
213
     * @param array $activeRoles
214
     * @param array $inactiveRoles
215
     */
216 2
    public function getCachedActiveRoles(User $user, &$activeRoles, &$inactiveRoles)
217
    {
218 2
        if (!array_key_exists($user->getId(), $this->cache)) {
219 2
            $this->getActiveRoles($user, $retrievedActiveRoles, $retrievedInactiveRoles);
220 2
            $this->cache[$user->getId()] = ['active' => $retrievedActiveRoles, 'inactive' => $retrievedInactiveRoles];
221
        }
222
223 2
        $activeRoles = $this->cache[$user->getId()]['active'];
224 2
        $inactiveRoles = $this->cache[$user->getId()]['inactive'];
225 2
    }
226
227
    public function getRoleConfiguration()
228
    {
229
        return $this->roleConfiguration;
230
    }
231
}
232