Completed
Push — master ( f2e76a...2195c2 )
by
unknown
23s queued 10s
created

EnforcementManager   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 64
dl 0
loc 226
rs 9.84
c 0
b 0
f 0
wmc 32

7 Methods

Rating   Name   Duplication   Size   Complexity  
A isGracePeriodInEffect() 0 22 4
A isEnabled() 0 3 1
A canSkipMFA() 0 14 3
A isMFARequired() 0 23 6
B hasAdminAccess() 0 22 7
B shouldRedirectToMFA() 0 27 8
A hasCompletedRegistration() 0 12 3
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Service;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\MFA\Extension\MemberExtension;
9
use SilverStripe\MFA\Extension\SiteConfigExtension;
10
use SilverStripe\ORM\FieldType\DBDate;
11
use SilverStripe\Security\Member;
12
use SilverStripe\SiteConfig\SiteConfig;
13
use SilverStripe\SiteConfig\SiteConfigLeftAndMain;
14
15
/**
16
 * The EnforcementManager class is responsible for making decisions regarding multi factor authentication app flow,
17
 * e.g. "should we redirect to the MFA section", "can the user skip MFA registration" etc.
18
 */
19
class EnforcementManager
20
{
21
    use Configurable;
22
    use Injectable;
23
24
    /**
25
     * Indicate how many MFA methods the user must authenticate with before they are considered logged in
26
     *
27
     * @config
28
     * @var int
29
     */
30
    private static $required_mfa_methods = 1;
0 ignored issues
show
introduced by
The private property $required_mfa_methods is not used, and could be removed.
Loading history...
31
32
    /**
33
     * If true, redirects to MFA will only provided when the current user has access to some part of the CMS or
34
     * administration area.
35
     *
36
     * @config
37
     * @var bool
38
     */
39
    private static $requires_admin_access = true;
0 ignored issues
show
introduced by
The private property $requires_admin_access is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Whether enforcement of MFA is enabled. If this is disabled, users will not be redirected to MFA registration
43
     * or verification on login flows.
44
     *
45
     * @config
46
     * @var bool
47
     */
48
    private static $enabled = true;
0 ignored issues
show
introduced by
The private property $enabled is not used, and could be removed.
Loading history...
49
50
    /**
51
     * Whether the current member can skip the multi factor authentication registration process.
52
     *
53
     * This is determined by a combination of:
54
     *  - Whether MFA is required or optional
55
     *  - If MFA is required, whether there is a grace period
56
     *  - If MFA is required and there is a grace period, whether we're currently within that timeframe
57
     *
58
     * @param Member&MemberExtension $member
59
     * @return bool
60
     */
61
    public function canSkipMFA(Member $member): bool
62
    {
63
        if ($this->isMFARequired()) {
64
            return false;
65
        }
66
67
        // If they've already registered MFA methods we will not allow them to skip the authentication process
68
        $registeredMethods = $member->RegisteredMFAMethods();
69
        if ($registeredMethods->exists()) {
70
            return false;
71
        }
72
73
        // MFA is optional, or is required but might be within a grace period (see isMFARequired)
74
        return true;
75
    }
76
77
    /**
78
     * Whether the authentication process should redirect the user to multi factor authentication registration or
79
     * login.
80
     *
81
     * This is determined by a combination of:
82
     *  - Whether MFA is enabled
83
     *  - Whether MFA is required or optional
84
     *  - Whether the user has registered MFA methods already
85
     *  - If the user doesn't have any registered MFA methods already, and MFA is optional, whether the user has opted
86
     *    to skip the registration process
87
     *
88
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
89
     *
90
     * @param Member&MemberExtension $member
91
     * @return bool
92
     */
93
    public function shouldRedirectToMFA(Member $member): bool
94
    {
95
        if (!$this->isEnabled()) {
96
            return false;
97
        }
98
99
        if ($this->config()->get('requires_admin_access') && !$this->hasAdminAccess($member)) {
100
            return false;
101
        }
102
103
        if ($member->RegisteredMFAMethods()->exists()) {
104
            return true;
105
        }
106
107
        if ($this->isMFARequired()) {
108
            return true;
109
        }
110
111
        if ($this->isGracePeriodInEffect()) {
112
            return true;
113
        }
114
115
        if (!$member->HasSkippedMFARegistration) {
116
            return true;
117
        }
118
119
        return false;
120
    }
121
122
    /**
123
     * Check if the provided member has registered the required MFA methods. This includes a "back-up" method set in
124
     * configuration plus at least one other method.
125
     * Note that this method returns true if there is no backup method registered (and they have one other method
126
     *
127
     * @param Member&MemberExtension $member
128
     * @return bool
129
     */
130
    public function hasCompletedRegistration(Member $member): bool
131
    {
132
        $methodCount = $member->RegisteredMFAMethods()->count();
133
134
        $backupMethod = Config::inst()->get(MethodRegistry::class, 'default_backup_method');
135
        if (!$backupMethod) {
136
            // Ensure they have at least one method
137
            return $methodCount > 0;
138
        }
139
140
        // Ensure they have the required backup method and at least 2 methods (the backup method plus one other)
141
        return ((bool) $member->RegisteredMFAMethods()->find('MethodClassName', $backupMethod)) && $methodCount > 1;
142
    }
143
144
    /**
145
     * Whether multi factor authentication is required for site members. This also takes into account whether a
146
     * grace period is set and whether we're currently inside the window for it.
147
     *
148
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
149
     *
150
     * @return bool
151
     */
152
    public function isMFARequired(): bool
153
    {
154
        /** @var SiteConfig&SiteConfigExtension $siteConfig */
155
        $siteConfig = SiteConfig::current_site_config();
156
157
        $isRequired = $siteConfig->MFARequired;
158
        if (!$isRequired) {
159
            return false;
160
        }
161
162
        $gracePeriod = $siteConfig->MFAGracePeriodExpires;
163
        if ($isRequired && !$gracePeriod) {
164
            return true;
165
        }
166
167
        /** @var DBDate $gracePeriodDate */
168
        $gracePeriodDate = $siteConfig->dbObject('MFAGracePeriodExpires');
169
        if ($isRequired && $gracePeriodDate->InPast()) {
170
            return true;
171
        }
172
173
        // MFA is required, a grace period is set, and it's in the future
174
        return false;
175
    }
176
177
    /**
178
     * Specifically determines whether the MFA Grace Period is currently active.
179
     *
180
     * @return bool
181
     */
182
    public function isGracePeriodInEffect(): bool
183
    {
184
        /** @var SiteConfig&SiteConfigExtension $siteConfig */
185
        $siteConfig = SiteConfig::current_site_config();
186
187
        $isRequired = $siteConfig->MFARequired;
188
        if (!$isRequired) {
189
            return false;
190
        }
191
192
        $gracePeriod = $siteConfig->MFAGracePeriodExpires;
193
        if (!$gracePeriod) {
194
            return false;
195
        }
196
197
        /** @var DBDate $gracePeriodDate */
198
        $gracePeriodDate = $siteConfig->dbObject('MFAGracePeriodExpires');
199
        if ($gracePeriodDate->InPast()) {
200
            return false;
201
        }
202
203
        return true;
204
    }
205
206
    /**
207
     * Decides whether the current user has access to any LeftAndMain controller, which indicates some level
208
     * of access to the CMS.
209
     *
210
     * See LeftAndMain::init().
211
     *
212
     * @param Member $member
213
     * @return bool
214
     */
215
    protected function hasAdminAccess(Member $member): bool
216
    {
217
        // We need to use an actual LeftAndMain implementation, otherwise LeftAndMain::canView() returns true
218
        // because no required permission codes are declared
219
        $leftAndMain = SiteConfigLeftAndMain::singleton();
220
        if ($leftAndMain->canView($member)) {
221
            return true;
222
        }
223
224
        // Look through all LeftAndMain subclasses to find if one permits the member to view
225
        $menu = $leftAndMain->MainMenu();
226
        foreach ($menu as $candidate) {
227
            if ($candidate->Link
228
                && $candidate->Link != $leftAndMain->Link()
229
                && $candidate->MenuItem->controller
230
                && singleton($candidate->MenuItem->controller)->canView($member)
231
            ) {
232
                return true;
233
            }
234
        }
235
236
        return false;
237
    }
238
239
    /**
240
     * @return bool
241
     */
242
    protected function isEnabled(): bool
243
    {
244
        return (bool) $this->config()->get('enabled');
245
    }
246
}
247