Passed
Push — master ( 2dd807...470cd2 )
by Jan
04:52
created

UserSettingsController   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 151
c 2
b 0
f 0
dl 0
loc 293
rs 9.76
wmc 33

5 Methods

Rating   Name   Duplication   Size   Complexity  
F userSettings() 0 163 18
B removeU2FToken() 0 47 7
A __construct() 0 4 1
A resetTrustedDevices() 0 27 4
A showBackupCodes() 0 19 3
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
/**
24
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25
 *
26
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software; you can redistribute it and/or
29
 * modify it under the terms of the GNU General Public License
30
 * as published by the Free Software Foundation; either version 2
31
 * of the License, or (at your option) any later version.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 * GNU General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU General Public License
39
 * along with this program; if not, write to the Free Software
40
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
41
 */
42
43
namespace App\Controller;
44
45
use App\Entity\UserSystem\U2FKey;
46
use App\Entity\UserSystem\User;
47
use App\Events\SecurityEvent;
48
use App\Events\SecurityEvents;
49
use App\Form\TFAGoogleSettingsType;
50
use App\Form\UserSettingsType;
51
use App\Services\TFA\BackupCodeManager;
52
use Doctrine\ORM\EntityManagerInterface;
53
use RuntimeException;
54
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
55
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
56
use Symfony\Component\EventDispatcher\EventDispatcher;
57
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
58
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
59
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
60
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
61
use Symfony\Component\Form\Extension\Core\Type\TextType;
62
use Symfony\Component\HttpFoundation\RedirectResponse;
63
use Symfony\Component\HttpFoundation\Request;
64
use Symfony\Component\Routing\Annotation\Route;
65
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
66
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
67
use Symfony\Component\Validator\Constraints\Length;
68
69
/**
70
 * @Route("/user")
71
 */
72
class UserSettingsController extends AbstractController
73
{
74
    protected $demo_mode;
75
    /** @var EventDispatcher */
76
    protected $eventDispatcher;
77
78
    public function __construct(bool $demo_mode, EventDispatcherInterface $eventDispatcher)
79
    {
80
        $this->demo_mode = $demo_mode;
81
        $this->eventDispatcher = $eventDispatcher;
0 ignored issues
show
Documentation Bug introduced by
$eventDispatcher is of type Symfony\Component\EventD...ventDispatcherInterface, but the property $eventDispatcher was declared to be of type Symfony\Component\EventDispatcher\EventDispatcher. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
82
    }
83
84
    /**
85
     * @Route("/2fa_backup_codes", name="show_backup_codes")
86
     */
87
    public function showBackupCodes()
88
    {
89
        $user = $this->getUser();
90
91
        //When user change its settings, he should be logged  in fully.
92
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
93
94
        if (! $user instanceof User) {
95
            return new RuntimeException('This controller only works only for Part-DB User objects!');
96
        }
97
98
        if (empty($user->getBackupCodes())) {
99
            $this->addFlash('error', 'tfa_backup.no_codes_enabled');
100
101
            throw new RuntimeException('You do not have any backup codes enabled, therefore you can not view them!');
102
        }
103
104
        return $this->render('Users/backup_codes.html.twig', [
105
            'user' => $user,
106
        ]);
107
    }
108
109
    /**
110
     * @Route("/u2f_delete", name="u2f_delete", methods={"DELETE"})
111
     *
112
     * @return RedirectResponse
113
     */
114
    public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager): RedirectResponse
115
    {
116
        if ($this->demo_mode) {
117
            throw new RuntimeException('You can not do 2FA things in demo mode');
118
        }
119
120
        $user = $this->getUser();
121
122
        //When user change its settings, he should be logged  in fully.
123
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
124
125
        if (! $user instanceof User) {
126
            throw new RuntimeException('This controller only works only for Part-DB User objects!');
127
        }
128
129
        if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
130
            if ($request->request->has('key_id')) {
131
                $key_id = $request->request->get('key_id');
132
                $key_repo = $entityManager->getRepository(U2FKey::class);
133
                /** @var U2FKey|null $u2f */
134
                $u2f = $key_repo->find($key_id);
135
                if (null === $u2f) {
136
                    $this->addFlash('danger', 'tfa_u2f.u2f_delete.not_existing');
137
138
                    throw new RuntimeException('Key not existing!');
139
                }
140
141
                //User can only delete its own U2F keys
142
                if ($u2f->getUser() !== $user) {
143
                    $this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
144
145
                    throw new RuntimeException('You can only delete your own U2F keys!');
146
                }
147
148
                $backupCodeManager->disableBackupCodesIfUnused($user);
149
                $entityManager->remove($u2f);
150
                $entityManager->flush();
151
                $this->addFlash('success', 'tfa.u2f.u2f_delete.success');
152
153
                $security_event = new SecurityEvent($user);
154
                $this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
155
            }
156
        } else {
157
            $this->addFlash('error', 'csfr_invalid');
158
        }
159
160
        return $this->redirectToRoute('user_settings');
161
    }
162
163
    /**
164
     * @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"})
165
     *
166
     * @return RuntimeException|RedirectResponse
167
     */
168
    public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager)
169
    {
170
        if ($this->demo_mode) {
171
            throw new RuntimeException('You can not do 2FA things in demo mode');
172
        }
173
174
        $user = $this->getUser();
175
176
        //When user change its settings, he should be logged  in fully.
177
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
178
179
        if (! $user instanceof User) {
180
            return new RuntimeException('This controller only works only for Part-DB User objects!');
181
        }
182
183
        if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
184
            $user->invalidateTrustedDeviceTokens();
185
            $entityManager->flush();
186
            $this->addFlash('success', 'tfa_trustedDevice.invalidate.success');
187
188
            $security_event = new SecurityEvent($user);
189
            $this->eventDispatcher->dispatch($security_event, SecurityEvents::TRUSTED_DEVICE_RESET);
190
        } else {
191
            $this->addFlash('error', 'csfr_invalid');
192
        }
193
194
        return $this->redirectToRoute('user_settings');
195
    }
196
197
    /**
198
     * @Route("/settings", name="user_settings")
199
     *
200
     * @return RedirectResponse|\Symfony\Component\HttpFoundation\Response
201
     */
202
    public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager)
203
    {
204
        /** @var User */
205
        $user = $this->getUser();
206
207
        $page_need_reload = false;
208
209
        //When user change its settings, he should be logged  in fully.
210
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
211
212
        if (! $user instanceof User) {
213
            throw new RuntimeException('This controller only works only for Part-DB User objects!');
214
        }
215
216
        $security_event = new SecurityEvent($user);
217
218
        /***************************
219
         * User settings form
220
         ***************************/
221
222
        $form = $this->createForm(UserSettingsType::class, $user);
223
224
        $form->handleRequest($request);
225
226
        if (! $this->demo_mode && $form->isSubmitted() && $form->isValid()) {
227
            //Check if user theme setting has changed
228
            if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) {
229
                $page_need_reload = true;
230
            }
231
232
            $em->flush();
233
            $this->addFlash('success', 'user.settings.saved_flash');
234
        }
235
236
        /*****************************
237
         * Password change form
238
         ****************************/
239
240
        $pw_form = $this->createFormBuilder()
241
            //Username field for autocomplete
242
            ->add('username', TextType::class, [
243
                'data' => $user->getName(),
244
                'attr' => [
245
                    'autocomplete' => 'username',
246
                ],
247
                'disabled' => true,
248
                'row_attr' => [
249
                    'class' => 'd-none',
250
                ],
251
            ])
252
            ->add('old_password', PasswordType::class, [
253
                'label' => 'user.settings.pw_old.label',
254
                'disabled' => $this->demo_mode,
255
                'attr' => [
256
                    'autocomplete' => 'current-password',
257
                ],
258
                'constraints' => [new UserPassword()],
259
            ]) //This constraint checks, if the current user pw was inputted.
260
            ->add('new_password', RepeatedType::class, [
261
                'disabled' => $this->demo_mode,
262
                'type' => PasswordType::class,
263
                'first_options' => [
264
                    'label' => 'user.settings.pw_new.label',
265
                ],
266
                'second_options' => [
267
                    'label' => 'user.settings.pw_confirm.label',
268
                ],
269
                'invalid_message' => 'password_must_match',
270
                'options' => [
271
                    'attr' => [
272
                        'autocomplete' => 'new-password',
273
                    ],
274
                ],
275
                'constraints' => [new Length([
276
                    'min' => 6,
277
                    'max' => 128,
278
                ])],
279
            ])
280
            ->add('submit', SubmitType::class, ['label' => 'save'])
281
            ->getForm();
282
283
        $pw_form->handleRequest($request);
284
285
        //Check if password if everything was correct, then save it to User and DB
286
        if (! $this->demo_mode && $pw_form->isSubmitted() && $pw_form->isValid()) {
287
            $password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData());
288
            $user->setPassword($password);
289
290
            //After the change reset the password change needed setting
291
            $user->setNeedPwChange(false);
292
293
            $em->persist($user);
294
            $em->flush();
295
            $this->addFlash('success', 'user.settings.pw_changed_flash');
296
            $this->eventDispatcher->dispatch($security_event, SecurityEvents::PASSWORD_CHANGED);
297
        }
298
299
        //Handle 2FA things
300
        $google_form = $this->createForm(TFAGoogleSettingsType::class, $user);
301
        $google_enabled = $user->isGoogleAuthenticatorEnabled();
302
        if (! $google_enabled && ! $form->isSubmitted()) {
303
            $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
304
            $google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret());
305
        }
306
        $google_form->handleRequest($request);
307
308
        if (! $this->demo_mode && $google_form->isSubmitted() && $google_form->isValid()) {
309
            if (! $google_enabled) {
310
                //Save 2FA settings (save secrets)
311
                $user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
312
                $backupCodeManager->enableBackupCodes($user);
313
314
                $em->flush();
315
                $this->addFlash('success', 'user.settings.2fa.google.activated');
316
317
                $this->eventDispatcher->dispatch($security_event, SecurityEvents::GOOGLE_ENABLED);
318
319
                return $this->redirectToRoute('user_settings');
320
            }
321
322
            //Remove secret to disable google authenticator
323
            $user->setGoogleAuthenticatorSecret(null);
324
            $backupCodeManager->disableBackupCodesIfUnused($user);
325
            $em->flush();
326
            $this->addFlash('success', 'user.settings.2fa.google.disabled');
327
            $this->eventDispatcher->dispatch($security_event, SecurityEvents::GOOGLE_DISABLED);
328
329
            return $this->redirectToRoute('user_settings');
330
        }
331
332
        $backup_form = $this->get('form.factory')->createNamedBuilder('backup_codes')->add('reset_codes', SubmitType::class, [
333
            'label' => 'tfa_backup.regenerate_codes',
334
            'attr' => [
335
                'class' => 'btn-danger',
336
            ],
337
            'disabled' => empty($user->getBackupCodes()),
338
        ])->getForm();
339
340
        $backup_form->handleRequest($request);
341
        if (! $this->demo_mode && $backup_form->isSubmitted() && $backup_form->isValid()) {
342
            $backupCodeManager->regenerateBackupCodes($user);
343
            $em->flush();
344
            $this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated');
345
            $this->eventDispatcher->dispatch($security_event, SecurityEvents::BACKUP_KEYS_RESET);
346
        }
347
348
        /******************************
349
         * Output both forms
350
         *****************************/
351
352
        return $this->render('Users/user_settings.html.twig', [
353
            'user' => $user,
354
            'settings_form' => $form->createView(),
355
            'pw_form' => $pw_form->createView(),
356
            'page_need_reload' => $page_need_reload,
357
358
            'google_form' => $google_form->createView(),
359
            'backup_form' => $backup_form->createView(),
360
            'tfa_google' => [
361
                'enabled' => $google_enabled,
362
                'qrContent' => $googleAuthenticator->getQRContent($user),
363
                'secret' => $user->getGoogleAuthenticatorSecret(),
364
                'username' => $user->getGoogleAuthenticatorUsername(),
365
            ],
366
        ]);
367
    }
368
}
369