AdminRegistrationController::finishRegistration()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 18
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 32
rs 9.3554
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Controller;
6
7
use Psr\Log\LoggerInterface;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\MFA\Extension\MemberExtension;
13
use SilverStripe\MFA\RequestHandler\BaseHandlerTrait;
14
use SilverStripe\MFA\RequestHandler\RegistrationHandlerTrait;
15
use SilverStripe\MFA\Service\MethodRegistry;
16
use SilverStripe\MFA\Service\RegisteredMethodManager;
17
use SilverStripe\MFA\State\AvailableMethodDetailsInterface;
18
use SilverStripe\ORM\ValidationException;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\Security;
21
use SilverStripe\Security\SecurityToken;
22
23
/**
24
 * This controller handles actions that a user may perform on MFA methods registered on their own account while logged
25
 * in. This includes deleting methods, registering new methods and replacing (re-registering) existing methods.
26
 */
27
class AdminRegistrationController extends LeftAndMain
28
{
29
    use RegistrationHandlerTrait;
30
    use BaseHandlerTrait;
31
32
    private static $url_segment = 'mfa';
0 ignored issues
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
33
34
    private static $ignore_menuitem = true;
0 ignored issues
show
introduced by
The private property $ignore_menuitem is not used, and could be removed.
Loading history...
35
36
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
37
        'GET register/$Method' => 'startRegistration',
38
        'POST register/$Method' => 'finishRegistration',
39
        'DELETE method/$Method' => 'removeRegisteredMethod',
40
        'PUT method/$Method/default' => 'setDefaultRegisteredMethod',
41
    ];
42
43
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
44
        'startRegistration',
45
        'finishRegistration',
46
        'removeRegisteredMethod',
47
        'setDefaultRegisteredMethod',
48
    ];
49
50
    private static $required_permission_codes = false;
0 ignored issues
show
introduced by
The private property $required_permission_codes is not used, and could be removed.
Loading history...
51
52
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
53
        'Logger' => '%$' . LoggerInterface::class . '.mfa',
54
    ];
55
56
    /**
57
     * @var LoggerInterface
58
     */
59
    protected $logger;
60
61
    /**
62
     * Start a registration for a method on the currently logged in user
63
     *
64
     * @param HTTPRequest $request
65
     * @return HTTPResponse
66
     */
67
    public function startRegistration(HTTPRequest $request): HTTPResponse
68
    {
69
        // Create a fresh store from the current logged in user
70
        $member = Security::getCurrentUser();
71
        $store = $this->createStore($member);
72
73
        if (!$this->getSudoModeService()->check($request->getSession())) {
74
            return $this->jsonResponse(
75
                ['errors' => [_t(__CLASS__ . '.INVALID_SESSION', 'Invalid session. Please refresh and try again.')]],
76
                400
77
            );
78
        }
79
80
        // Get the specified method
81
        $method = MethodRegistry::singleton()->getMethodByURLSegment($request->param('Method'));
82
83
        if (!$method) {
84
            return $this->jsonResponse(
85
                ['errors' => [_t(__CLASS__ . '.INVALID_METHOD', 'No such method is available')]],
86
                400
87
            );
88
        }
89
90
        $response = $this->createStartRegistrationResponse($store, $method, true);
91
        $store->save($request);
92
93
        return $response;
94
    }
95
96
    /**
97
     * Complete a registration for a method for the currently logged in user
98
     *
99
     * @param HTTPRequest $request
100
     * @return HTTPResponse
101
     */
102
    public function finishRegistration(HTTPRequest $request): HTTPResponse
103
    {
104
        $store = $this->getStore();
105
106
        if (!$store || !$this->getSudoModeService()->check($request->getSession())) {
0 ignored issues
show
introduced by
$store is of type SilverStripe\MFA\Store\StoreInterface, thus it always evaluated to true.
Loading history...
107
            return $this->jsonResponse(
108
                ['errors' => [_t(__CLASS__ . '.INVALID_SESSION', 'Invalid session. Please refresh and try again.')]],
109
                400
110
            );
111
        }
112
113
        $method = MethodRegistry::singleton()->getMethodByURLSegment($request->param('Method'));
114
115
        if (!$method) {
116
            return $this->jsonResponse(
117
                ['errors' => [_t(__CLASS__ . '.INVALID_METHOD', 'No such method is available')]],
118
                400
119
            );
120
        }
121
122
        $result = $this->completeRegistrationRequest($store, $method, $request);
123
124
        if (!$result->isSuccessful()) {
125
            return $this->jsonResponse(['errors' => [$result->getMessage()]], 400);
126
        }
127
128
        $store::clear($request);
129
130
        return $this->jsonResponse([
131
            'success' => true,
132
            'method' => $result->getContext()['registeredMethod'] ?? null,
133
        ], 201);
134
    }
135
136
    /**
137
     * Remove the specified method from the currently logged in user
138
     *
139
     * @param HTTPRequest $request
140
     * @return HTTPResponse
141
     */
142
    public function removeRegisteredMethod(HTTPRequest $request): HTTPResponse
143
    {
144
        // Ensure CSRF protection
145
        if (!SecurityToken::inst()->checkRequest($request)) {
146
            return $this->jsonResponse(
147
                ['errors' => [_t(__CLASS__ . '.CSRF_FAILURE', 'Request timed out, please try again')]],
148
                400
149
            );
150
        }
151
152
        // Get the specified method
153
        $methodRegistry = MethodRegistry::singleton();
154
        $specifiedMethod = $request->param('Method');
155
156
        if (!$specifiedMethod || !($method = $methodRegistry->getMethodByURLSegment($specifiedMethod))) {
157
            return $this->jsonResponse(
158
                ['errors' => [_t(__CLASS__ . '.INVALID_METHOD', 'No such method is available')]],
159
                400
160
            );
161
        }
162
163
        // Remove the method from the user
164
        $member = Security::getCurrentUser();
165
        $registeredMethodManager = RegisteredMethodManager::singleton();
166
        $result = $registeredMethodManager->deleteFromMember($member, $method);
167
168
        if (!$result) {
169
            return $this->jsonResponse(
170
                ['errors' => [_t(
171
                    __CLASS__ . '.COULD_NOT_DELETE',
172
                    'Could not delete the specified method from the user'
173
                )]],
174
                400
175
            );
176
        }
177
178
        $backupMethod = $methodRegistry->getBackupMethod();
179
        return $this->jsonResponse([
180
            'success' => true,
181
            'availableMethod' => Injector::inst()->create(AvailableMethodDetailsInterface::class, $method),
182
            // Indicate if the user has a backup method registered to keep the UI up to date
183
            // Deleting methods may remove the backup method if there are no other methods remaining.
184
            'hasBackupMethod' => $backupMethod && $registeredMethodManager->getFromMember(
185
                $member,
186
                $backupMethod
187
            ),
188
        ]);
189
    }
190
191
    /**
192
     * Set the default registered method for the current user to that provided by the MethodID parameter.
193
     *
194
     * @param HTTPRequest $request
195
     * @return HTTPResponse
196
     */
197
    public function setDefaultRegisteredMethod(HTTPRequest $request): HTTPResponse
198
    {
199
        // Ensure CSRF and sudo-mode protection
200
        if (
201
            !SecurityToken::inst()->checkRequest($request)
202
            || !$this->getSudoModeService()->check($request->getSession())
203
        ) {
204
            return $this->jsonResponse(
205
                ['errors' => [_t(__CLASS__ . '.CSRF_FAILURE', 'Request timed out, please try again')]],
206
                400
207
            );
208
        }
209
210
        // Get the specified method
211
        $methodRegistry = MethodRegistry::singleton();
212
        $specifiedMethod = $request->param('Method');
213
214
        if (!$specifiedMethod || !($method = $methodRegistry->getMethodByURLSegment($specifiedMethod))) {
215
            return $this->jsonResponse(
216
                ['errors' => [_t(__CLASS__ . '.INVALID_METHOD', 'No such method is available')]],
217
                400
218
            );
219
        }
220
221
        // Set the method as the default
222
        /** @var Member&MemberExtension $member */
223
        $member = Security::getCurrentUser();
224
        $registeredMethodManager = RegisteredMethodManager::singleton();
225
        $registeredMethod = $registeredMethodManager->getFromMember($member, $method);
226
        if (!$registeredMethod) {
227
            return $this->jsonResponse(
228
                ['errors' => [_t(__CLASS__ . '.INVALID_REGISTERED_METHOD', 'No such registered method is available')]],
229
                400
230
            );
231
        }
232
        try {
233
            $member->setDefaultRegisteredMethod($registeredMethod);
234
            $member->write();
235
        } catch (ValidationException $exception) {
236
            $this->logger->debug(
237
                'Failed to set default registered method for user #' . $member->ID . ' to ' . $specifiedMethod
238
                . ': ' . $exception->getMessage()
239
            );
240
241
            return $this->jsonResponse(
242
                ['errors' => [_t(
243
                    __CLASS__ . '.COULD_NOT_SET_DEFAULT',
244
                    'Could not set the default method for the user'
245
                )]],
246
                400
247
            );
248
        }
249
250
        return $this->jsonResponse(['success' => true]);
251
    }
252
253
    /**
254
     * Respond with the given array as a JSON response
255
     *
256
     * @param array $response
257
     * @param int $code The HTTP response code to set on the response
258
     * @return HTTPResponse
259
     */
260
    protected function jsonResponse(array $response, int $code = 200): HTTPResponse
261
    {
262
        return HTTPResponse::create(json_encode($response))
263
            ->addHeader('Content-Type', 'application/json')
264
            ->setStatusCode($code);
265
    }
266
267
    /**
268
     * @param LoggerInterface|null $logger
269
     * @return $this
270
     */
271
    public function setLogger(?LoggerInterface $logger): self
272
    {
273
        $this->logger = $logger;
274
        return $this;
275
    }
276
}
277