Passed
Pull Request — master (#32)
by Robbie
03:47
created

SchemaGenerator::getSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 0
dl 0
loc 18
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\MFA\Service;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Core\Extensible;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\MFA\Exception\MemberNotFoundException;
9
use SilverStripe\MFA\Extension\MemberExtension;
10
use SilverStripe\MFA\Store\StoreInterface;
11
use SilverStripe\ORM\FieldType\DBDate;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\Security;
14
use SilverStripe\SiteConfig\SiteConfig;
15
16
/**
17
 * Generates a multi-factor authentication frontend app schema from the given request
18
 */
19
class SchemaGenerator
20
{
21
    use Extensible;
22
    use Injectable;
23
24
    /**
25
     * @var HTTPRequest
26
     */
27
    protected $request;
28
29
    /**
30
     * @var StoreInterface
31
     */
32
    protected $store;
33
34
    /**
35
     * @param HTTPRequest $request
36
     * @param StoreInterface $store
37
     */
38
    public function __construct(HTTPRequest $request, StoreInterface $store)
39
    {
40
        $this->setRequest($request);
41
        $this->setStore($store);
42
    }
43
44
    /**
45
     * Gets the schema data for the multi factor authentication app, using the current Member as context
46
     *
47
     * @return array
48
     */
49
    public function getSchema()
50
    {
51
        $registeredMethods = $this->getRegisteredMethods();
52
53
        // Skip registration details if the user has already registered this method
54
        $exclude = array_column($registeredMethods, 'urlSegment');
55
56
        $schema = [
57
            'registeredMethods' => $registeredMethods,
58
            'availableMethods' => $this->getAvailableMethods($exclude),
59
            'defaultMethod' => $this->getDefaultMethod(),
60
            'canSkip' => $this->canSkipMFA(),
61
            'shouldRedirect' => $this->shouldRedirectToMFA(),
62
        ];
63
64
        $this->extend('updateSchema', $schema);
65
66
        return $schema;
67
    }
68
69
    /**
70
     * @return Member|MemberExtension
71
     * @throws MemberNotFoundException
72
     */
73
    public function getMember()
74
    {
75
        $member = $this->store->getMember() ?: Security::getCurrentUser();
76
77
        // If we don't have a valid member we shouldn't be here...
78
        if (!$member) {
79
            throw new MemberNotFoundException();
80
        }
81
82
        return $member;
83
    }
84
85
    /**
86
     * Get a list of methods registered to the user
87
     *
88
     * @return array[]
89
     */
90
    protected function getRegisteredMethods()
91
    {
92
        $registeredMethods = $this->getMember()->RegisteredMFAMethods();
93
94
        // Generate a map of URL Segments to 'lead in labels', which are used to describe the method in the login UI
95
        $registeredMethodDetails = [];
96
        foreach ($registeredMethods as $registeredMethod) {
97
            $method = $registeredMethod->getMethod();
98
99
            $registeredMethodDetails[] = [
100
                'urlSegment' => $method->getURLSegment(),
101
                'leadInLabel' => $method->getLoginHandler()->getLeadInLabel()
102
            ];
103
        }
104
105
        return $registeredMethodDetails;
106
    }
107
108
    /**
109
     * Get details in a list for all available methods, optionally excluding those with urlSegments provided in
110
     * $exclude
111
     *
112
     * @param array $exclude
113
     * @return array[]
114
     */
115
    protected function getAvailableMethods(array $exclude = [])
116
    {
117
        // Prepare an array to hold details for methods available to register
118
        $availableMethods = [];
119
120
        // Get all methods enabled on the site
121
        $allMethods = MethodRegistry::singleton()->getMethods();
122
123
        // Compile details for methods that aren't already registered to the user
124
        foreach ($allMethods as $method) {
125
            if (in_array($method->getURLSegment(), $exclude)) {
126
                continue;
127
            }
128
129
            $registerHandler = $method->getRegisterHandler();
130
131
            $availableMethods[] = [
132
                'urlSegment' => $method->getURLSegment(),
133
                'name' => $registerHandler->getName(),
134
                'description' => $registerHandler->getDescription(),
135
                'supportLink' => $registerHandler->getSupportLink(),
136
            ];
137
        }
138
139
        return $availableMethods;
140
    }
141
142
    /**
143
     * Get the URL Segment for the configured default method on the current member, or null if none is configured
144
     *
145
     * @return string|null
146
     */
147
    protected function getDefaultMethod()
148
    {
149
        $defaultMethod = $this->getMember()->DefaultRegisteredMethod;
150
        return $defaultMethod ? $defaultMethod->getMethod()->getURLSegment() : null;
0 ignored issues
show
Bug introduced by
The method getMethod() does not exist on SilverStripe\MFA\Method\MethodInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

150
        return $defaultMethod ? $defaultMethod->/** @scrutinizer ignore-call */ getMethod()->getURLSegment() : null;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
151
    }
152
153
    /**
154
     * Whether the current member can skip the multi factor authentication registration process.
155
     *
156
     * This is determined by a combination of:
157
     *  - Whether MFA is required or optional
158
     *  - If MFA is required, whether there is a grace period
159
     *  - If MFA is required and there is a grace period, whether we're currently within that timeframe
160
     *
161
     * @return bool
162
     */
163
    public function canSkipMFA()
164
    {
165
        if ($this->isMFARequired()) {
166
            return false;
167
        }
168
169
        // If they've already registered MFA methods we will not allow them to skip the authentication process
170
        $registeredMethods = $this->getRegisteredMethods();
171
        if (count($registeredMethods)) {
172
            return false;
173
        }
174
175
        // MFA is optional, or is required but might be within a grace period (see isMFARequired)
176
        return true;
177
    }
178
179
    /**
180
     * Whether the authentication process should redirect the user to multi factor authentication registration or
181
     * login.
182
     *
183
     * This is determined by a combination of:
184
     *  - Whether MFA is required or optional
185
     *  - Whether the user has registered MFA methods already
186
     *  - If the user doesn't have any registered MFA methods already, and MFA is optional, whether the user has opted
187
     *    to skip the registration process
188
     *
189
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
190
     *
191
     * @return bool;
192
     */
193
    protected function shouldRedirectToMFA()
194
    {
195
        $isRequired = $this->isMFARequired();
196
        if ($isRequired) {
197
            return true;
198
        }
199
200
        $hasSkipped = $this->getMember()->HasSkippedMFARegistration;
201
        if (!$hasSkipped) {
202
            return true;
203
        }
204
205
        return false;
206
    }
207
208
    /**
209
     * Whether multi factor authentication is required for site members. This also takes into account whether a
210
     * grace period is set and whether we're currently inside the window for it.
211
     *
212
     * Note that in determining this, we ignore whether or not MFA is enabled for the site in general.
213
     *
214
     * @return bool
215
     */
216
    protected function isMFARequired()
217
    {
218
        $siteConfig = SiteConfig::current_site_config();
219
220
        $isRequired = $siteConfig->MFARequired;
221
        if (!$isRequired) {
222
            return false;
223
        }
224
225
        $gracePeriod = $siteConfig->MFAGracePeriodExpires;
226
        if ($isRequired && !$gracePeriod) {
227
            return true;
228
        }
229
230
        /** @var DBDate $gracePeriodDate */
231
        $gracePeriodDate = $siteConfig->dbObject('MFAGracePeriodExpires');
232
        if ($isRequired && $gracePeriodDate->InPast()) {
233
            return true;
234
        }
235
236
        // MFA is required, a grace period is set, and it's in the future
237
        return false;
238
    }
239
240
    /**
241
     * @param HTTPRequest $request
242
     * @return $this
243
     */
244
    public function setRequest(HTTPRequest $request)
245
    {
246
        $this->request = $request;
247
        return $this;
248
    }
249
250
    /**
251
     * @param StoreInterface $store
252
     * @return $this
253
     */
254
    public function setStore(StoreInterface $store)
255
    {
256
        $this->store = $store;
257
        return $this;
258
    }
259
}
260