Projet

Général

Profil

0001-accounts-send-validation-email-before-self-triggered.patch

Paul Marillonnet, 19 septembre 2019 15:23

Télécharger (18,6 ko)

Voir les différences:

Subject: [PATCH] accounts: send validation email before self-triggered account
 deletion (#27823)

 src/authentic2/app_settings.py                |  3 +
 .../account_delete_notification_subject.txt   |  2 +-
 .../account_deletion_code_body.html           | 15 +++
 .../authentic2/account_deletion_code_body.txt | 10 ++
 .../account_deletion_code_subject.txt         |  1 +
 ...e.html => accounts_delete_validation.html} | 10 +-
 .../authentic2/accounts_plain_message.html    | 10 ++
 src/authentic2/urls.py                        |  3 +
 src/authentic2/utils/__init__.py              | 29 ++++++
 src/authentic2/views.py                       | 91 +++++++++++++------
 tests/test_views.py                           | 83 ++++++++++++++++-
 11 files changed, 221 insertions(+), 36 deletions(-)
 create mode 100644 src/authentic2/templates/authentic2/account_deletion_code_body.html
 create mode 100644 src/authentic2/templates/authentic2/account_deletion_code_body.txt
 create mode 100644 src/authentic2/templates/authentic2/account_deletion_code_subject.txt
 rename src/authentic2/templates/authentic2/{accounts_delete.html => accounts_delete_validation.html} (57%)
 create mode 100644 src/authentic2/templates/authentic2/accounts_plain_message.html
src/authentic2/app_settings.py
278 278
    A2_EMAIL_CHANGE_TOKEN_LIFETIME=Setting(
279 279
        default=7200,
280 280
        definition='Lifetime in seconds of the token sent to verify email adresses'),
281
    A2_DELETION_REQUEST_LIFETIME=Setting(
282
        default=48*3600,
283
        definition='Lifetime in seconds of the user account deletion request'),
281 284
    A2_REDIRECT_WHITELIST=Setting(
282 285
        default=(),
283 286
        definition='List of origins which are authorized to ask for redirection.'),
src/authentic2/templates/authentic2/account_delete_notification_subject.txt
1
{% load i18n %}{% autoescape off %}{% blocktrans %}Account deletion request on {{ site }}{% endblocktrans %}{% endautoescape %}
1
{% load i18n %}{% autoescape off %}{% blocktrans %}Account deletion on {{ site }}{% endblocktrans %}{% endautoescape %}
src/authentic2/templates/authentic2/account_deletion_code_body.html
1
{% load i18n %}
2
<html>
3
  <body style="max-width: 90ex">
4
        <p>{% blocktrans %}{{ full_name }},{% endblocktrans %}</p>
5
        <p>
6
{% blocktrans %}
7
Please click on {{ deletion_url }}
8
if you want to validate your account deletion request on
9
{{ site }}.
10
If so, all related data will be deleted in the next few hours.
11
You won't be able to log in with this account anymore.
12
{% endblocktrans %}
13
        </p>
14
  </body>
15
</html>
src/authentic2/templates/authentic2/account_deletion_code_body.txt
1
{% load i18n %}{% autoescape off %}{% blocktrans %}{{ full_name }},{% endblocktrans %}
2

  
3
{% blocktrans %}
4
Please click on {{ deletion_url }}
5
if you want to validate your account deletion request on
6
{{ site }}.
7
If so, all related data will be deleted in the next few hours.
8
You won't be able to log in with this account anymore.
9
{% endblocktrans %}
10
{% endautoescape %}
src/authentic2/templates/authentic2/account_deletion_code_subject.txt
1
{% load i18n %}{% autoescape off %}{% blocktrans %}Validate account deletion request on {{ site }}{% endblocktrans %}{% endautoescape %}
src/authentic2/templates/authentic2/accounts_delete.html → src/authentic2/templates/authentic2/accounts_delete_validation.html
15 15
{% block content %}
16 16
<form method="post">
17 17
  {% csrf_token %}
18
  <p>{% trans "Delete my account and all my personal datas ?" %}</p>
19
  {{ form.as_p }}
20
  <button class="delete-button" name="submit">{% trans "Delete" %}</button>
18
  <p>
19
  {% blocktrans with full_name=user.get_full_name %}
20
  You are about to delete the account of <strong>{{ full_name }}</strong>.
21
  This will remove all related personal data and you won't be able to log in with this account anymore.
22
  {% endblocktrans %}
23
  </p>
24
  <button class="delete-button" name="submit">{% trans "Confirm deletion" %}</button>
21 25
  <button class="cancel-button" name="cancel" formnovalidate>{% trans "Cancel" %}</button>
22 26
</form>
23 27
{% endblock %}
src/authentic2/templates/authentic2/accounts_plain_message.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block page-title %}
5
  {{ block.super }} - {% trans "Account deletion" %}
6
{% endblock %}
7

  
8
{% block content %}
9
  <p>{{ message }}</p>
10
{% endblock %}
src/authentic2/urls.py
44 44
    url(r'^delete/$',
45 45
        login_required(views.DeleteView.as_view()),
46 46
        name='delete_account'),
47
    url(r'validate-deletion/(?P<deletion_token>[\w: -]+)/$',
48
        views.ValidateDeletionView.as_view(),
49
        name='validate_deletion'),
47 50
    url(r'^logged-in/$',
48 51
        views.logged_in,
49 52
        name='logged-in'),
src/authentic2/utils/__init__.py
682 682
    return activate_url
683 683

  
684 684

  
685
def build_deletion_url(request, **kwargs):
686
    data = kwargs.copy()
687
    data['user_pk'] = request.user.pk
688
    deletion_token = signing.dumps(data)
689
    delete_url = request.build_absolute_uri(
690
            reverse('validate_deletion', kwargs={'deletion_token': deletion_token}))
691
    return delete_url
692

  
693

  
685 694
def send_registration_mail(request, email, ou, template_names=None, next_url=None, context=None,
686 695
                           **kwargs):
687 696
    '''Send a registration mail to an user. All given kwargs will be used
......
728 737
                registration_url)
729 738

  
730 739

  
740
def send_account_deletion_code(request, user):
741
    '''Send an account deletion notification code to a user.
742

  
743
       Can raise an smtplib.SMTPException
744
    '''
745
    logger = logging.getLogger(__name__)
746
    deletion_url = build_deletion_url(request)
747
    context = {
748
        'full_name': request.user.get_full_name(),
749
        'user': request.user,
750
        'site': request.get_host(),
751
        'deletion_url': deletion_url}
752
    template_names = [
753
        'authentic2/account_deletion_code']
754
    if user.ou:
755
        template_names.insert(0, 'authentic2/account_deletion_code_%s' % user.ou.slug)
756
    send_templated_mail(user.email, template_names, context, request=request)
757
    logger.info(u'account deletion code sent to %s', user.email)
758

  
759

  
731 760
def send_account_deletion_mail(request, user):
732 761
    '''Send an account deletion notification mail to a user.
733 762

  
src/authentic2/views.py
1060 1060
                                  request=self.request)
1061 1061

  
1062 1062

  
1063
class DeleteView(FormView):
1064
    template_name = 'authentic2/accounts_delete.html'
1065
    success_url = reverse_lazy('auth_logout')
1066
    title = _('Delete account')
1063
registration_completion = valid_token(RegistrationCompletionView.as_view())
1064

  
1067 1065

  
1066
class DeleteView(View):
1068 1067
    def dispatch(self, request, *args, **kwargs):
1069 1068
        if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT:
1070 1069
            return utils.redirect(request, '..')
1071 1070
        return super(DeleteView, self).dispatch(request, *args, **kwargs)
1072 1071

  
1073
    def post(self, request, *args, **kwargs):
1074
        if 'cancel' in request.POST:
1075
            return utils.redirect(request, 'account_management')
1076
        return super(DeleteView, self).post(request, *args, **kwargs)
1072
    def get(self, request, *args, **kwargs):
1073
        utils.send_account_deletion_code(self.request, self.request.user)
1074
        messages.info(request,
1075
                _("An account deletion validation email has been sent to your email address."))
1076
        return utils.redirect(request, 'account_management')
1077 1077

  
1078
    def get_form_class(self):
1079
        if self.request.user.has_usable_password():
1080
            return profile_forms.DeleteAccountForm
1081
        return Form
1082 1078

  
1083
    def get_form_kwargs(self, **kwargs):
1084
        kwargs = super(DeleteView, self).get_form_kwargs(**kwargs)
1085
        if self.request.user.has_usable_password():
1086
            kwargs['user'] = self.request.user
1087
        return kwargs
1079
class ValidateDeletionView(TemplateView):
1080
    template_name = 'authentic2/accounts_delete_validation.html'
1081
    title = _('Confirm account deletion')
1082
    user = None
1088 1083

  
1089
    def form_valid(self, form):
1090
        utils.send_account_deletion_mail(self.request, self.request.user)
1091
        models.DeletedUser.objects.delete_user(self.request.user)
1092
        self.request.user.email += '#%d' % random.randint(1, 10000000)
1093
        self.request.user.email_verified = False
1094
        self.request.user.save(update_fields=['email', 'email_verified'])
1095
        logger.info(u'deletion of account %s requested', self.request.user)
1096
        hooks.call_hooks('event', name='delete-account', user=self.request.user)
1097
        message_template = loader.get_template('authentic2/account_deletion_message.html')
1098
        messages.info(self.request, message_template.render(request=self.request))
1099
        return super(DeleteView, self).form_valid(form)
1084
    def dispatch(self, request, *args, **kwargs):
1085
        error = None
1086
        try:
1087
            deletion_token = signing.loads(kwargs['deletion_token'],
1088
                    max_age=app_settings.A2_DELETION_REQUEST_LIFETIME)
1089
            user_pk = deletion_token['user_pk']
1090
            self.user = get_user_model().objects.get(pk=user_pk)
1091
            # A user account wont be deactived twice
1092
            if not self.user.is_active:
1093
                raise ValidationError(
1094
                    _('This account had previously been deactivated and will be deleted soon.'))
1095
            logger.info('user %s confirmed the deletion of their own account', self.user)
1096
        except signing.SignatureExpired:
1097
            error = _('The account deletion request is too old, try again')
1098
        except signing.BadSignature:
1099
            error = _('The account deletion request is invalid, try again')
1100
        except ValueError:
1101
            error = _('The account deletion request was not on this site, try again')
1102
        except ValidationError as e:
1103
            error = e.message
1104
        except get_user_model().DoesNotExist:
1105
            error = _('This account has previously been deleted.')
1106

  
1107
        if error:
1108
            return render(request, 'authentic2/accounts_plain_message.html',
1109
                    context={'message': error})
1110
        return super(ValidateDeletionView, self).dispatch(request, *args, **kwargs)
1100 1111

  
1101
registration_completion = valid_token(RegistrationCompletionView.as_view())
1112
    def post(self, request, *args, **kwargs):
1113
        if 'cancel' not in request.POST:
1114
            utils.send_account_deletion_mail(self.request, self.user)
1115
            models.DeletedUser.objects.delete_user(self.user)
1116
            self.user.email += '#%d' % random.randint(1, 10000000)
1117
            self.user.email_verified = False
1118
            self.user.save(update_fields=['email', 'email_verified'])
1119
            logger.info(u'deletion of account %s performed', self.user)
1120
            hooks.call_hooks('event', name='delete-account', user=self.user)
1121
            if self.user == request.user:
1122
                # No validation message displayed, as the user will surely
1123
                # notice their own account deletion...
1124
                return utils.redirect(request, 'auth_logout')
1125
            # No real use for cancel_url or next_url here, assuming the link
1126
            # has been received by email.
1127
            return render(request, 'authentic2/accounts_plain_message.html',
1128
                    context={'message': 'Deletion performed.'})
1129
        return utils.redirect(request, '/')
1130

  
1131
    def get_context_data(self, **kwargs):
1132
        ctx = super(ValidateDeletionView, self).get_context_data(**kwargs)
1133
        ctx['user'] = self.user # Not necessarily the user in request
1134
        return ctx
1102 1135

  
1103 1136

  
1104 1137
class RegistrationCompleteView(TemplateView):
tests/test_views.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16
# authentic2
17 17

  
18
from utils import login
18
from utils import login, logout, get_link_from_mail
19 19
import pytest
20 20

  
21 21
from django.core.urlresolvers import reverse
22
from django.utils.html import escape
22 23
from django.utils.six.moves.urllib.parse import urlparse
23 24

  
24 25
from authentic2.custom_user.models import User
......
41 42
    assert simple_user.is_active
42 43
    assert not len(mailoutbox)
43 44
    page = login(app, simple_user, path=reverse('delete_account'))
44
    page.form.set('password', simple_user.username)
45
    assert len(mailoutbox) == 1
46
    link = get_link_from_mail(mailoutbox[0])
47
    assert 'Validate account deletion request on testserver' == mailoutbox[0].subject
48
    assert [simple_user.email] == mailoutbox[0].to
49
    page = app.get(link)
45 50
    # FIXME: webtest does not set the Referer header, so the logout page will always ask for
46 51
    # confirmation under tests
47 52
    response = page.form.submit(name='submit').follow()
48 53
    response = response.form.submit()
49
    assert len(mailoutbox) == 1
50 54
    assert not User.objects.get(pk=simple_user.pk).is_active
55
    assert len(mailoutbox) == 2
56
    assert 'Account deletion on testserver' == mailoutbox[1].subject
57
    assert [simple_user.email] == mailoutbox[0].to
51 58
    assert urlparse(response.location).path == '/'
52 59
    response = response.follow().follow()
53 60
    assert response.request.url.endswith('/login/?next=/')
54 61

  
55 62

  
63
def test_account_delete_when_logged_out(app, simple_user, mailoutbox):
64
    assert simple_user.is_active
65
    assert not len(mailoutbox)
66
    page = login(app, simple_user, path=reverse('delete_account'))
67
    assert len(mailoutbox) == 1
68
    link = get_link_from_mail(mailoutbox[0])
69
    logout(app)
70
    page = app.get(link)
71
    assert 'You are about to delete the account of <strong>%s</strong>.' % \
72
            escape(simple_user.get_full_name()) in page.text
73
    response = page.form.submit(name='submit')
74
    assert not User.objects.get(pk=simple_user.pk).is_active
75
    assert len(mailoutbox) == 2
76
    assert 'Account deletion on testserver' == mailoutbox[1].subject
77
    assert [simple_user.email] == mailoutbox[0].to
78
    assert "Deletion performed" in response.text
79

  
80

  
81
def test_account_delete_by_other_user(app, simple_user, user_ou1, mailoutbox):
82
    assert simple_user.is_active
83
    assert user_ou1.is_active
84
    assert not len(mailoutbox)
85
    page = login(app, simple_user, path=reverse('delete_account'))
86
    assert len(mailoutbox) == 1
87
    link = get_link_from_mail(mailoutbox[0])
88
    logout(app)
89
    login(app, user_ou1, path=reverse('account_management'))
90
    page = app.get(link)
91
    assert 'You are about to delete the account of <strong>%s</strong>.' % \
92
            escape(simple_user.get_full_name()) in page.text
93
    response = page.form.submit(name='submit')
94
    assert not User.objects.get(pk=simple_user.pk).is_active
95
    assert User.objects.get(pk=user_ou1.pk).is_active
96
    assert "Deletion performed" in response.text
97
    assert len(mailoutbox) == 2
98
    assert 'Account deletion on testserver' == mailoutbox[1].subject
99
    assert [simple_user.email] == mailoutbox[0].to
100

  
101

  
102
def test_account_delete_fake_token(app, simple_user, mailoutbox):
103
    response = app.get(reverse('validate_deletion', kwargs={'deletion_token': 'thisismostlikelynotavalidtoken'}))
104
    assert "The account deletion request is invalid, try again" in response.text
105

  
106

  
107
def test_account_delete_expired_token(app, simple_user, mailoutbox, freezer):
108
    freezer.move_to('2019-08-01')
109
    page = login(app, simple_user, path=reverse('delete_account'))
110
    freezer.move_to('2019-08-04') # Too late...
111
    link = get_link_from_mail(mailoutbox[0])
112
    response = app.get(link)
113
    assert "The account deletion request is too old, try again" in response.text
114

  
115

  
116
def test_account_delete_valid_token_unexistent_user(app, simple_user, mailoutbox):
117
    page = login(app, simple_user, path=reverse('delete_account'))
118
    link = get_link_from_mail(mailoutbox[0])
119
    simple_user.delete()
120
    response = app.get(link)
121
    assert 'This account has previously been deleted.' in response.text
122

  
123

  
124
def test_account_delete_valid_token_inactive_user(app, simple_user, mailoutbox):
125
    page = login(app, simple_user, path=reverse('delete_account'))
126
    link = get_link_from_mail(mailoutbox[0])
127
    simple_user.is_active = False
128
    simple_user.save()
129
    response = app.get(link)
130
    assert "This account had previously been deactivated" in response.text
131

  
132

  
56 133
def test_login_invalid_next(app):
57 134
    app.get(reverse('auth_login') + '?next=plop')
58 135

  
59
-