0001-views-ask-for-reauthentication-when-deleting-account.patch
src/authentic2/forms/profile.py | ||
---|---|---|
24 | 24 |
from .utils import NextUrlFormMixin |
25 | 25 | |
26 | 26 | |
27 |
class DeleteAccountForm(forms.Form): |
|
28 |
password = forms.CharField(widget=forms.PasswordInput, label=_("Password")) |
|
29 | ||
30 |
def __init__(self, *args, **kwargs): |
|
31 |
self.user = kwargs.pop('user') |
|
32 |
super(DeleteAccountForm, self).__init__(*args, **kwargs) |
|
33 | ||
34 |
def clean_password(self): |
|
35 |
password = self.cleaned_data.get('password') |
|
36 |
if password and not self.user.check_password(password): |
|
37 |
raise forms.ValidationError(ugettext('Password is invalid')) |
|
38 |
return password |
|
39 | ||
40 | ||
41 | 27 |
class EmailChangeFormNoPassword(forms.Form): |
42 | 28 |
email = forms.EmailField(label=_('New email')) |
43 | 29 |
src/authentic2/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import time |
|
17 | 18 |
import collections |
18 | 19 |
import logging |
19 | 20 |
import random |
... | ... | |
49 | 50 |
from django.views.generic.edit import CreateView |
50 | 51 |
from django.forms import CharField, Form |
51 | 52 |
from django.core.urlresolvers import reverse_lazy |
53 |
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired |
|
52 | 54 |
from django.http import HttpResponseBadRequest |
53 | 55 | |
54 | 56 |
from . import (utils, app_settings, compat, decorators, constants, |
... | ... | |
1082 | 1084 |
template_name = 'authentic2/accounts_delete.html' |
1083 | 1085 |
success_url = reverse_lazy('auth_logout') |
1084 | 1086 |
title = _('Delete account') |
1087 |
last_authentication_timeout = 60 |
|
1088 |
token_timeout = 10 * 60 |
|
1085 | 1089 | |
1086 | 1090 |
def dispatch(self, request, *args, **kwargs): |
1087 | 1091 |
if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT: |
1088 | 1092 |
return utils.redirect(request, '..') |
1093 |
token = self.request.GET.get('token') |
|
1094 |
signer = TimestampSigner(salt='delete-account') |
|
1095 |
try: |
|
1096 |
token_valid = token and signer.unsign(token, max_age=self.token_timeout) == str(self.request.user.uuid) |
|
1097 |
except (BadSignature, SignatureExpired): |
|
1098 |
token_valid = False |
|
1099 |
if not token_valid: |
|
1100 |
if self.has_recent_authentication(request): |
|
1101 |
return utils.redirect(request, request.path, params={'token': signer.sign(str(self.request.user.uuid))}) |
|
1102 |
else: |
|
1103 |
messages.info(request, |
|
1104 |
_('Your last authentication is too old, ' |
|
1105 |
'you need to reauthenticate to delete your account')) |
|
1106 |
return utils.login_require(request) |
|
1089 | 1107 |
return super(DeleteView, self).dispatch(request, *args, **kwargs) |
1090 | 1108 | |
1109 |
def has_recent_authentication(self, request): |
|
1110 |
delta = time.time() - utils.last_authentication_event(request=request)['when'] |
|
1111 |
return delta < self.last_authentication_timeout |
|
1112 | ||
1091 | 1113 |
def post(self, request, *args, **kwargs): |
1092 | 1114 |
if 'cancel' in request.POST: |
1093 | 1115 |
return utils.redirect(request, 'account_management') |
1094 | 1116 |
return super(DeleteView, self).post(request, *args, **kwargs) |
1095 | 1117 | |
1096 | 1118 |
def get_form_class(self): |
1097 |
if self.request.user.has_usable_password(): |
|
1098 |
return profile_forms.DeleteAccountForm |
|
1099 | 1119 |
return Form |
1100 | 1120 | |
1101 |
def get_form_kwargs(self, **kwargs): |
|
1102 |
kwargs = super(DeleteView, self).get_form_kwargs(**kwargs) |
|
1103 |
if self.request.user.has_usable_password(): |
|
1104 |
kwargs['user'] = self.request.user |
|
1105 |
return kwargs |
|
1106 | ||
1107 | 1121 |
def form_valid(self, form): |
1108 | 1122 |
utils.send_account_deletion_mail(self.request, self.request.user) |
1109 | 1123 |
models.DeletedUser.objects.delete_user(self.request.user) |
tests/test_views.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 |
# authentic2 |
17 | 17 | |
18 |
import time |
|
19 |
import datetime |
|
20 | ||
18 | 21 |
from utils import login |
19 | 22 |
import pytest |
20 | 23 | |
... | ... | |
40 | 43 |
def test_account_delete(app, simple_user, mailoutbox): |
41 | 44 |
assert simple_user.is_active |
42 | 45 |
assert not len(mailoutbox) |
43 |
page = login(app, simple_user, path=reverse('delete_account')) |
|
44 |
page.form.set('password', simple_user.username) |
|
45 |
# FIXME: webtest does not set the Referer header, so the logout page will always ask for |
|
46 |
# confirmation under tests |
|
47 |
response = page.form.submit(name='submit').follow() |
|
46 |
response = login(app, simple_user, path=reverse('delete_account')).follow() |
|
47 |
response = response.form.submit(name='submit').follow() |
|
48 |
response = response.form.submit() |
|
49 |
assert len(mailoutbox) == 1 |
|
50 |
assert not User.objects.get(pk=simple_user.pk).is_active |
|
51 |
assert urlparse(response.location).path == '/' |
|
52 |
response = response.follow().follow() |
|
53 |
assert response.request.url.endswith('/login/?next=/') |
|
54 | ||
55 | ||
56 |
def test_account_delete_expired_authentication(app, simple_user, mailoutbox, freezer): |
|
57 |
t = time.time() |
|
58 |
assert simple_user.is_active |
|
59 |
assert not len(mailoutbox) |
|
60 |
response = login(app, simple_user, path='/accounts/') |
|
61 | ||
62 |
# move 80 seconds in the future, so that last authentication is at least 60 seconds old |
|
63 |
freezer.move_to(datetime.timedelta(seconds=80)) |
|
64 |
assert time.time() - t > 60 |
|
65 | ||
66 |
# check we are redirected to login page |
|
67 |
response = response.click('Delete account') |
|
68 |
assert urlparse(response.location).path == '/login/' |
|
69 |
response = response.follow() |
|
70 |
assert 'you need to reauthenticate' in response.text |
|
71 | ||
72 |
response.form.set('username', simple_user.username) |
|
73 |
response.form.set('password', simple_user.username) |
|
74 |
response = response.form.submit(name='login-password-submit') |
|
75 | ||
76 |
# check we are redirected to the delete account page |
|
77 |
assert urlparse(response.location).path == '/accounts/delete/' |
|
78 |
response = response.follow() |
|
79 | ||
80 |
# check a token is added |
|
81 |
assert 'token' in response.location |
|
82 |
response = response.follow() |
|
83 | ||
84 |
# check delete view is now working |
|
85 |
response = response.form.submit(name='submit').follow() |
|
48 | 86 |
response = response.form.submit() |
49 | 87 |
assert len(mailoutbox) == 1 |
50 | 88 |
assert not User.objects.get(pk=simple_user.pk).is_active |
51 |
- |