Passed
Push — master ( d5a28b...d12ca0 )
by
unknown
17:01
created

getRecommendedProviderIdentifier()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 21
rs 9.2222
cc 6
nc 5
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Http\Message\UriInterface;
23
use TYPO3\CMS\Backend\Routing\UriBuilder;
24
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
25
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
26
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
27
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
28
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
29
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
30
use TYPO3\CMS\Core\Http\HtmlResponse;
31
use TYPO3\CMS\Core\Http\RedirectResponse;
32
use TYPO3\CMS\Core\Imaging\Icon;
33
use TYPO3\CMS\Core\Imaging\IconFactory;
34
use TYPO3\CMS\Core\Messaging\FlashMessage;
35
use TYPO3\CMS\Core\Messaging\FlashMessageService;
36
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
37
use TYPO3\CMS\Core\Utility\GeneralUtility;
38
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
39
use TYPO3\CMS\Fluid\View\StandaloneView;
40
41
/**
42
 * Controller to configure MFA providers in the backend
43
 *
44
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
45
 */
46
class MfaConfigurationController extends AbstractMfaController
47
{
48
    protected array $allowedActions = ['overview', 'setup', 'activate', 'deactivate', 'unlock', 'edit', 'save'];
49
    private array $providerActionsWhenInactive = ['setup', 'activate'];
50
    private array $providerActionsWhenActive = ['deactivate', 'unlock', 'edit', 'save'];
51
52
    protected IconFactory $iconFactory;
53
54
    public function __construct(
55
        IconFactory $iconFactory,
56
        UriBuilder $uriBuilder,
57
        MfaProviderRegistry $mfaProviderRegistry,
58
        ModuleTemplateFactory $moduleTemplateFactory
59
    ) {
60
        $this->iconFactory = $iconFactory;
61
        parent::__construct($uriBuilder, $mfaProviderRegistry, $moduleTemplateFactory);
62
    }
63
64
    /**
65
     * Main entry point, checking prerequisite, initializing and setting
66
     * up the view and finally dispatching to the requested action.
67
     */
68
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
69
    {
70
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
71
        $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview');
72
73
        if (!$this->isActionAllowed($action)) {
74
            return new HtmlResponse('Action not allowed', 400);
75
        }
76
77
        $mfaProvider = null;
78
        $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? '');
79
        // Check if given identifier is valid
80
        if ($this->isValidIdentifier($identifier)) {
81
            $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier);
82
        }
83
        // All actions expect "overview" require a provider to deal with.
84
        // If non is found at this point, initiate a redirect to the overview.
85
        if ($mfaProvider === null && $action !== 'overview') {
86
            $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotFound'), '', FlashMessage::ERROR);
87
            return new RedirectResponse($this->getActionUri('overview'));
88
        }
89
        // If a valid provider is given, check if the requested action can be performed on this provider
90
        if ($mfaProvider !== null) {
91
            $isProviderActive = $mfaProvider->isActive(
92
                MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser())
93
            );
94
            // Some actions require the provider to be inactive
95
            if ($isProviderActive && in_array($action, $this->providerActionsWhenInactive, true)) {
96
                $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerActive'), '', FlashMessage::ERROR);
97
                return new RedirectResponse($this->getActionUri('overview'));
98
            }
99
            // Some actions require the provider to be active
100
            if (!$isProviderActive && in_array($action, $this->providerActionsWhenActive, true)) {
101
                $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotActive'), '', FlashMessage::ERROR);
102
                return new RedirectResponse($this->getActionUri('overview'));
103
            }
104
        }
105
106
        switch ($action) {
107
            case 'overview':
108
                return $this->overviewAction($request, $this->initializeView($action));
109
            case 'setup':
110
            case 'edit':
111
                return $this->{$action . 'Action'}($request, $mfaProvider, $this->initializeView($action));
112
            case 'activate':
113
            case 'deactivate':
114
            case 'unlock':
115
            case 'save':
116
                return $this->{$action . 'Action'}($request, $mfaProvider);
117
            default:
118
                return new HtmlResponse('Action not allowed', 400);
119
        }
120
    }
121
122
    /**
123
     * Setup the overview with all available MFA providers
124
     */
125
    public function overviewAction(ServerRequestInterface $request, ViewInterface $view): ResponseInterface
126
    {
127
        $this->addOverviewButtons($request);
128
        $view->assignMultiple([
129
            'providers' => $this->allowedProviders,
130
            'defaultProvider' => $this->getDefaultProviderIdentifier(),
131
            'recommendedProvider' => $this->getRecommendedProviderIdentifier(),
132
            'setupRequired' => $this->mfaRequired && !$this->mfaProviderRegistry->hasActiveProviders($this->getBackendUser())
133
        ]);
134
        $this->moduleTemplate->setContent($view->render());
0 ignored issues
show
Bug introduced by
The method setContent() does not exist on null. ( Ignorable by Annotation )

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

134
        $this->moduleTemplate->/** @scrutinizer ignore-call */ 
135
                               setContent($view->render());

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...
135
        return new HtmlResponse($this->moduleTemplate->renderContent());
136
    }
137
138
    /**
139
     * Render form to setup a provider by using provider specific content
140
     */
141
    public function setupAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider, ViewInterface $view): ResponseInterface
142
    {
143
        $this->addFormButtons();
144
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
145
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP);
146
        $view->assignMultiple([
147
            'provider' => $mfaProvider,
148
            'providerContent' => $providerResponse->getBody()
149
        ]);
150
        $this->moduleTemplate->setContent($view->render());
151
        return new HtmlResponse($this->moduleTemplate->renderContent());
152
    }
153
154
    /**
155
     * Handle activate request, receiving from the setup view
156
     * by forwarding the request to the appropriate provider.
157
     * Furthermore, add the provider as default provider in case
158
     * it is the recommended provider for this user, or no default
159
     * provider is yet defined the newly activated provider is allowed
160
     * to be a default provider and there are no other providers which
161
     * would suite as default provider.
162
     */
163
    public function activateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
164
    {
165
        $backendUser = $this->getBackendUser();
166
        $isRecommendedProvider = $this->getRecommendedProviderIdentifier() === $mfaProvider->getIdentifier();
167
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser);
168
        $languageService = $this->getLanguageService();
169
        if (!$mfaProvider->activate($request, $propertyManager)) {
170
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
171
            return new RedirectResponse($this->getActionUri('setup', ['identifier' => $mfaProvider->getIdentifier()]));
172
        }
173
        if ($isRecommendedProvider
174
            || (
175
                $this->getDefaultProviderIdentifier() === ''
176
                && $mfaProvider->isDefaultProviderAllowed()
177
                && !$this->hasSuitableDefaultProviders([$mfaProvider->getIdentifier()])
178
            )
179
        ) {
180
            $this->setDefaultProvider($mfaProvider);
181
        }
182
        // If this is the first activated provider, the user has logged in without being required
183
        // to pass the MFA challenge. Therefore no session entry exists. To prevent the challenge
184
        // from showing up after the activation we need to set the session data here.
185
        if (!(bool)($backendUser->getSessionData('mfa') ?? false)) {
186
            $backendUser->setSessionData('mfa', true);
187
        }
188
        $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
189
        return new RedirectResponse($this->getActionUri('overview'));
190
    }
191
192
    /**
193
     * Handle deactivate request by forwarding the request to the
194
     * appropriate provider. Also remove the provider as default
195
     * provider from user UC, if set.
196
     */
197
    public function deactivateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
198
    {
199
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
200
        $languageService = $this->getLanguageService();
201
        if (!$mfaProvider->deactivate($request, $propertyManager)) {
202
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
203
        } else {
204
            if ($this->isDefaultProvider($mfaProvider)) {
205
                $this->removeDefaultProvider();
206
            }
207
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
208
        }
209
        return new RedirectResponse($this->getActionUri('overview'));
210
    }
211
212
    /**
213
     * Handle unlock request by forwarding the request to the appropriate provider
214
     */
215
    public function unlockAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
216
    {
217
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
218
        $languageService = $this->getLanguageService();
219
        if (!$mfaProvider->unlock($request, $propertyManager)) {
220
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
221
        } else {
222
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
223
        }
224
        return new RedirectResponse($this->getActionUri('overview'));
225
    }
226
227
    /**
228
     * Render form to edit a provider by using provider specific content
229
     */
230
    public function editAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider, ViewInterface $view): ResponseInterface
231
    {
232
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
233
        if ($mfaProvider->isLocked($propertyManager)) {
234
            // Do not show edit view for locked providers
235
            $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerIsLocked'), '', FlashMessage::ERROR);
236
            return new RedirectResponse($this->getActionUri('overview'));
237
        }
238
        $this->addFormButtons();
239
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::EDIT);
240
        $view->assignMultiple([
241
            'provider' => $mfaProvider,
242
            'providerContent' => $providerResponse->getBody(),
243
            'isDefaultProvider' => $this->isDefaultProvider($mfaProvider)
244
        ]);
245
        $this->moduleTemplate->setContent($view->render());
246
        return new HtmlResponse($this->moduleTemplate->renderContent());
247
    }
248
249
    /**
250
     * Handle save request, receiving from the edit view by
251
     * forwarding the request to the appropriate provider.
252
     */
253
    public function saveAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
254
    {
255
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
256
        $languageService = $this->getLanguageService();
257
        if (!$mfaProvider->update($request, $propertyManager)) {
258
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
259
        } else {
260
            if ((bool)($request->getParsedBody()['defaultProvider'] ?? false)) {
261
                $this->setDefaultProvider($mfaProvider);
262
            } elseif ($this->isDefaultProvider($mfaProvider)) {
263
                $this->removeDefaultProvider();
264
            }
265
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
266
        }
267
        if (!$mfaProvider->isActive($propertyManager)) {
268
            return new RedirectResponse($this->getActionUri('overview'));
269
        }
270
        return new RedirectResponse($this->getActionUri('edit', ['identifier' => $mfaProvider->getIdentifier()]));
271
    }
272
273
    /**
274
     * Initialize the standalone view and set the template name
275
     */
276
    protected function initializeView(string $templateName): ViewInterface
277
    {
278
        $view = GeneralUtility::makeInstance(StandaloneView::class);
279
        $view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/Mfa']);
280
        $view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
281
        $view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
282
        $view->setTemplate($templateName);
283
        return $view;
284
    }
285
286
    /**
287
     * Build a uri for the current controller based on the
288
     * given action, respecting additional parameters.
289
     */
290
    protected function getActionUri(string $action, array $additionalParameters = []): UriInterface
291
    {
292
        if (!$this->isActionAllowed($action)) {
293
            $action = 'overview';
294
        }
295
        return $this->uriBuilder->buildUriFromRoute('mfa', array_merge(['action' => $action], $additionalParameters));
296
    }
297
298
    /**
299
     * Check if there are more suitable default providers for the current user
300
     */
301
    protected function hasSuitableDefaultProviders(array $excludedProviders = []): bool
302
    {
303
        foreach ($this->allowedProviders as $identifier => $provider) {
304
            if (!in_array($identifier, $excludedProviders, true)
305
                && $provider->isDefaultProviderAllowed()
306
                && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()))
307
            ) {
308
                return true;
309
            }
310
        }
311
        return false;
312
    }
313
314
    /**
315
     * Get the default provider
316
     */
317
    protected function getDefaultProviderIdentifier(): string
318
    {
319
        $defaultProviderIdentifier = (string)($this->getBackendUser()->uc['mfa']['defaultProvider'] ?? '');
320
        // The default provider value is only valid, if the corresponding provider exist and is allowed
321
        if ($this->isValidIdentifier($defaultProviderIdentifier)) {
322
            $defaultProvider = $this->mfaProviderRegistry->getProvider($defaultProviderIdentifier);
323
            $propertyManager = MfaProviderPropertyManager::create($defaultProvider, $this->getBackendUser());
324
            // Also check if the provider is activated for the user
325
            if ($defaultProvider->isActive($propertyManager)) {
326
                return $defaultProviderIdentifier;
327
            }
328
        }
329
330
        // If the stored provider is not valid, clean up the UC
331
        $this->removeDefaultProvider();
332
        return '';
333
    }
334
335
    /**
336
     * Get the recommended provider
337
     */
338
    protected function getRecommendedProviderIdentifier(): string
339
    {
340
        $recommendedProviderIdentifier = (string)($this->mfaTsConfig['recommendedProvider'] ?? '');
341
        // Check if valid and allowed to be default provider, which is obviously a prerequisite
342
        if (!$this->isValidIdentifier($recommendedProviderIdentifier)
343
            || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed()
344
        ) {
345
            // If the provider, defined in user TSconfig is not valid or is not set, check the globally defined
346
            $recommendedProviderIdentifier = (string)($GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider'] ?? '');
347
            if (!$this->isValidIdentifier($recommendedProviderIdentifier)
348
                || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed()
349
            ) {
350
                // If also not valid or not set, return
351
                return '';
352
            }
353
        }
354
355
        $provider = $this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier);
356
        $propertyManager = MfaProviderPropertyManager::create($provider, $this->getBackendUser());
357
        // If the defined recommended provider is valid, check if it is not yet activated
358
        return !$provider->isActive($propertyManager) ? $recommendedProviderIdentifier : '';
359
    }
360
361
    protected function isDefaultProvider(MfaProviderManifestInterface $mfaProvider): bool
362
    {
363
        return $this->getDefaultProviderIdentifier() === $mfaProvider->getIdentifier();
364
    }
365
366
    protected function setDefaultProvider(MfaProviderManifestInterface $mfaProvider): void
367
    {
368
        $this->getBackendUser()->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier();
369
        $this->getBackendUser()->writeUC();
370
    }
371
372
    protected function removeDefaultProvider(): void
373
    {
374
        $this->getBackendUser()->uc['mfa']['defaultProvider'] = '';
375
        $this->getBackendUser()->writeUC();
376
    }
377
378
    protected function addFlashMessage(string $message, string $title = '', int $severity = FlashMessage::INFO): void
379
    {
380
        $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true);
381
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
382
        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
383
        $defaultFlashMessageQueue->enqueue($flashMessage);
384
    }
385
386
    protected function addOverviewButtons(ServerRequestInterface $request): void
387
    {
388
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
389
390
        if (($returnUrl = $this->getReturnUrl($request)) !== '') {
391
            $button = $buttonBar
392
                ->makeLinkButton()
393
                ->setHref($returnUrl)
394
                ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL))
395
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
396
                ->setShowLabelText(true);
397
            $buttonBar->addButton($button);
398
        }
399
400
        $reloadButton = $buttonBar
401
            ->makeLinkButton()
402
            ->setHref($request->getAttribute('normalizedParams')->getRequestUri())
403
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
404
            ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
405
        $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
406
    }
407
408
    protected function addFormButtons(): void
409
    {
410
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
411
        $lang = $this->getLanguageService();
412
413
        $closeButton = $buttonBar
414
            ->makeLinkButton()
415
            ->setHref($this->uriBuilder->buildUriFromRoute('mfa', ['action' => 'overview']))
416
            ->setClasses('t3js-editform-close')
417
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
418
            ->setShowLabelText(true)
419
            ->setIcon($this->iconFactory->getIcon('actions-close', Icon::SIZE_SMALL));
420
        $buttonBar->addButton($closeButton);
421
422
        $saveButton = $buttonBar
423
            ->makeInputButton()
424
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
425
            ->setName('save')
426
            ->setValue('1')
427
            ->setShowLabelText(true)
428
            ->setForm('mfaConfigurationController')
429
            ->setIcon($this->iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL));
430
        $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
431
    }
432
433
    protected function getReturnUrl(ServerRequestInterface $request): string
434
    {
435
        $returnUrl = GeneralUtility::sanitizeLocalUrl(
436
            $request->getQueryParams()['returnUrl'] ?? $request->getParsedBody()['returnUrl'] ?? ''
437
        );
438
439
        if ($returnUrl === '' && ExtensionManagementUtility::isLoaded('setup')) {
440
            $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('user_setup');
441
        }
442
443
        return $returnUrl;
444
    }
445
}
446