UserContext.php 15.7 KB
Newer Older
1
2
3
4
<?php

namespace UnicaenAuth\Service;

5
use Doctrine\ORM\ORMException;
6
use UnicaenApp\Exception\RuntimeException;
7
use UnicaenApp\Traits\SessionContainerTrait;
8
use UnicaenAuth\Acl\NamedRole;
9
10
11
12
use UnicaenAuth\Authentication\SessionIdentity;
use UnicaenAuth\Authentication\Storage\Auth;
use UnicaenAuth\Authentication\Storage\Usurpation;
use UnicaenAuth\Entity\Db\AbstractUser;
13
14
use UnicaenAuth\Entity\Db\AbstractRole;
use UnicaenAuth\Entity\Db\Role;
15
use UnicaenAuth\Entity\Ldap\People;
16
use UnicaenAuth\Entity\Shibboleth\ShibUser;
17
use UnicaenAuth\Event\UserRoleSelectedEvent;
18
use UnicaenAuth\Formatter\RoleFormatter;
19
use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait;
20
use UnicaenAuth\Provider\Identity\Chain;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
21
22
23
24
use Laminas\Authentication\AuthenticationService;
use Laminas\EventManager\EventManagerAwareInterface;
use Laminas\EventManager\EventManagerAwareTrait;
use Laminas\Permissions\Acl\Role\RoleInterface;
25
26
27
use ZfcUser\Entity\UserInterface;

/**
28
 * Service centralisant des méthodes utiles concernant l'utilisateur authentifié.
29
 *
30
 * @author Unicaen
31
 */
32
class UserContext extends AbstractService implements EventManagerAwareInterface
33
{
34
    use EventManagerAwareTrait;
35
    use SessionContainerTrait;
36
    use ModuleOptionsAwareTrait;
37
38
39
40
41

    /**
     * @var mixed
     */
    protected $identity;
42

43
    /**
44
     * @var array
45
     */
46
    protected $identityRoles;
47

48
49
50
51
52
53
54
55
56
57
58
59
60
    /**
     * @var AuthenticationService
     */
    protected $authenticationService;

    /**
     * @param AuthenticationService $authenticationService
     */
    public function setAuthenticationService(AuthenticationService $authenticationService)
    {
        $this->authenticationService = $authenticationService;
    }

61
62
63
64
65
66
67
68
69
70
71
72
    /**
     * Retourne l'utilisateur BDD courant
     *
     * @return UserInterface
     */
    public function getDbUser()
    {
        if (($identity = $this->getIdentity())) {
            if (isset($identity['db']) && $identity['db'] instanceof UserInterface) {
                return $identity['db'];
            }
        }
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        return null;
    }

    /**
     * Retourne l'utilisateur LDAP courant
     *
     * @return People
     */
    public function getLdapUser()
    {
        if (($identity = $this->getIdentity())) {
            if (isset($identity['ldap']) && $identity['ldap'] instanceof People) {
                return $identity['ldap'];
            }
        }
89

90
91
92
        return null;
    }

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    /**
     * Retourne l'éventuel utilisateur Shibboleth courant.
     *
     * @return ShibUser|null
     */
    public function getShibUser()
    {
        if (($identity = $this->getIdentity())) {
            if (isset($identity['shib']) && $identity['shib'] instanceof ShibUser) {
                return $identity['shib'];
            }
        }

        return null;
    }

109
    /**
110
     * Retourne les données d'identité correspondant à l'utilisateur courant.
111
     *
112
     * @return mixed
113
     */
114
    public function getIdentity()
115
    {
116
        if (null === $this->identity) {
117
118
            if ($this->authenticationService->hasIdentity()) {
                $this->identity = $this->authenticationService->getIdentity();
119
120
            }
        }
121

122
123
        return $this->identity;
    }
124

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
    /**
     * Retourne l'identifiant de connexion de l'utilisateur courant.
     *
     * @return string|null
     */
    public function getIdentityUsername()
    {
        if ($user = $this->getShibUser()) {
            return $user->getUsername();
        }
        if ($user = $this->getLdapUser()) {
            return $user->getUsername();
        }
        if ($user = $this->getDbUser()) {
            return $user->getUsername();
        }

        return null;
    }

145
146
147
148
149
150
151
152
153
154
155
156
    /**
     * Retourne le type de l'authentification effectuée.
     *
     * @return string
     */
    public function getAuthenticationType(): string
    {
        $identityArray = $this->getIdentity();

        return Auth::extractTypeFromIdentityArray($identityArray);
    }

Laurent Lécluse's avatar
Laurent Lécluse committed
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    /**
     * @param string $roleId
     *
     * @return Role
     */
    public function getIdentityRole($roleId)
    {
        $roles = $this->getServiceAuthorize()->getRoles();
        if (isset($roles[$roleId])) {
            return $roles[$roleId];
        }

        return null;
    }

172
    /**
173
     * Retourne tous les rôles de l'utilisateur courant, pas seulement le rôle courant sélectionné.
174
     *
175
176
177
     * Les clés du tableau sont les ID de rôles, les valeurs sont les objets Role
     *
     * @return Role[]
178
179
180
181
     */
    public function getIdentityRoles()
    {
        if (null === $this->identityRoles) {
182
183
184
            $this->identityRoles = [];

            $roles            = $this->getServiceAuthorize()->getRoles();
185
186
            $identityProvider = $this->getIdentityProvider();
            if ($identityProvider instanceof Chain) {
187
                $identityRoles = $identityProvider->getAllIdentityRoles();
188
            } else {
189
                $identityRoles = $identityProvider->getIdentityRoles();
190
            }
191
192
            foreach ($identityRoles as $role) {
                if ($role instanceof RoleInterface) {
193
                    $this->identityRoles[$role->getRoleId()] = $role;
194
195
196
                } elseif (is_string($role) && isset($roles[$role])) {
                    $role = $roles[$role];
                    /** @var RoleInterface $role */
197
198
                    $this->identityRoles[$role->getRoleId()] = $role;
                }
199
            }
200
        }
201

202
203
        return $this->identityRoles;
    }
204

205
206
207
208
209
210
211
212


    public function clearIdentityRoles()
    {
        $this->identityRoles = null;
    }


213
214
215
216
217
218
219
220
221
222
    /**
     * Retourne tous les rôles de l'utilisateur courant au format littéral.
     *
     * @return array
     * @see getIdentityRoles()
     */
    public function getIdentityRolesToString()
    {
        $f = new RoleFormatter();
        $rolesToStrings = [];
223

224
225
226
227
228
229
        foreach ($this->getIdentityRoles() as $identityRole) {
            $rolesToStrings[$identityRole->getRoleId()] = $f->format($identityRole);
        }

        return $rolesToStrings;
    }
230

231
    /**
232
     * Retourne parmi tous les rôles de l'utilisateur courant ceux qui peuvent être sélectionnés.
233
     *
234
235
     * NB: si plus d'un rôle sont sélectionnables, on zappe le rôle "Authentifié".
     *
236
237
238
239
     * @return array
     */
    public function getSelectableIdentityRoles()
    {
240
241
242
        $filter = function ($r) {
            return !($r instanceof NamedRole && !$r->getSelectable());
        };
Bertrand Gauthier's avatar
Bertrand Gauthier committed
243
        $roles  = array_filter($this->getIdentityRoles(), $filter);
244

245
246
247
248
249
        // si plus d'un rôle sont sélectionnables, on zappe le rôle "Authentifié"
        if (count($roles) > 1 && isset($roles['user'])) {
            unset($roles['user']);
        }

Bertrand Gauthier's avatar
Bertrand Gauthier committed
250
        return $roles;
251
    }
252

253
    /**
254
     * Si un utilisateur est authentifié, retourne le rôle utilisateur sélectionné,
255
     * ou alors le premier sélectionnable si aucun n'a été sélectionné.
256
     *
257
     * @return RoleInterface|null
258
259
260
     */
    public function getSelectedIdentityRole()
    {
261
262
263
        // Si aucun utilisateur n'est authentifié, basta !
        if (! $this->getIdentity()) {
            return null;
264
265
        }

266
267
268
269
        // NB: Si un rôle est spécifié en session comme devant être le prochain rôle sélectionné,
        // c'est lui qui est pris en compte.
        if ($next = $this->getNextSelectedIdentityRole()) {
            $this->getSessionContainer()->selectedIdentityRole = $next; // écriture en session
270
        }
271

272
273
274
275
276
277
278
279
        // Si en session aucun rôle n'est sélectionné ou si cette sélection n'est pas valide,
        // on sélectionne le 1er rôle sélectionnable.
        $selectedRoleId = $this->getSessionContainer()->selectedIdentityRole;
        $selectableRoles = $this->getSelectableIdentityRoles();
        if (null === $selectedRoleId || ! isset($selectableRoles[$selectedRoleId])) {
            $firstSelectableRoleId = reset($selectableRoles);
            $this->setSelectedIdentityRole($firstSelectableRoleId); // écriture en session
        }
280

281
282
283
284
        // Rôle sélectionné en session.
        $roleId = $this->getSessionContainer()->selectedIdentityRole; // lecture en session
        if (! $roleId) {
            return null;
285
        }
286

287
288
289
290
291
292
        $role = $selectableRoles[$roleId];
        if (! $this->isRoleValid($role)) {
            return null;
        }

        return $role;
293
    }
294

295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
    /**
     * Retourne le rôle utilisateur sélectionné éventuel au format littéral.
     *
     * @return string
     * @see getSelectedIdentityRole()
     */
    public function getSelectedIdentityRoleToString()
    {
        $role = $this->getSelectedIdentityRole();

        if (! $role) {
            return null;
        }

        $f = new RoleFormatter();
310

311
312
        return $f->format($role);
    }
313

314
    /**
Bertrand Gauthier's avatar
Bertrand Gauthier committed
315
     * Mémorise en session le rôle spécifié comme étant le rôle courant de l'utilisateur.
316
     *
Bertrand Gauthier's avatar
Bertrand Gauthier committed
317
     * NB: seul l'id du rôle est mémorisé en session.
318
     *
Bertrand Gauthier's avatar
Bertrand Gauthier committed
319
     * @param RoleInterface|string $role
320
     *
321
     * @return \UnicaenAuth\Service\UserContext
322
     * @throws RuntimeException
323
324
325
326
327
     */
    public function setSelectedIdentityRole($role)
    {
        if ($role) {
            if (!$this->isRoleValid($role)) {
328
                throw new RuntimeException("Rôle spécifié invalide : '$role'");
329
            }
Bertrand Gauthier's avatar
Bertrand Gauthier committed
330
331
332
333
            if ($role instanceof RoleInterface) {
                $role = $role->getRoleId();
            }
            $this->getSessionContainer()->selectedIdentityRole = $role;
334
        } else {
335
336
            unset($this->getSessionContainer()->selectedIdentityRole);
        }
337

338
339
340
341
342
343
        $selectableIdentityRoles = $this->getSelectableIdentityRoles();
        if (isset($selectableIdentityRoles[$role])){
            $role = $selectableIdentityRoles[$role];
            if ($role instanceof AbstractRole) {
                $this->saveUserLastRole($role);
            }
344

345
346
            $this->triggerUserRoleSelectedEvent(UserRoleSelectedEvent::POST_SELECTION, $role);
        }
347

348
349
350
        return $this;
    }

351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
    /**
     * @param AbstractRole $role
     */
    private function saveUserLastRole(AbstractRole $role)
    {
        /** @var AbstractUser $user */
        $user = $this->getDbUser();
        if (! $user) {
            return;
        }

        $user->setLastRole($role);
        try {
            $this->getEntityManager()->flush($user);
        } catch (ORMException $e) {
            throw new RuntimeException("Erreur rencontrée lors de l'enregistrement en bdd", null, $e);
        }
    }

370
371
372
373
374
    /**
     * Retourne l'éventuel rôle spécifié en session devant être le prochain rôle sélectionné.
     *
     * @return string|null
     */
375
    public function getNextSelectedIdentityRole()
376
377
378
379
380
381
382
383
384
    {
        return $this->getSessionContainer()->nextSelectedIdentityRole;
    }

    /**
     * Mémorise en session le rôle devant être le prochain rôle sélectionné.
     *
     * NB: seul l'id du rôle est mémorisé en session ; la durée de vie du stockage est de 1 requête seulement.
     *
385
     * @param RoleInterface|string $role Le ROLE_ID du rôle (string) ou une instance (RoleInterface)
386
     *
387
388
389
390
391
392
393
394
395
396
397
     * @return \UnicaenAuth\Service\UserContext
     */
    public function setNextSelectedIdentityRole($role)
    {
        if ($role instanceof RoleInterface) {
            $role = $role->getRoleId();
        }

        if ($role) {
            $this->getSessionContainer()->nextSelectedIdentityRole = $role;
            $this->getSessionContainer()->setExpirationHops(1, 'nextSelectedIdentityRole');
398
        } else {
399
400
401
            unset($this->getSessionContainer()->nextSelectedIdentityRole);
        }

402
        $role = $this->getSelectableIdentityRoles()[$role] ?? null;
403
404
405
        if ($role instanceof AbstractRole) {
            $this->saveUserLastRole($role);
        }
406
407
408
        if ($role) {
            $this->triggerUserRoleSelectedEvent(UserRoleSelectedEvent::POST_SELECTION, $role);
        }
409

410
411
        return $this;
    }
412

413
414
415
416
417
418
419
420
421
422
423
424
    /**
     * Déclenche l'événement donnant à l'application l'opportunité de réagir à la sélection d'un rôle.
     *
     * @param string $name Ex: UserRoleSelectedEvent::POST_SELECTION
     * @param RoleInterface|string|null $role Rôle sélectionné
     */
    private function triggerUserRoleSelectedEvent($name, $role)
    {
        $event = new UserRoleSelectedEvent($name);
        $event
            ->setRole($role)
            ->setTarget($this);
425
        $this->getEventManager()->triggerEvent($event);
426
    }
427

428
    /**
429
     * Teste si le rôle spécifié fait partie des rôles disponibles.
430
431
     *
     * @param RoleInterface|string $role
432
     *
433
     * @return boolean
434
     */
435
    public function isRoleValid($role)
436
    {
Bertrand Gauthier's avatar
Bertrand Gauthier committed
437
438
439
        if ($role instanceof RoleInterface) {
            $role = $role->getRoleId();
        }
440

441
        foreach ($this->getIdentityRoles() as $r) {
442
            if ($r instanceof RoleInterface) {
443
444
445
446
447
448
                $r = $r->getRoleId();
            }
            if ($role === $r) {
                return true;
            }
        }
449

450
451
        return false;
    }
452

453
    /**
454
     *
455
     * @return Chain
456
457
458
     */
    private function getIdentityProvider()
    {
459
460
        /* @var $identityProvider Chain */
        $identityProvider = $this->getServiceAuthorize()->getIdentityProvider();
461

462
463
        return $identityProvider;
    }
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

    /**
     * Détermine si une usurpation d'identité est en cours.
     *
     * @return bool
     */
    public function isUsurpationEnCours(): bool
    {
        /** @var array $currentIdentity */
        $currentIdentity = $this->getIdentity();

        return isset($currentIdentity['usurpation']);
    }

    /**
     * Usurpe l'identité d'un autre utilisateur.
     * @param string $usernameUsurpe
     * @return SessionIdentity|null
     */
    public function usurperIdentite(string $usernameUsurpe): ?SessionIdentity
    {
        /** @var array $currentIdentityArray */
        $currentIdentityArray = $this->getIdentity();

        $identity = null;
        if ($identity === null && isset($currentIdentityArray['db'])) {
            /** @var AbstractUser $identity */
            $identity = $currentIdentityArray['db'];
        }
        if ($identity === null && isset($currentIdentityArray['ldap'])) {
            /** @var People $identity */
            $identity = $currentIdentityArray['ldap'];
        }
        if ($identity === null && isset($currentIdentityArray['shib'])) {
            /** @var ShibUser $identity */
            $identity = $currentIdentityArray['shib'];
        }
        if ($identity === null) {
            return null;
        }

        // seuls les logins spécifiés dans la config sont habilités à usurper des identités
        if (! in_array($identity->getUsername(), $this->moduleOptions->getUsurpationAllowedUsernames())) {
            throw new RuntimeException("Usurpation non explicitement autorisée");
        }

        $usernameUsurpateur = $identity->getUsername();

        $sessionIdentity = SessionIdentity::newInstanceForUsurpation($usernameUsurpe, $usernameUsurpateur, $this->getAuthenticationType());
        $this->authenticationService->getStorage()->write($sessionIdentity);

        return $sessionIdentity;
    }

    /**
     * Met fin à l'usurpation d'identité en cours.
     */
    public function stopperUsurpation()
    {
        if (! $this->isUsurpationEnCours()) {
            return;
        }

        /** @var array $currentIdentityArray */
        $currentIdentityArray = $this->getIdentity();

        /** @var AbstractUser $usurpateur */
        $usurpateur = Usurpation::extractUsurpateurFromIdentityArray($currentIdentityArray);

        $sessionIdentity = SessionIdentity::newInstance($usurpateur->getUsername(), $this->getAuthenticationType());
        $this->authenticationService->getStorage()->write($sessionIdentity);
535
536
537
538
539

        // Sélection du dernier rôle endossé.
        if ($role = $usurpateur->getLastRole()) {
            $this->setNextSelectedIdentityRole($role);
        }
540
    }
541
}