Completed
Pull Request — master (#571)
by Matthew
03:49 queued 44s
created

SecurityManager::allows()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.8449

Importance

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