Issues (186)

includes/Security/SecurityManager.php (1 issue)

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