Passed
Push — master ( aa0dec...0bd8f9 )
by Garion
02:29
created

EnforcementManager::shouldRedirectToMFA()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 11
nc 6
nop 1
dl 0
loc 23
rs 8.8333
c 0
b 0
f 0
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 the current member can skip the multi factor authentication registration process.
43
     *
44
     * This is determined by a combination of:
45
     *  - Whether MFA is required or optional
46
     *  - If MFA is required, whether there is a grace period
47
     *  - If MFA is required and there is a grace period, whether we're currently within that timeframe
48
     *
49
     * @param Member&MemberExtension $member
50
     * @return bool
51
     */
52
    public function canSkipMFA(Member $member): bool
53
    {
54
        if ($this->isMFARequired()) {
55
            return false;
56
        }
57
58
        // If they've already registered MFA methods we will not allow them to skip the authentication process
59
        $registeredMethods = $member->RegisteredMFAMethods();
60
        if ($registeredMethods->exists()) {
61
            return false;
62
        }
63
64
        // MFA is optional, or is required but might be within a grace period (see isMFARequired)
65
        return true;
66
    }
67
68
    /**
69
     * Whether the authentication process should redirect the user to multi factor authentication registration or
70
     * login.
71
     *
72
     * This is determined by a combination of:
73
     *  - Whether MFA is enabled
74
     *  - Whether MFA is required or optional
75
     *  - Whether the user has registered MFA methods already
76
     *  - If the user doesn't have any registered MFA methods already, and MFA is optional, whether the user has opted
77
     *    to skip the registration process
78
     *
79
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
80
     *
81
     * @param Member&MemberExtension $member
82
     * @return bool
83
     */
84
    public function shouldRedirectToMFA(Member $member): bool
85
    {
86
        if ($this->config()->get('requires_admin_access') && !$this->hasAdminAccess($member)) {
87
            return false;
88
        }
89
90
        if ($member->RegisteredMFAMethods()->exists()) {
91
            return true;
92
        }
93
94
        if ($this->isMFARequired()) {
95
            return true;
96
        }
97
98
        if ($this->isGracePeriodInEffect()) {
99
            return true;
100
        }
101
102
        if (!$member->HasSkippedMFARegistration) {
103
            return true;
104
        }
105
106
        return false;
107
    }
108
109
    /**
110
     * Check if the provided member has registered the required MFA methods. This includes a "back-up" method set in
111
     * configuration plus at least one other method.
112
     * Note that this method returns true if there is no backup method registered (and they have one other method
113
     *
114
     * @param Member&MemberExtension $member
115
     * @return bool
116
     */
117
    public function hasCompletedRegistration(Member $member): bool
118
    {
119
        $methodCount = $member->RegisteredMFAMethods()->count();
120
121
        $backupMethod = Config::inst()->get(MethodRegistry::class, 'default_backup_method');
122
        if (!$backupMethod) {
123
            // Ensure they have at least one method
124
            return $methodCount > 0;
125
        }
126
127
        // Ensure they have the required backup method and at least 2 methods (the backup method plus one other)
128
        return ((bool) $member->RegisteredMFAMethods()->find('MethodClassName', $backupMethod)) && $methodCount > 1;
129
    }
130
131
    /**
132
     * Whether multi factor authentication is required for site members. This also takes into account whether a
133
     * grace period is set and whether we're currently inside the window for it.
134
     *
135
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
136
     *
137
     * @return bool
138
     */
139
    public function isMFARequired(): bool
140
    {
141
        /** @var SiteConfig&SiteConfigExtension $siteConfig */
142
        $siteConfig = SiteConfig::current_site_config();
143
144
        $isRequired = $siteConfig->MFARequired;
145
        if (!$isRequired) {
146
            return false;
147
        }
148
149
        $gracePeriod = $siteConfig->MFAGracePeriodExpires;
150
        if ($isRequired && !$gracePeriod) {
151
            return true;
152
        }
153
154
        /** @var DBDate $gracePeriodDate */
155
        $gracePeriodDate = $siteConfig->dbObject('MFAGracePeriodExpires');
156
        if ($isRequired && $gracePeriodDate->InPast()) {
157
            return true;
158
        }
159
160
        // MFA is required, a grace period is set, and it's in the future
161
        return false;
162
    }
163
164
    /**
165
     * Specifically determines whether the MFA Grace Period is currently active.
166
     *
167
     * @return bool
168
     */
169
    public function isGracePeriodInEffect(): bool
170
    {
171
        /** @var SiteConfig&SiteConfigExtension $siteConfig */
172
        $siteConfig = SiteConfig::current_site_config();
173
174
        $isRequired = $siteConfig->MFARequired;
175
        if (!$isRequired) {
176
            return false;
177
        }
178
179
        $gracePeriod = $siteConfig->MFAGracePeriodExpires;
180
        if (!$gracePeriod) {
181
            return false;
182
        }
183
184
        /** @var DBDate $gracePeriodDate */
185
        $gracePeriodDate = $siteConfig->dbObject('MFAGracePeriodExpires');
186
        if ($gracePeriodDate->InPast()) {
187
            return false;
188
        }
189
190
        return true;
191
    }
192
193
    /**
194
     * Decides whether the current user has access to any LeftAndMain controller, which indicates some level
195
     * of access to the CMS.
196
     *
197
     * See LeftAndMain::init().
198
     *
199
     * @param Member $member
200
     * @return bool
201
     */
202
    protected function hasAdminAccess(Member $member): bool
203
    {
204
        // We need to use an actual LeftAndMain implementation, otherwise LeftAndMain::canView() returns true
205
        // because no required permission codes are declared
206
        $leftAndMain = SiteConfigLeftAndMain::singleton();
207
        if ($leftAndMain->canView($member)) {
208
            return true;
209
        }
210
211
        // Look through all LeftAndMain subclasses to find if one permits the member to view
212
        $menu = $leftAndMain->MainMenu();
213
        foreach ($menu as $candidate) {
214
            if ($candidate->Link
215
                && $candidate->Link != $leftAndMain->Link()
216
                && $candidate->MenuItem->controller
217
                && singleton($candidate->MenuItem->controller)->canView($member)
218
            ) {
219
                return true;
220
            }
221
        }
222
223
        return false;
224
    }
225
}
226