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>
|
href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||||
<a class="btn btn-default btn-sm"
|
<a class="btn btn-default btn-sm"
|
||||||
href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -56,6 +56,8 @@ urlpatterns = [
|
||||||
users.UserUpdateView.as_view(), name='user-update'),
|
users.UserUpdateView.as_view(), name='user-update'),
|
||||||
path('users/<int:pk>/delete/',
|
path('users/<int:pk>/delete/',
|
||||||
users.UserDeleteView.as_view(), name='user-delete'),
|
users.UserDeleteView.as_view(), name='user-delete'),
|
||||||
|
path('users/<int:pk>/reset/',
|
||||||
|
users.UserPasswordResetView.as_view(), name='user-password-reset'),
|
||||||
# Audit Log
|
# Audit Log
|
||||||
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
|
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
|
||||||
# Groups
|
# Groups
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
"""passbook User administration"""
|
"""passbook User administration"""
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
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.utils.translation import ugettext as _
|
||||||
|
from django.views import View
|
||||||
from django.views.generic import DeleteView, ListView, UpdateView
|
from django.views.generic import DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
from passbook.core.forms.users import UserDetailForm
|
from passbook.core.forms.users import UserDetailForm
|
||||||
from passbook.core.models import User
|
from passbook.core.models import Nonce, User
|
||||||
|
|
||||||
|
|
||||||
class UserListView(AdminRequiredMixin, ListView):
|
class UserListView(AdminRequiredMixin, ListView):
|
||||||
|
@ -34,3 +37,17 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||||
|
|
||||||
success_url = reverse_lazy('passbook_admin:users')
|
success_url = reverse_lazy('passbook_admin:users')
|
||||||
success_message = _('Successfully updated User')
|
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.factor import AuthenticationFactor
|
||||||
from passbook.core.auth.view import AuthenticationView
|
from passbook.core.auth.view import AuthenticationView
|
||||||
from passbook.core.forms.authentication import PasswordFactorForm
|
from passbook.core.forms.authentication import PasswordFactorForm
|
||||||
|
from passbook.core.models import Nonce
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
@ -29,7 +30,8 @@ class PasswordFactor(FormView, AuthenticationFactor):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if 'password-forgotten' in request.GET:
|
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
|
# TODO: Send email to user
|
||||||
self.authenticator.cleanup()
|
self.authenticator.cleanup()
|
||||||
messages.success(request, _('Check your E-Mails for a password reset link.'))
|
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"""
|
"""passbook core models"""
|
||||||
import re
|
import re
|
||||||
|
from datetime import timedelta
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
@ -18,6 +19,11 @@ from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def default_nonce_duration():
|
||||||
|
"""Default duration a Nonce is valid"""
|
||||||
|
return now() + timedelta(hours=4)
|
||||||
|
|
||||||
class Group(UUIDModel):
|
class Group(UUIDModel):
|
||||||
"""Custom Group model which supports a basic hierarchy"""
|
"""Custom Group model which supports a basic hierarchy"""
|
||||||
|
|
||||||
|
@ -399,3 +405,17 @@ class Invitation(UUIDModel):
|
||||||
|
|
||||||
verbose_name = _('Invitation')
|
verbose_name = _('Invitation')
|
||||||
verbose_name_plural = _('Invitations')
|
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="login-pf-page">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<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">
|
<header class="login-pf-page-header">
|
||||||
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
|
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
|
||||||
alt="passbook logo" />
|
alt="passbook logo" />
|
||||||
|
|
|
@ -19,7 +19,10 @@ core_urls = [
|
||||||
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
||||||
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
|
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/', 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/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/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||||
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
|
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||||
# User views
|
# User views
|
||||||
|
|
|
@ -3,17 +3,17 @@ from logging import getLogger
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from django.contrib import messages
|
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.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.http import HttpRequest, HttpResponse
|
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.utils.translation import ugettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from passbook.core.auth.view import AuthenticationView
|
from passbook.core.auth.view import AuthenticationView
|
||||||
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
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.core.signals import invitation_used, user_signed_up
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
@ -190,3 +190,18 @@ class SignUpView(UserPassesTestMixin, FormView):
|
||||||
# Create Account Confirmation UUID
|
# Create Account Confirmation UUID
|
||||||
# AccountConfirmation.objects.create(user=new_user)
|
# AccountConfirmation.objects.create(user=new_user)
|
||||||
return 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`
|
# Specify which fields can be used to authenticate. Can be any combination of `username` and `email`
|
||||||
uid_fields:
|
uid_fields:
|
||||||
- username
|
- username
|
||||||
- e-mail
|
- email
|
||||||
# Factors to load
|
# Factors to load
|
||||||
factors:
|
factors:
|
||||||
- passbook.core.auth.factors.backend
|
- passbook.core.auth.factors.backend
|
||||||
|
|
Reference in New Issue