Projet

Général

Profil

0001-Allow-users-to-provide-their-email-or-username-for-p.patch

Benjamin Renard, 04 décembre 2020 19:14

Télécharger (20,8 ko)

Voir les différences:

Subject: [PATCH] Allow users to provide their email or username for password
 reset process

 src/authentic2/app_settings.py                |   3 +
 src/authentic2/forms/passwords.py             |   7 +-
 src/authentic2/journal_event_types.py         |   4 +-
 .../locale/fr/LC_MESSAGES/django.po           | 115 ++++++++++--------
 .../password_reset_instructions.html          |   5 +-
 src/authentic2/utils/__init__.py              |   4 +-
 src/authentic2/views.py                       |  14 ++-
 tests/test_password_reset.py                  |  30 ++++-
 8 files changed, 117 insertions(+), 65 deletions(-)
src/authentic2/app_settings.py
139 139
    A2_USER_CAN_RESET_PASSWORD=Setting(
140 140
        default=None,
141 141
        definition='Allow online reset of passwords'),
142
    A2_RESET_PASSWORD_ID_LABEL=Setting(
143
        default=None,
144
        definition='Alternate ID label for the password reset form'),
142 145
    A2_EMAIL_IS_UNIQUE=Setting(
143 146
        default=False,
144 147
        definition='Email of users must be unique'),
src/authentic2/forms/passwords.py
19 19

  
20 20
from django.contrib.auth import forms as auth_forms
21 21
from django.core.exceptions import ValidationError
22
from django.db.models import Q
22 23
from django.forms import Form
23 24
from django import forms
24 25
from django.utils.translation import ugettext_lazy as _
......
35 36
class PasswordResetForm(forms.Form):
36 37
    next_url = forms.CharField(widget=forms.HiddenInput, required=False)
37 38

  
38
    email = ValidatedEmailField(
39
        label=_("Email"), max_length=254)
39
    email = forms.CharField(
40
        label=_("Email or username"), max_length=254)
40 41

  
41 42
    def save(self):
42 43
        """
......
45 46
        """
46 47
        email = self.cleaned_data["email"].strip()
47 48
        users = get_user_queryset()
48
        active_users = users.filter(email__iexact=email, is_active=True)
49
        active_users = users.filter(Q(email__iexact=email) | Q(username__iexact=email), is_active=True)
49 50
        for user in active_users:
50 51
            # we don't set the password to a random string, as some users should not have
51 52
            # a password
src/authentic2/journal_event_types.py
154 154
    def get_message(cls, event, context):
155 155
        email = event.get_data('email')
156 156
        if email:
157
            return _('password reset request with email "%s"') % email
157
            return _('password reset request with email (or username) "%s"') % email
158 158
        return super().get_message(event, context)
159 159

  
160 160

  
......
179 179
    def get_message(cls, event, context):
180 180
        email = event.get_data('email')
181 181
        if email:
182
            return _('password reset failure with email "%s"') % email
182
            return _('password reset failure with email (or username) "%s"') % email
183 183
        return super().get_message(event, context)
184 184

  
185 185

  
src/authentic2/locale/fr/LC_MESSAGES/django.po
7 7
msgstr ""
8 8
"Project-Id-Version: Authentic\n"
9 9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2020-11-23 10:34+0100\n"
10
"POT-Creation-Date: 2020-12-04 18:34+0100\n"
11 11
"PO-Revision-Date: 2020-10-13 11:25+0200\n"
12 12
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
13 13
"Language-Team: None\n"
......
17 17
"Content-Transfer-Encoding: 8bit\n"
18 18
"Plural-Forms: nplurals=2; plural=n>1;\n"
19 19

  
20
#: debian/multitenant/debian_config.py:40 src/authentic2/forms/passwords.py:39
20
#: debian/multitenant/debian_config.py:40
21 21
#: src/authentic2/forms/registration.py:40 src/authentic2/manager/forms.py:644
22 22
#: src/authentic2/manager/templates/authentic2/manager/user_create_registration_email_body.txt:4
23 23
#: src/authentic2/saml/models.py:134
......
1148 1148
msgid "The image is not valid"
1149 1149
msgstr "L’image n’est pas valide"
1150 1150

  
1151
#: src/authentic2/forms/passwords.py:95 src/authentic2/forms/passwords.py:108
1151
#: src/authentic2/forms/passwords.py:40
1152
msgid "Email or username"
1153
msgstr "Courriel ou nom d'utilisateur"
1154

  
1155
#: src/authentic2/forms/passwords.py:96 src/authentic2/forms/passwords.py:109
1152 1156
msgid "New password"
1153 1157
msgstr "Nouveau mot de passe"
1154 1158

  
1155
#: src/authentic2/forms/passwords.py:96 src/authentic2/forms/passwords.py:109
1159
#: src/authentic2/forms/passwords.py:97 src/authentic2/forms/passwords.py:110
1156 1160
msgid "New password confirmation"
1157 1161
msgstr "Confirmation du nouveau mot de passe"
1158 1162

  
1159
#: src/authentic2/forms/passwords.py:101 src/authentic2/forms/passwords.py:115
1163
#: src/authentic2/forms/passwords.py:102 src/authentic2/forms/passwords.py:116
1160 1164
msgid "New password must differ from old password"
1161 1165
msgstr "Le nouveau mot de passe doit être différent de l’ancien."
1162 1166

  
1163
#: src/authentic2/forms/passwords.py:107
1167
#: src/authentic2/forms/passwords.py:108
1164 1168
msgid "Old password"
1165 1169
msgstr "Ancien mot de passe"
1166 1170

  
......
1390 1394
msgstr "demande de réinitialisation du mot de passe"
1391 1395

  
1392 1396
#: src/authentic2/journal_event_types.py:157
1393
#, python-format
1394
msgid "password reset request with email \"%s\""
1397
msgid "password reset request with email (or username) \"%s\""
1395 1398
msgstr ""
1396
"demande de réinitialisation du mot de passe pour l’adresse de courriel « %s »"
1399
"demande de réinitialisation du mot de passe pour l’adresse de courriel (ou le "
1400
"nom d'utilisateur) « %s »"
1397 1401

  
1398 1402
#: src/authentic2/journal_event_types.py:163 src/authentic2/models.py:359
1399 1403
#: src/authentic2/models.py:360
......
1406 1410

  
1407 1411
#: src/authentic2/journal_event_types.py:182
1408 1412
#, python-format
1409
msgid "password reset failure with email \"%s\""
1413
msgid "password reset failure with email (or username) \"%s\""
1410 1414
msgstr ""
1411
"échec de réinitialisation du mot de passe pour l’adresse de courriel « %s »"
1415
"échec de réinitialisation du mot de passe pour l’adresse de courriel (ou le "
1416
"nom d'utilisateur) « %s »"
1412 1417

  
1413 1418
#: src/authentic2/journal_event_types.py:188
1414 1419
msgid "password change"
......
4651 4656
msgstr "Déconnecté"
4652 4657

  
4653 4658
#: src/authentic2/templates/registration/password_change_done.html:9
4654
#: src/authentic2/views.py:1302
4659
#: src/authentic2/views.py:1303
4655 4660
msgid "Password changed"
4656 4661
msgstr "Mot de passe modifié"
4657 4662

  
......
4692 4697
msgstr "Instructions de réinitialisation du mot de passe"
4693 4698

  
4694 4699
#: src/authentic2/templates/registration/password_reset_instructions.html:10
4695
#, python-format
4696 4700
msgid ""
4697 4701
"\n"
4698
"    If your email address exists in our database, an email has been sent to "
4699
"%(email)s.\n"
4702
"    If a user account exists in our database with your email address (or "
4703
"your\n"
4704
"    username), an email has been sent to you.\n"
4700 4705
"    "
4701 4706
msgstr ""
4702 4707
"\n"
4703
"    Si votre adresse électronique est présente dans notre base, un courriel "
4704
"a été envoyé à %(email)s.\n"
4708
"    Si un compte utilisateur est présent dans notre base avec votre adresse "
4709
"électronique (ou votre nom d'utilisateur), un courriel vous a été envoyé.\n"
4705 4710
"    "
4706 4711

  
4707
#: src/authentic2/templates/registration/password_reset_instructions.html:15
4712
#: src/authentic2/templates/registration/password_reset_instructions.html:16
4708 4713
msgid ""
4709 4714
"\n"
4710 4715
"    Follow the instructions in this email in order to choose a new "
......
4716 4721
"de passe.\n"
4717 4722
"    "
4718 4723

  
4719
#: src/authentic2/templates/registration/password_reset_instructions.html:21
4724
#: src/authentic2/templates/registration/password_reset_instructions.html:22
4720 4725
#: src/authentic2/templates/registration/registration_complete.html:23
4721 4726
msgid ""
4722 4727
"\n"
......
4729 4734
"    également être considéré comme un pourriel (spam) : n’oubliez pas\n"
4730 4735
"    de regarder dans votre dossier « courriers indésirables ».        "
4731 4736

  
4732
#: src/authentic2/templates/registration/password_reset_instructions.html:27
4737
#: src/authentic2/templates/registration/password_reset_instructions.html:28
4733 4738
#, python-format
4734 4739
msgid ""
4735 4740
"\n"
......
4746 4751
"    puis recommencez la procédure de réinitialisation du mot de passe.\n"
4747 4752
"    "
4748 4753

  
4749
#: src/authentic2/templates/registration/password_reset_instructions.html:35
4754
#: src/authentic2/templates/registration/password_reset_instructions.html:36
4750 4755
msgid "Back to login"
4751 4756
msgstr "Retour à la page de connexion"
4752 4757

  
......
4805 4810
#: src/authentic2/templates/registration/registration_completion_choose.html:5
4806 4811
#: src/authentic2/templates/registration/registration_completion_form.html:5
4807 4812
#: src/authentic2/templates/registration/registration_completion_form.html:10
4808
#: src/authentic2/views.py:800
4813
#: src/authentic2/views.py:801
4809 4814
msgid "Registration"
4810 4815
msgstr "Création d’un compte"
4811 4816

  
......
5000 5005
"Cette page est périmée car vous vous êtes connecté entretemps; nous l’avons "
5001 5006
"rechargée pour vous."
5002 5007

  
5003
#: src/authentic2/views.py:645 src/authentic2/views.py:730
5008
#: src/authentic2/views.py:645 src/authentic2/views.py:731
5004 5009
msgid "Password Reset"
5005 5010
msgstr "Réinitialisation du mot de passe"
5006 5011

  
5007
#: src/authentic2/views.py:681 src/authentic2/views.py:834
5008
#, python-format
5012
#: src/authentic2/views.py:682
5009 5013
msgid ""
5010
"An email has already been sent to %s. Click \"Validate\" again if you really "
5011
"want it to be sent again."
5014
"An email has already been sent to your address. Click \"Validate\" again if "
5015
"you really want it to be sent again."
5012 5016
msgstr ""
5013
"Un courriel a déjà été envoyé à %s. Cliquez sur « Valider » si vous voulez "
5014
"vraiment qu’il soit réenvoyé."
5017
"Un courriel a déjà été envoyé à votre adresse. Cliquez sur « Valider » si "
5018
"vous voulez vraiment qu’il soit réenvoyé."
5015 5019

  
5016
#: src/authentic2/views.py:692 src/authentic2/views.py:844
5020
#: src/authentic2/views.py:693 src/authentic2/views.py:845
5017 5021
msgid ""
5018 5022
"Multiple emails have already been sent to this address. Further attempts are "
5019 5023
"blocked, please check your spam folder or try again later."
......
5022 5026
"envois sont bloqués, vous devriez vérifier votre dossier d’indésirables "
5023 5027
"(spams, pourriels) ou réessayer plus tard."
5024 5028

  
5025
#: src/authentic2/views.py:701
5029
#: src/authentic2/views.py:702
5026 5030
msgid ""
5027 5031
"Multiple password reset attempts have already been made from this IP "
5028 5032
"address. No further email will be sent, please check your spam folder or try "
......
5033 5037
"devriez vérifier votre dossier d’indésirables (spams, pourriels) ou "
5034 5038
"réessayer plus tard."
5035 5039

  
5036
#: src/authentic2/views.py:743
5040
#: src/authentic2/views.py:744
5037 5041
msgid "Password reset token is unknown or expired"
5038 5042
msgstr "Le jeton de réinitialisation de mot de passe est inconnu ou expiré"
5039 5043

  
5040
#: src/authentic2/views.py:746
5044
#: src/authentic2/views.py:747
5041 5045
msgid "Password reset token is invalid"
5042 5046
msgstr "Le jeton de réinitialisation de mot de passe est invalide"
5043 5047

  
5044
#: src/authentic2/views.py:754
5048
#: src/authentic2/views.py:755
5045 5049
msgid "User not found"
5046 5050
msgstr "Utilisateur introuvable"
5047 5051

  
5048
#: src/authentic2/views.py:763
5052
#: src/authentic2/views.py:764
5049 5053
msgid ""
5050 5054
"It's not possible to reset your password. Please contact an administrator."
5051 5055
msgstr ""
5052 5056
"Votre mot de passe n’a pas pu être réinitialisé. Veuillez contacter votre "
5053 5057
"administrateur."
5054 5058

  
5055
#: src/authentic2/views.py:770
5059
#: src/authentic2/views.py:771
5056 5060
msgid "Enter new password"
5057 5061
msgstr "Entrez un nouveau mot de passe"
5058 5062

  
5059
#: src/authentic2/views.py:852
5063
#: src/authentic2/views.py:835
5064
#, python-format
5065
msgid ""
5066
"An email has already been sent to %s. Click \"Validate\" again if you really "
5067
"want it to be sent again."
5068
msgstr ""
5069
"Un courriel a déjà été envoyé à %s. Cliquez sur « Valider » si vous voulez "
5070
"vraiment qu’il soit réenvoyé."
5071

  
5072
#: src/authentic2/views.py:853
5060 5073
msgid ""
5061 5074
"Multiple registration attempts have already been made from this IP address. "
5062 5075
"No further email will be sent, please check your spam folder or try again "
......
5067 5080
"vérifier votre dossier d’indésirables (spams, pourriels) ou réessayer plus "
5068 5081
"tard."
5069 5082

  
5070
#: src/authentic2/views.py:921
5083
#: src/authentic2/views.py:922
5071 5084
msgid "Your activation key is unknown or expired"
5072 5085
msgstr "Votre clé d’activation est inconnue ou a expiré"
5073 5086

  
5074
#: src/authentic2/views.py:924
5087
#: src/authentic2/views.py:925
5075 5088
msgid "Activation failed"
5076 5089
msgstr "Échec à l’activation du compte"
5077 5090

  
5078
#: src/authentic2/views.py:1186
5091
#: src/authentic2/views.py:1187
5079 5092
msgid "Request account deletion"
5080 5093
msgstr "Demande de suppression de compte"
5081 5094

  
5082
#: src/authentic2/views.py:1198
5095
#: src/authentic2/views.py:1199
5083 5096
msgid ""
5084 5097
"An account deletion validation email has been sent to your email address."
5085 5098
msgstr ""
5086 5099
"Un message pour valider la suppression du compte a été envoyé à votre "
5087 5100
"adresse électronique."
5088 5101

  
5089
#: src/authentic2/views.py:1209
5102
#: src/authentic2/views.py:1210
5090 5103
msgid "Confirm account deletion"
5091 5104
msgstr "Confirmation de la suppression du compte"
5092 5105

  
5093
#: src/authentic2/views.py:1221 src/authentic2/views.py:1235
5106
#: src/authentic2/views.py:1222 src/authentic2/views.py:1236
5094 5107
msgid "This account has previously been deleted."
5095 5108
msgstr "Ce compte a déjà été supprimé."
5096 5109

  
5097
#: src/authentic2/views.py:1224
5110
#: src/authentic2/views.py:1225
5098 5111
msgid "This account is inactive, it cannot be deleted."
5099 5112
msgstr "Ce compte est désactivé, il ne peut plus être supprimé."
5100 5113

  
5101
#: src/authentic2/views.py:1227
5114
#: src/authentic2/views.py:1228
5102 5115
msgid "The account deletion request is too old, try again"
5103 5116
msgstr "La demande de suppression de compte est expirée."
5104 5117

  
5105
#: src/authentic2/views.py:1229
5118
#: src/authentic2/views.py:1230
5106 5119
msgid "The account deletion request is invalid, try again"
5107 5120
msgstr "La demande de suppression de compte n’est pas valide."
5108 5121

  
5109
#: src/authentic2/views.py:1231
5122
#: src/authentic2/views.py:1232
5110 5123
msgid "The account deletion request was not on this site, try again"
5111 5124
msgstr ""
5112 5125
"Votre demande de suppression de compte vient d’un autre site que celui-ci."
5113 5126

  
5114
#: src/authentic2/views.py:1255
5127
#: src/authentic2/views.py:1256
5115 5128
msgid "Deletion performed."
5116 5129
msgstr "Suppression effectuée."
5117 5130

  
5118
#: src/authentic2/views.py:1280
5131
#: src/authentic2/views.py:1281
5119 5132
msgid "Password Change"
5120 5133
msgstr "Changement de mot de passe"
5121 5134

  
5122
#: src/authentic2/views.py:1291
5135
#: src/authentic2/views.py:1292
5123 5136
msgid "Password change is forbidden"
5124 5137
msgstr "Changement de mot de passe interdit"
5125 5138

  
5126
#: src/authentic2/views.py:1336
5139
#: src/authentic2/views.py:1337
5127 5140
msgid "Consent Management"
5128 5141
msgstr "Gestion des autorisations"
src/authentic2/templates/registration/password_reset_instructions.html
7 7

  
8 8
{% block content %}
9 9
  <p><strong>
10
    {% blocktrans with email=request.session.reset_email %}
11
    If your email address exists in our database, an email has been sent to {{ email }}.
10
    {% blocktrans %}
11
    If a user account exists in our database with your email address (or your
12
    username), an email has been sent to you.
12 13
    {% endblocktrans %}
13 14
  </strong></p>
14 15
  <p><strong>
src/authentic2/utils/__init__.py
804 804
        user.save()
805 805
    lifetime = settings.PASSWORD_RESET_TIMEOUT_DAYS * 3600 * 24
806 806
    # invalidate any token associated with this user
807
    Token.objects.filter(kind='pw-reset', content__user=user.pk, content__email=user.email).delete()
808
    token = Token.create('pw-reset', {'user': user.pk, 'email': user.email}, duration=lifetime)
807
    Token.objects.filter(kind='pw-reset', content__user=user.pk, content__email=user.email, content__username=user.username).delete()
808
    token = Token.create('pw-reset', {'user': user.pk, 'email': user.email, 'username': user.username}, duration=lifetime)
809 809
    reset_url = make_url(
810 810
        'password_reset_confirm',
811 811
        kwargs={'token': token.uuid_b64url},
src/authentic2/views.py
653 653
            'registration/password_reset_form.html',
654 654
        ]
655 655

  
656
    def get_form(self, **kwargs):
657
        """Return an instance of the form to be used in this view."""
658
        form = super(PasswordResetView, self).get_form(**kwargs)
659
        if app_settings.A2_RESET_PASSWORD_ID_LABEL:
660
            form.fields['email'].label = app_settings.A2_RESET_PASSWORD_ID_LABEL
661
        return form
662

  
656 663
    def get_form_kwargs(self, **kwargs):
657 664
        kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs)
658 665
        initial = kwargs.setdefault('initial', {})
......
671 678

  
672 679
        # if an email has already been sent, warn once before allowing resend
673 680
        token = models.Token.objects.filter(
674
            kind='pw-reset', content__email=email, expires__gt=timezone.now()
681
            Q(content__email=email) | Q(content__username=email),
682
            kind='pw-reset', expires__gt=timezone.now()
675 683
        ).exists()
676 684
        resend_key = 'pw-reset-allow-resend'
677 685
        if app_settings.A2_TOKEN_EXISTS_WARNING and token and not self.request.session.get(resend_key):
678 686
            self.request.session[resend_key] = True
679 687
            form.add_error(
680 688
                'email',
681
                _('An email has already been sent to %s. Click "Validate" again if '
682
                  'you really want it to be sent again.') % email
689
                _('An email has already been sent to your address. Click "Validate" again if '
690
                  'you really want it to be sent again.')
683 691
            )
684 692
            return self.form_invalid(form)
685 693
        self.request.session[resend_key] = False
tests/test_password_reset.py
43 43
    utils.assert_event('user.password.reset', user=simple_user, session=app.session)
44 44

  
45 45

  
46
def test_view(app, simple_user, mailoutbox, settings):
46
def test_view_with_email(app, simple_user, mailoutbox, settings):
47 47
    url = reverse('password_reset')
48 48
    resp = app.get(url, status=200)
49 49
    resp.form.set('email', simple_user.email)
......
53 53
    utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
54 54
    assert resp['Location'].endswith('/instructions/')
55 55
    resp = resp.follow()
56
    assert simple_user.email in resp.text
56
    assert '"noreply@example.net"' in resp.text
57
    assert 'show only addr' not in resp.text
58
    assert len(mailoutbox) == 1
59
    url = utils.get_link_from_mail(mailoutbox[0])
60
    relative_url = url.split('testserver')[1]
61
    resp = app.get(relative_url, status=200)
62
    resp.form.set('new_password1', '1234==aA')
63
    resp.form.set('new_password2', '1234==aA')
64
    resp = resp.form.submit()
65
    # verify user is logged
66
    assert str(app.session['_auth_user_id']) == str(simple_user.pk)
67

  
68
    with override_settings(A2_USER_CAN_RESET_PASSWORD=False):
69
        url = reverse('password_reset')
70
        app.get(url, status=404)
71

  
72

  
73
def test_view_with_username(app, simple_user, mailoutbox, settings):
74
    url = reverse('password_reset')
75
    resp = app.get(url, status=200)
76
    resp.form.set('email', simple_user.username)
77
    assert len(mailoutbox) == 0
78
    settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
79
    resp = resp.form.submit()
80
    utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
81
    assert resp['Location'].endswith('/instructions/')
82
    resp = resp.follow()
57 83
    assert '"noreply@example.net"' in resp.text
58 84
    assert 'show only addr' not in resp.text
59 85
    assert len(mailoutbox) == 1
60
-