From a0d42092e359e8b90ae442affa224d6e3b7a13e7 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 25 Feb 2019 20:46:23 +0100 Subject: [PATCH] add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7 --- .../templates/administration/user/list.html | 2 ++ passbook/admin/urls.py | 2 ++ passbook/admin/views/users.py | 21 +++++++++++-- passbook/core/auth/factors/password.py | 4 ++- passbook/core/migrations/0012_nonce.py | 31 +++++++++++++++++++ passbook/core/models.py | 20 ++++++++++++ passbook/core/templates/login/base.html | 2 +- passbook/core/urls.py | 3 ++ passbook/core/views/authentication.py | 21 +++++++++++-- .../migrations/0002_auto_20190225_1912.py | 17 ++++++++++ passbook/lib/default.yml | 2 +- 11 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 passbook/core/migrations/0012_nonce.py create mode 100644 passbook/hibp_policy/migrations/0002_auto_20190225_1912.py diff --git a/passbook/admin/templates/administration/user/list.html b/passbook/admin/templates/administration/user/list.html index 26f630ad9..f198d56a0 100644 --- a/passbook/admin/templates/administration/user/list.html +++ b/passbook/admin/templates/administration/user/list.html @@ -31,6 +31,8 @@ href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %} {% trans 'Delete' %} + {% trans 'Reset Password' %} {% endfor %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 3e86f40c6..e2d3a6236 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -56,6 +56,8 @@ urlpatterns = [ users.UserUpdateView.as_view(), name='user-update'), path('users//delete/', users.UserDeleteView.as_view(), name='user-delete'), + path('users//reset/', + users.UserPasswordResetView.as_view(), name='user-password-reset'), # Audit Log path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), # Groups diff --git a/passbook/admin/views/users.py b/passbook/admin/views/users.py index e0d610eba..a6bfc9e85 100644 --- a/passbook/admin/views/users.py +++ b/passbook/admin/views/users.py @@ -1,12 +1,15 @@ """passbook User administration""" +from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext as _ +from django.views import View from django.views.generic import DeleteView, ListView, UpdateView from passbook.admin.mixins import AdminRequiredMixin from passbook.core.forms.users import UserDetailForm -from passbook.core.models import User +from passbook.core.models import Nonce, User class UserListView(AdminRequiredMixin, ListView): @@ -34,3 +37,17 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): success_url = reverse_lazy('passbook_admin:users') success_message = _('Successfully updated User') + + +class UserPasswordResetView(AdminRequiredMixin, View): + """Get Password reset link for user""" + + # pylint: disable=invalid-name + def get(self, request, pk): + """Create nonce for user and return link""" + user = get_object_or_404(User, pk=pk) + nonce = Nonce.objects.create(user=user) + link = request.build_absolute_uri(reverse( + 'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid})) + messages.success(request, _('Password reset link:
%(link)s
' % {'link': link})) + return redirect('passbook_admin:users') diff --git a/passbook/core/auth/factors/password.py b/passbook/core/auth/factors/password.py index 2d6d1514e..b9072bb08 100644 --- a/passbook/core/auth/factors/password.py +++ b/passbook/core/auth/factors/password.py @@ -12,6 +12,7 @@ from django.views.generic import FormView from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.view import AuthenticationView from passbook.core.forms.authentication import PasswordFactorForm +from passbook.core.models import Nonce from passbook.lib.config import CONFIG LOGGER = getLogger(__name__) @@ -29,7 +30,8 @@ class PasswordFactor(FormView, AuthenticationFactor): def get(self, request, *args, **kwargs): if 'password-forgotten' in request.GET: - # TODO: Save nonce key in database for password reset + nonce = Nonce.objects.create(user=self.pending_user) + LOGGER.debug("DEBUG %s", str(nonce.uuid)) # TODO: Send email to user self.authenticator.cleanup() messages.success(request, _('Check your E-Mails for a password reset link.')) diff --git a/passbook/core/migrations/0012_nonce.py b/passbook/core/migrations/0012_nonce.py new file mode 100644 index 000000000..335ffb3db --- /dev/null +++ b/passbook/core/migrations/0012_nonce.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.7 on 2019-02-25 19:12 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import passbook.core.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_core', '0011_auto_20190225_1438'), + ] + + operations = [ + migrations.CreateModel( + name='Nonce', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Nonce', + 'verbose_name_plural': 'Nonces', + }, + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 25f5d81b6..e5fc6a6d9 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -1,5 +1,6 @@ """passbook core models""" import re +from datetime import timedelta from logging import getLogger from random import SystemRandom from time import sleep @@ -18,6 +19,11 @@ from passbook.lib.models import CreatedUpdatedModel, UUIDModel LOGGER = getLogger(__name__) + +def default_nonce_duration(): + """Default duration a Nonce is valid""" + return now() + timedelta(hours=4) + class Group(UUIDModel): """Custom Group model which supports a basic hierarchy""" @@ -399,3 +405,17 @@ class Invitation(UUIDModel): verbose_name = _('Invitation') verbose_name_plural = _('Invitations') + +class Nonce(UUIDModel): + """One-time link for password resets/signup-confirmations""" + + expires = models.DateTimeField(default=default_nonce_duration) + user = models.ForeignKey('User', on_delete=models.CASCADE) + + def __str__(self): + return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires) + + class Meta: + + verbose_name = _('Nonce') + verbose_name_plural = _('Nonces') diff --git a/passbook/core/templates/login/base.html b/passbook/core/templates/login/base.html index cf9797e05..09198a160 100644 --- a/passbook/core/templates/login/base.html +++ b/passbook/core/templates/login/base.html @@ -29,7 +29,7 @@