add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7
This commit is contained in:
		| @ -31,6 +31,8 @@ | ||||
|                         href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> | ||||
|                     <a class="btn btn-default btn-sm" | ||||
|                         href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> | ||||
|                     <a class="btn btn-default btn-sm" | ||||
|                         href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a> | ||||
|                 </td> | ||||
|             </tr> | ||||
|             {% endfor %} | ||||
|  | ||||
| @ -56,6 +56,8 @@ urlpatterns = [ | ||||
|          users.UserUpdateView.as_view(), name='user-update'), | ||||
|     path('users/<int:pk>/delete/', | ||||
|          users.UserDeleteView.as_view(), name='user-delete'), | ||||
|     path('users/<int:pk>/reset/', | ||||
|          users.UserPasswordResetView.as_view(), name='user-password-reset'), | ||||
|     # Audit Log | ||||
|     path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), | ||||
|     # Groups | ||||
|  | ||||
| @ -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: <pre>%(link)s</pre>' % {'link': link})) | ||||
|         return redirect('passbook_admin:users') | ||||
|  | ||||
| @ -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.')) | ||||
|  | ||||
							
								
								
									
										31
									
								
								passbook/core/migrations/0012_nonce.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								passbook/core/migrations/0012_nonce.py
									
									
									
									
									
										Normal file
									
								
							| @ -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', | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -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') | ||||
|  | ||||
| @ -29,7 +29,7 @@ | ||||
| <div class="login-pf-page"> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row"> | ||||
|             <div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4"> | ||||
|             <div class="col-sm-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4"> | ||||
|                 <header class="login-pf-page-header"> | ||||
|                     <img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" | ||||
|                         alt="passbook logo" /> | ||||
|  | ||||
| @ -19,7 +19,10 @@ core_urls = [ | ||||
|     path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), | ||||
|     path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'), | ||||
|     path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'), | ||||
|     # path('auth/sign_up/<uuid:nonce>/confirm/', , name='auth-sign-up-confirm'), | ||||
|     path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'), | ||||
|     path('auth/password/reset/<uuid:nonce>/', authentication.PasswordResetView.as_view(), | ||||
|          name='auth-password-reset'), | ||||
|     path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'), | ||||
|     path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'), | ||||
|     # User views | ||||
|  | ||||
| @ -3,17 +3,17 @@ from logging import getLogger | ||||
| from typing import Dict | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth import logout | ||||
| from django.contrib.auth import login, logout | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import redirect, reverse | ||||
| from django.shortcuts import get_object_or_404, redirect, reverse | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views import View | ||||
| from django.views.generic import FormView | ||||
|  | ||||
| from passbook.core.auth.view import AuthenticationView | ||||
| from passbook.core.forms.authentication import LoginForm, SignUpForm | ||||
| from passbook.core.models import Invitation, Source, User | ||||
| from passbook.core.models import Invitation, Nonce, Source, User | ||||
| from passbook.core.signals import invitation_used, user_signed_up | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
| @ -190,3 +190,18 @@ class SignUpView(UserPassesTestMixin, FormView): | ||||
|         #     Create Account Confirmation UUID | ||||
|         #     AccountConfirmation.objects.create(user=new_user) | ||||
|         return new_user | ||||
|  | ||||
| class PasswordResetView(View): | ||||
|     """Temporarily authenticate User and allow them to reset their password""" | ||||
|  | ||||
|     def get(self, request, nonce): | ||||
|         """Authenticate user with nonce and redirect to password change view""" | ||||
|         # 3. (Optional) Trap user in password change view | ||||
|         nonce = get_object_or_404(Nonce, uuid=nonce) | ||||
|         # Workaround: hardcoded reference to ModelBackend, needs testing | ||||
|         nonce.user.backend = 'django.contrib.auth.backends.ModelBackend' | ||||
|         login(request, nonce.user) | ||||
|         nonce.delete() | ||||
|         messages.success(request, _(('Temporarily authenticated with Nonce, ' | ||||
|                                      'please change your password'))) | ||||
|         return redirect('passbook_core:user-change-password') | ||||
|  | ||||
							
								
								
									
										17
									
								
								passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| # Generated by Django 2.1.7 on 2019-02-25 19:12 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_hibp_policy', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name='haveibeenpwendpolicy', | ||||
|             options={'verbose_name': 'have i been pwned Policy', 'verbose_name_plural': 'have i been pwned Policies'}, | ||||
|         ), | ||||
|     ] | ||||
| @ -61,7 +61,7 @@ passbook: | ||||
|   # Specify which fields can be used to authenticate. Can be any combination of `username` and `email` | ||||
|   uid_fields: | ||||
|     - username | ||||
|     - e-mail | ||||
|     - email | ||||
|   # Factors to load | ||||
|   factors: | ||||
|    - passbook.core.auth.factors.backend | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer