User.php 10.9 KB
Newer Older
1
<?php
2

3
4
namespace UnicaenAuth\Service;

5
6
7
use DateTime;
use Doctrine\ORM\NoResultException;
use Ramsey\Uuid\Uuid;
8
9
use UnicaenApp\Entity\Ldap\People;
use UnicaenApp\Exception\RuntimeException;
10
use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper;
11
use UnicaenAuth\Entity\Shibboleth\ShibUser;
12
use UnicaenAuth\Event\UserAuthenticatedEvent;
13
use UnicaenAuth\Options\ModuleOptions;
14
use Zend\Crypt\Password\Bcrypt;
15
16
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;
17
18
19
20
21
22
use Zend\Form\Element\Csrf;
use Zend\Form\Element\Password;
use Zend\Form\Element\Submit;
use Zend\Form\Element\Text;
use Zend\Form\Form;
use Zend\InputFilter\Input;
23
24
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
25
use Zend\Validator\Identical;
26
use ZfcUser\Entity\UserInterface;
27
use ZfcUser\Options\AuthenticationOptionsInterface;
28
use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions;
29
use UnicaenAuth\Entity\Db\AbstractUser;
30
31

/**
32
 * Service traitant des utilisateurs locaux de l'application.
33
 *
34
 * @see \UnicaenAuth\Authentication\Adapter\AbstractFactory
35
 * @author Unicaen
36
 */
37
class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
38
{
39
    use ServiceLocatorAwareTrait;
40

41
    const EVENT_USER_AUTHENTICATED_PRE_PERSIST = 'userAuthenticated.prePersist';
42

43
44
45
46
47
    /**
     * @var EventManagerInterface
     */
    protected $eventManager;

48
    /**
49
     * @var ModuleOptions
50
51
52
53
     */
    protected $options;

    /**
54
     * @var AuthenticationOptionsInterface
55
56
     */
    protected $zfcUserOptions;
57

58
59
60
61
62
    /**
     * @var UserMapper
     */
    protected $userMapper;

63
    /**
64
     * @var LdapPeopleMapper
65
     */
66
    protected $ldapPeopleMapper;
67

68
    /**
69
     * Save authenticated user in database from LDAP or Shibboleth data.
70
     *
71
     * @param People|ShibUser $userData
72
73
     * @return bool
     */
74
    public function userAuthenticated($userData)
75
76
77
78
    {
        if (!$this->getOptions()->getSaveLdapUserInDatabase()) {
            return false;
        }
79
80
81

        switch (true) {
            case $userData instanceof People:
82
                $username = $userData->getData($this->getOptions()->getLdapUsername());
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
                $email = $userData->getMail();
                $password = 'ldap';
                $state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1;
                break;
            case $userData instanceof ShibUser:
                $username = $userData->getUsername();
                $email = $userData->getEmail();
                $password = 'shib';
                $state = 1;
                break;
            default:
                throw new RuntimeException("A implémenter!!");
                break;
        }

        if (!$username) {
99
100
            return false;
        }
101

102
        if (is_int($username)) {
103
            // c'est un id : cela signifie que l'utilisateur existe déjà dans la bdd (et pas dans le LDAP), rien à faire
104
105
            return true;
        }
106

107
        if (!is_string($username)) {
108
            throw new RuntimeException("Identité rencontrée inattendue.");
109
        }
110

111
112
113
114
115
116
117
118
119
        $mapper = $this->getUserMapper();

        /** @var UserInterface $entity */
        $entity = $mapper->findByUsername($username);
        if (!$entity) {
            $entityClass = $this->getZfcUserOptions()->getUserEntityClass();
            $entity = new $entityClass;
            $entity->setUsername($username);
            $method = 'insert';
120
        }
121
122
        else {
            $method = 'update';
123
        }
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
        $entity->setEmail($email);
        $entity->setDisplayName($userData->getDisplayName());
        $entity->setPassword($password);
        $entity->setState($state);

        // pre-persist
        $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::PRE_PERSIST);
        $this->triggerEvent($event, $entity, $userData);

        // persist
        $mapper->$method($entity);

        // post-persist
        $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::POST_PERSIST);
        $this->triggerEvent($event, $entity, $userData);
139

140
141
        return true;
    }
142

143
    /**
144
145
146
     * @param UserAuthenticatedEvent $event
     * @param UserInterface          $entity
     * @param People|ShibUser        $userData
147
     */
148
    private function triggerEvent(UserAuthenticatedEvent $event, $entity, $userData)
149
150
151
152
153
154
155
156
157
158
159
    {
        $event->setTarget($this);
        $event->setDbUser($entity);
        if ($userData instanceof People) {
            $event->setLdapUser($userData);
        } elseif ($userData instanceof ShibUser) {
            $event->setShibUser($userData);
        }

        $this->getEventManager()->trigger($event);
    }
160

161
162
163
164
165
166
167
168
169
170
171
172
    /**
     * @return UserMapper
     */
    public function getUserMapper()
    {
        if ($this->userMapper === null) {
            $this->userMapper = $this->getServiceLocator()->get('zfcuser_user_mapper');
        }

        return $this->userMapper;
    }

173
174
175
176
177
178
179
180
181
182
183
    /**
     * Retrieve the event manager
     *
     * Lazy-loads an EventManager instance if none registered.
     *
     * @return EventManagerInterface
     */
    public function getEventManager()
    {
        return $this->eventManager;
    }
184

185
186
187
188
    /**
     * Inject an EventManager instance
     *
     * @param  EventManagerInterface $eventManager
189
     * @return self
190
191
192
     */
    public function setEventManager(EventManagerInterface $eventManager)
    {
193
        $eventManager->setIdentifiers([
194
195
            __CLASS__,
            get_called_class(),
196
        ]);
197
        $this->eventManager = $eventManager;
198

199
200
        return $this;
    }
201
202

    /**
203
     * @param ModuleOptions $options
204
     * @return self
205
     */
206
    public function setOptions(ModuleOptions $options)
207
208
    {
        $this->options = $options;
209

210
        return $this;
211
212
213
    }

    /**
214
     * @return ModuleOptions
215
216
217
     */
    public function getOptions()
    {
218
        if (!$this->options instanceof ModuleOptions) {
219
            $this->setOptions($this->getServiceLocator()->get('unicaen-auth_module_options'));
220
        }
221

222
223
224
225
        return $this->options;
    }

    /**
226
     * @param ZfcUserModuleOptions $options
227
     * @return self
228
     */
229
    public function setZfcUserOptions(ZfcUserModuleOptions $options)
230
231
    {
        $this->zfcUserOptions = $options;
232

233
        return $this;
234
235
236
    }

    /**
237
     * @return ZfcUserModuleOptions
238
239
240
     */
    public function getZfcUserOptions()
    {
241
        if (!$this->zfcUserOptions instanceof ZfcUserModuleOptions) {
Laurent Lécluse's avatar
Laurent Lécluse committed
242
            $this->setZfcUserOptions($this->getServiceLocator()->get('zfcuser_module_options'));
243
        }
244

245
246
        return $this->zfcUserOptions;
    }
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379

    const PASSWORD_RESET_TOKEN_SEP = '-';
    const PASSWORD_RESET_TOKEN_DATE_FORMAT = 'YmdHis';

    /**
     * Construit le formulaire de saisie de l'adresse électronique à laquelle envoyer le lien de
     * changement de mot de passe.
     *
     * @return Form
     */
    public function createResetPasswordEmailForm()
    {
        $form = new Form();
        $form->add((new Text('email'))->setLabel("Adresse électronique :"));
        $form->add((new Csrf('csrf')));
        $form->add((new Submit('submit'))->setLabel("Envoyer le lien"));
        $form->getInputFilter()->add((new Input('email'))->setRequired(true));

        return $form;
    }

    /**
     * Construit le formulaire de saisie d'un nouveau mot de passe.
     *
     * @return Form
     */
    public function createPasswordChangeForm()
    {
        $form = new Form();
        $form->add((new Password('password'))->setLabel("Nouveau mot de passe :"));
        $form->add((new Password('passwordbis'))->setLabel("Confirmation du nouveau mot de passe :"));
        $form->add((new Csrf('csrf')));
        $form->add((new Submit('submit'))->setLabel("Enregistrer"));
        $form->getInputFilter()->add((new Input('password'))->setRequired(true));

        $passwordbisInput = (new Input('passwordbis'))->setRequired(true);
        $passwordbisInput->getValidatorChain()->attach(new Identical('password'));
        $form->getInputFilter()->add($passwordbisInput);

        return $form;
    }

    /**
     * Si l'utilisateur dont le username égale l'email spécifié est trouvé,
     * génère puis enregistre le token permettant d'autoriser cet utilisateur à changer son mot de passe.
     *
     * @param string $email Email de l'utilisateur qui doit être aussi son username
     * @return string|null Token généré
     * @throws NoResultException Aucun utilisateur trouvé avec cet email
     */
    public function updateUserPasswordResetToken($email)
    {
        // Si l'email est inconnu, on ne fera rien mais on ne le signale pas sinon le formulaire permettrait
        // de tester si des emails potentiellement valides existent dans la base.
        $user = $this->getUserMapper()->findByEmail($email); /** @var User $user */
        if ($user === null) {
            throw new NoResultException();
        }

        // Génération du token.
        $token = $this->generatePasswordResetToken();

        // Enregistrement du token dans la table des utilisateurs
        $user->setPasswordResetToken($token);
        $this->getUserMapper()->update($user);

        return $token;
    }

    /**
     * @param AbstractUser $user
     */
    public function clearUserPasswordResetToken(AbstractUser $user)
    {
        $user->setPasswordResetToken(null);
        $this->getUserMapper()->update($user);
    }

    /**
     * @param AbstractUser $user
     * @param string       $password
     */
    public function updateUserPassword(AbstractUser $user, $password)
    {
        $bcrypt = new Bcrypt();
        $bcrypt->setCost($this->getZfcUserOptions()->getPasswordCost());
        $password = $bcrypt->create($password);

        $user->setPasswordResetToken(null);
        $user->setPassword($password);
        $this->getUserMapper()->update($user);
    }

    /**
     * Génération d'un token pour la demande de renouvellement de mot de passe.
     *
     * @return string
     */
    public function generatePasswordResetToken()
    {
        // NB: la date de fin de vie du token est concaténée à la fin.
        $token = Uuid::uuid4()->toString() . self::PASSWORD_RESET_TOKEN_SEP . date('YmdHis', time() + 3600*24);
        // durée de vie = 24h

        return $token;
    }

    /**
     * Génération du motif permettant de rechercher un token dans la table des utilisateurs.
     *
     * Rappel: la date de génération est concaténée à la fin.
     *
     * @param string $tokenUnderTest Le token recherché
     * @return string
     */
    public function generatePasswordResetTokenSearchPattern($tokenUnderTest)
    {
        return $tokenUnderTest . self::PASSWORD_RESET_TOKEN_SEP . '%';
    }

    /**
     * Extrait la date de fin de vie d'un token.
     *
     * @param string $token
     * @return DateTime
     */
    public function extractDateFromResetPasswordToken($token)
    {
        $ts = ltrim(strrchr($token, $sep = self::PASSWORD_RESET_TOKEN_SEP), $sep);
        $date = DateTime::createFromFormat(self::PASSWORD_RESET_TOKEN_DATE_FORMAT, $ts);

        return $date;
    }
380
}