add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7
This commit is contained in:
parent
f2569b6424
commit
a0d42092e3
|
@ -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.'))
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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