mirror of
git://git.yoctoproject.org/layerindex-web.git
synced 2025-07-19 20:59:01 +02:00
Add user security questions
Add user security questions upon registration as extra authentication for password reset. Three unique security questions must be chosen and answered. Answers are then stored in the database with the same hashing algorithm as the users's password. On password reset, users get two chances to get two out of three security questions answered correctly. After a second failure their account is locked and email is sent to the admin. The same template is shown for the axes lockout. Super user cannot reset their password until they set security questions. Users can update their security questions or add them if they weren't originally set (in the case of super user) in Edit Profile. Signed-off-by: Amber Elliot <amber.n.elliot@intel.com>
This commit is contained in:
parent
0f3b3e42a6
commit
9a9bbeb8b6
|
@ -279,6 +279,7 @@ CACHES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AXES_CACHE = "axes_cache"
|
AXES_CACHE = "axes_cache"
|
||||||
|
AXES_LOCKOUT_TEMPLATE = "registration/account_lockout.html"
|
||||||
|
|
||||||
# Full path to directory to store logs for dynamically executed tasks
|
# Full path to directory to store logs for dynamically executed tasks
|
||||||
TASK_LOG_DIR = "/tmp/layerindex-task-logs"
|
TASK_LOG_DIR = "/tmp/layerindex-task-logs"
|
||||||
|
|
|
@ -236,3 +236,6 @@ admin.site.register(ClassicRecipe, ClassicRecipeAdmin)
|
||||||
admin.site.register(ComparisonRecipeUpdate, ComparisonRecipeUpdateAdmin)
|
admin.site.register(ComparisonRecipeUpdate, ComparisonRecipeUpdateAdmin)
|
||||||
admin.site.register(PythonEnvironment)
|
admin.site.register(PythonEnvironment)
|
||||||
admin.site.register(SiteNotice)
|
admin.site.register(SiteNotice)
|
||||||
|
admin.site.register(SecurityQuestion)
|
||||||
|
admin.site.register(SecurityQuestionAnswer)
|
||||||
|
admin.site.register(UserProfile)
|
||||||
|
|
|
@ -4,19 +4,58 @@
|
||||||
#
|
#
|
||||||
# Licensed under the MIT license, see COPYING.MIT for details
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from captcha.fields import CaptchaField
|
from captcha.fields import CaptchaField
|
||||||
from django_registration.forms import RegistrationForm
|
from django import forms
|
||||||
from django.contrib.auth.forms import PasswordResetForm
|
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django_registration.forms import RegistrationForm
|
||||||
|
|
||||||
|
from layerindex.models import SecurityQuestion
|
||||||
|
|
||||||
|
|
||||||
class CaptchaRegistrationForm(RegistrationForm):
|
class CaptchaRegistrationForm(RegistrationForm):
|
||||||
captcha = CaptchaField(label='Verification', help_text='Please enter the letters displayed for verification purposes', error_messages={'invalid':'Incorrect entry, please try again'})
|
captcha = CaptchaField(label='Verification',
|
||||||
|
help_text='Please enter the letters displayed for verification purposes',
|
||||||
|
error_messages={'invalid':'Incorrect entry, please try again'})
|
||||||
|
security_question_1 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_1 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True)
|
||||||
|
security_question_2 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_2 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True)
|
||||||
|
security_question_3 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_3 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CaptchaRegistrationForm, self ).__init__(*args, **kwargs)
|
||||||
|
self.fields['security_question_1'].initial=SecurityQuestion.objects.all()[0]
|
||||||
|
self.fields['security_question_2'].initial=SecurityQuestion.objects.all()[1]
|
||||||
|
self.fields['security_question_3'].initial=SecurityQuestion.objects.all()[2]
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(CaptchaRegistrationForm, self).clean()
|
||||||
|
security_question_1 = self.cleaned_data["security_question_1"]
|
||||||
|
security_question_2 = self.cleaned_data["security_question_2"]
|
||||||
|
security_question_3 = self.cleaned_data["security_question_3"]
|
||||||
|
if security_question_1 == security_question_2:
|
||||||
|
raise forms.ValidationError({'security_question_2': ["Questions may only be chosen once."]})
|
||||||
|
if security_question_1 == security_question_3 or security_question_2 == security_question_3:
|
||||||
|
raise forms.ValidationError({'security_question_3': ["Questions may only be chosen once."]})
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
User.USERNAME_FIELD,
|
||||||
|
'email',
|
||||||
|
'password1',
|
||||||
|
'password2',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CaptchaPasswordResetForm(PasswordResetForm):
|
class CaptchaPasswordResetForm(PasswordResetForm):
|
||||||
captcha = CaptchaField(label='Verification', help_text='Please enter the letters displayed for verification purposes', error_messages={'invalid':'Incorrect entry, please try again'})
|
captcha = CaptchaField(label='Verification',
|
||||||
|
help_text='Please enter the letters displayed for verification purposes',
|
||||||
|
error_messages={'invalid':'Incorrect entry, please try again'})
|
||||||
|
|
||||||
|
|
||||||
class DeleteAccountForm(forms.ModelForm):
|
class DeleteAccountForm(forms.ModelForm):
|
||||||
|
@ -32,3 +71,65 @@ class DeleteAccountForm(forms.ModelForm):
|
||||||
if not check_password(confirm_password, self.instance.password):
|
if not check_password(confirm_password, self.instance.password):
|
||||||
self.add_error('confirm_password', 'Password does not match.')
|
self.add_error('confirm_password', 'Password does not match.')
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityQuestionPasswordResetForm(SetPasswordForm):
|
||||||
|
correct_answers = 0
|
||||||
|
security_question_1 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_1 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True,)
|
||||||
|
security_question_2 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_2 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True)
|
||||||
|
security_question_3 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_3 = forms.CharField(widget=forms.TextInput(), label='Answer', required=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SecurityQuestionPasswordResetForm, self ).__init__(*args, **kwargs)
|
||||||
|
self.fields['security_question_1'].initial=SecurityQuestion.objects.all()[0]
|
||||||
|
self.fields['security_question_2'].initial=SecurityQuestion.objects.all()[1]
|
||||||
|
self.fields['security_question_3'].initial=SecurityQuestion.objects.all()[2]
|
||||||
|
|
||||||
|
def clean_answer_util(self, question, answer):
|
||||||
|
form_security_question = self.cleaned_data[question]
|
||||||
|
form_answer = self.cleaned_data[answer].replace(" ", "").lower()
|
||||||
|
# Attempt to get the user's hashed answer to the security question. If the user didn't choose
|
||||||
|
# this security question, throw an exception.
|
||||||
|
try:
|
||||||
|
question_answer = self.user.userprofile.securityquestionanswer_set.filter(
|
||||||
|
security_question__question=form_security_question)[0]
|
||||||
|
except IndexError as e:
|
||||||
|
raise forms.ValidationError("Security question is incorrect.")
|
||||||
|
user_answer = question_answer.answer
|
||||||
|
|
||||||
|
# Compare input answer to hashed database answer.
|
||||||
|
if check_password(form_answer, user_answer):
|
||||||
|
self.correct_answers = self.correct_answers+1
|
||||||
|
return form_answer
|
||||||
|
|
||||||
|
def clean_answer_1(self):
|
||||||
|
return self.clean_answer_util("security_question_1", "answer_1")
|
||||||
|
|
||||||
|
def clean_answer_2(self):
|
||||||
|
return self.clean_answer_util("security_question_2", "answer_2")
|
||||||
|
|
||||||
|
def clean_answer_3(self):
|
||||||
|
return self.clean_answer_util("security_question_3", "answer_3")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# We require two correct security questions. If less than two are correct, the user gets
|
||||||
|
# one additional attempt before their account is locked out.
|
||||||
|
answer_attempts = self.user.userprofile.answer_attempts
|
||||||
|
if self.correct_answers < 2:
|
||||||
|
if answer_attempts == 0:
|
||||||
|
self.user.userprofile.answer_attempts = self.user.userprofile.answer_attempts + 1
|
||||||
|
self.user.userprofile.save()
|
||||||
|
raise forms.ValidationError("One or more security answers are incorrect.", code="incorrect_answers")
|
||||||
|
else :
|
||||||
|
# Reset answer attempts to 0 and throw error to lock account.
|
||||||
|
self.user.userprofile.answer_attempts = 0
|
||||||
|
self.user.userprofile.save()
|
||||||
|
raise forms.ValidationError("Too many attempts! Your account has been locked. "
|
||||||
|
"Please contact your admin.", code="account_locked")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.user.userprofile.answer_attempts = 0
|
||||||
|
self.user.userprofile.save()
|
||||||
|
|
|
@ -4,20 +4,56 @@
|
||||||
#
|
#
|
||||||
# Licensed under the MIT license, see COPYING.MIT for details
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
from django_registration.backends.activation.views import RegistrationView
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.views import PasswordResetView
|
from django.contrib.auth.views import (PasswordResetConfirmView,
|
||||||
from layerindex.auth_forms import CaptchaRegistrationForm, CaptchaPasswordResetForm, DeleteAccountForm
|
PasswordResetView)
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django_registration import signals
|
||||||
|
from django_registration.backends.activation.views import RegistrationView
|
||||||
|
|
||||||
|
from layerindex.auth_forms import (CaptchaPasswordResetForm,
|
||||||
|
CaptchaRegistrationForm, DeleteAccountForm,
|
||||||
|
SecurityQuestionPasswordResetForm)
|
||||||
|
|
||||||
|
from .models import SecurityQuestion, SecurityQuestionAnswer, UserProfile
|
||||||
|
from . import tasks
|
||||||
|
import settings
|
||||||
|
|
||||||
class CaptchaRegistrationView(RegistrationView):
|
class CaptchaRegistrationView(RegistrationView):
|
||||||
form_class = CaptchaRegistrationForm
|
form_class = CaptchaRegistrationForm
|
||||||
|
|
||||||
|
def register(self, form):
|
||||||
|
new_user = self.create_inactive_user(form)
|
||||||
|
signals.user_registered.send(
|
||||||
|
sender=self.__class__,
|
||||||
|
user=new_user,
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add security question answers to the database
|
||||||
|
security_question_1 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_1"))
|
||||||
|
security_question_2 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_2"))
|
||||||
|
security_question_3 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_3"))
|
||||||
|
answer_1 = form.cleaned_data.get("answer_1").replace(" ", "").lower()
|
||||||
|
answer_2 = form.cleaned_data.get("answer_2").replace(" ", "").lower()
|
||||||
|
answer_3 = form.cleaned_data.get("answer_3").replace(" ", "").lower()
|
||||||
|
|
||||||
|
user = UserProfile.objects.create(user=new_user)
|
||||||
|
# Answers are hashed using Django's password hashing function make_password()
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_1,
|
||||||
|
answer=make_password(answer_1))
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_2,
|
||||||
|
answer=make_password(answer_2))
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_3,
|
||||||
|
answer=make_password(answer_3))
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CaptchaRegistrationView, self).get_context_data(**kwargs)
|
context = super(CaptchaRegistrationView, self).get_context_data(**kwargs)
|
||||||
form = context['form']
|
form = context['form']
|
||||||
|
@ -61,3 +97,39 @@ def delete_account_view(request, template_name):
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetSecurityQuestions(PasswordResetConfirmView):
|
||||||
|
form_class = SecurityQuestionPasswordResetForm
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.user.userprofile
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
return HttpResponseRedirect(reverse('password_reset_fail'))
|
||||||
|
if not self.user.is_active:
|
||||||
|
return HttpResponseRedirect(reverse('account_lockout'))
|
||||||
|
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = self.form_class(data=request.POST, user=self.user)
|
||||||
|
form.is_valid()
|
||||||
|
for error in form.non_field_errors().as_data():
|
||||||
|
if error.code == "account_locked":
|
||||||
|
# Deactivate user's account.
|
||||||
|
self.user.is_active = False
|
||||||
|
self.user.save()
|
||||||
|
# Send admin an email that user is locked out.
|
||||||
|
site_name = get_current_site(request).name
|
||||||
|
subject = "User account locked on " + site_name
|
||||||
|
text_content = "User " + self.user.username + " has been locked out on " + site_name + "."
|
||||||
|
admins = settings.ADMINS
|
||||||
|
from_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
tasks.send_email.apply_async((subject, text_content, from_email, [a[1] for a in admins]))
|
||||||
|
return HttpResponseRedirect(reverse('account_lockout'))
|
||||||
|
|
||||||
|
if error.code == "incorrect_answers":
|
||||||
|
# User has failed first attempt at answering questions, give them another try.
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
# layerindex-web - form definitions
|
# layerindex-web - form definitions
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013 Intel Corporation
|
# Copyright (C) 2013, 2016-2019 Intel Corporation
|
||||||
#
|
#
|
||||||
# Licensed under the MIT license, see COPYING.MIT for details
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from layerindex.models import LayerItem, LayerBranch, LayerMaintainer, LayerNote, RecipeChangeset, RecipeChange, ClassicRecipe
|
|
||||||
from django import forms
|
|
||||||
from django.core.validators import URLValidator, RegexValidator, EmailValidator
|
|
||||||
from django_registration.validators import ReservedNameValidator, DEFAULT_RESERVED_NAMES, validate_confusables
|
|
||||||
from django.forms.models import inlineformset_factory, modelformset_factory
|
|
||||||
from captcha.fields import CaptchaField
|
from captcha.fields import CaptchaField
|
||||||
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
import re
|
from django.core.validators import EmailValidator, RegexValidator, URLValidator
|
||||||
|
from django.forms.models import inlineformset_factory, modelformset_factory
|
||||||
|
from django_registration.forms import RegistrationForm
|
||||||
|
from django_registration.validators import (DEFAULT_RESERVED_NAMES,
|
||||||
|
ReservedNameValidator,
|
||||||
|
validate_confusables)
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
|
from layerindex.models import (Branch, ClassicRecipe,
|
||||||
|
LayerBranch, LayerItem, LayerMaintainer,
|
||||||
|
LayerNote, RecipeChange, RecipeChangeset,
|
||||||
|
SecurityQuestion, UserProfile)
|
||||||
|
|
||||||
|
|
||||||
class StyledForm(forms.Form):
|
class StyledForm(forms.Form):
|
||||||
|
@ -181,11 +189,31 @@ class EditNoteForm(StyledModelForm):
|
||||||
|
|
||||||
class EditProfileForm(StyledModelForm):
|
class EditProfileForm(StyledModelForm):
|
||||||
captcha = CaptchaField(label='Verification', help_text='Please enter the letters displayed for verification purposes', error_messages={'invalid':'Incorrect entry, please try again'})
|
captcha = CaptchaField(label='Verification', help_text='Please enter the letters displayed for verification purposes', error_messages={'invalid':'Incorrect entry, please try again'})
|
||||||
|
security_question_1 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_1 = forms.CharField(widget=forms.TextInput(), label='Answer', initial="*****")
|
||||||
|
security_question_2 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_2 = forms.CharField(widget=forms.TextInput(), label='Answer', initial="*****")
|
||||||
|
security_question_3 = forms.ModelChoiceField(queryset=SecurityQuestion.objects.all())
|
||||||
|
answer_3 = forms.CharField(widget=forms.TextInput(), label='Answer', initial="*****")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('username', 'first_name', 'last_name', 'email', 'captcha')
|
fields = ('username', 'first_name', 'last_name', 'email', 'captcha')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EditProfileForm, self ).__init__(*args, **kwargs)
|
||||||
|
user = kwargs.get("instance")
|
||||||
|
try:
|
||||||
|
self.fields['security_question_1'].initial=user.userprofile.securityquestionanswer_set.all()[0].security_question
|
||||||
|
self.fields['security_question_2'].initial=user.userprofile.securityquestionanswer_set.all()[1].security_question
|
||||||
|
self.fields['security_question_3'].initial=user.userprofile.securityquestionanswer_set.all()[2].security_question
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
# The super user won't have had security questions created already
|
||||||
|
self.fields['security_question_1'].initial=SecurityQuestion.objects.all()[0]
|
||||||
|
self.fields['security_question_2'].initial=SecurityQuestion.objects.all()[1]
|
||||||
|
self.fields['security_question_3'].initial=SecurityQuestion.objects.all()[2]
|
||||||
|
pass
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
username = self.cleaned_data['username']
|
username = self.cleaned_data['username']
|
||||||
if 'username' in self.changed_data:
|
if 'username' in self.changed_data:
|
||||||
|
@ -208,6 +236,25 @@ class EditProfileForm(StyledModelForm):
|
||||||
|
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(EditProfileForm, self).clean()
|
||||||
|
for data in self.changed_data:
|
||||||
|
# Check if a security answer has been updated. If one is updated, they must all be
|
||||||
|
# and each security question must be unique.
|
||||||
|
if 'answer' in data:
|
||||||
|
if 'answer_1' not in self.changed_data \
|
||||||
|
or 'answer_2' not in self.changed_data \
|
||||||
|
or 'answer_3' not in self.changed_data:
|
||||||
|
raise forms.ValidationError("Please answer three security questions.")
|
||||||
|
security_question_1 = self.cleaned_data["security_question_1"]
|
||||||
|
security_question_2 = self.cleaned_data["security_question_2"]
|
||||||
|
security_question_3 = self.cleaned_data["security_question_3"]
|
||||||
|
if security_question_1 == security_question_2:
|
||||||
|
raise forms.ValidationError({'security_question_2': ["Questions may only be chosen once."]})
|
||||||
|
if security_question_1 == security_question_3 or security_question_2 == security_question_3:
|
||||||
|
raise forms.ValidationError({'security_question_3': ["Questions may only be chosen once."]})
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class ClassicRecipeForm(StyledModelForm):
|
class ClassicRecipeForm(StyledModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
46
layerindex/migrations/0030_securityquestion.py
Normal file
46
layerindex/migrations/0030_securityquestion.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.17 on 2019-01-08 22:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('layerindex', '0028_branch_hidden'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SecurityQuestion',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('question', models.CharField(max_length=250)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SecurityQuestionAnswer',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('answer', models.CharField(max_length=250)),
|
||||||
|
('security_question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='layerindex.SecurityQuestion')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('answer_attempts', models.IntegerField(default=0)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='securityquestionanswer',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='layerindex.UserProfile'),
|
||||||
|
),
|
||||||
|
]
|
25
layerindex/migrations/0031_securityquestion_populate.py
Normal file
25
layerindex/migrations/0031_securityquestion_populate.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.17 on 2019-01-03 01:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from layerindex.securityquestions import security_questions
|
||||||
|
|
||||||
|
|
||||||
|
def populate_security_questions(apps, schema_editor):
|
||||||
|
SecurityQuestion = apps.get_model('layerindex', 'SecurityQuestion')
|
||||||
|
|
||||||
|
for question in security_questions:
|
||||||
|
securityquestion = SecurityQuestion()
|
||||||
|
securityquestion.question = question
|
||||||
|
securityquestion.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('layerindex', '0030_securityquestion'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(populate_security_questions),
|
||||||
|
]
|
|
@ -864,3 +864,21 @@ class SiteNotice(models.Model):
|
||||||
|
|
||||||
def text_sanitised(self):
|
def text_sanitised(self):
|
||||||
return utils.sanitise_html(self.text)
|
return utils.sanitise_html(self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityQuestion(models.Model):
|
||||||
|
question = models.CharField(max_length = 250, null=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s' % (self.question)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
user = models.OneToOneField(User)
|
||||||
|
answer_attempts = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityQuestionAnswer(models.Model):
|
||||||
|
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
|
||||||
|
security_question = models.ForeignKey(SecurityQuestion)
|
||||||
|
answer = models.CharField(max_length = 250, null=False)
|
||||||
|
|
6
layerindex/securityquestions.py
Normal file
6
layerindex/securityquestions.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
security_questions = ["What was the name of your first pet?",
|
||||||
|
"What was the last name of your third grade teacher?",
|
||||||
|
"What is your favorite pizza topping?",
|
||||||
|
"What is your favorite band?",
|
||||||
|
"What street did you grow up on?",
|
||||||
|
]
|
|
@ -36,8 +36,8 @@ from django.views.generic import DetailView, ListView, TemplateView
|
||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
from django.views.generic.edit import (CreateView, DeleteView, FormView,
|
from django.views.generic.edit import (CreateView, DeleteView, FormView,
|
||||||
UpdateView)
|
UpdateView)
|
||||||
|
|
||||||
from django_registration.backends.activation.views import RegistrationView
|
from django_registration.backends.activation.views import RegistrationView
|
||||||
|
from pkg_resources import parse_version
|
||||||
from reversion.models import Revision
|
from reversion.models import Revision
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
|
@ -51,7 +51,9 @@ from layerindex.models import (BBAppend, BBClass, Branch, ClassicRecipe,
|
||||||
LayerDependency, LayerItem, LayerMaintainer,
|
LayerDependency, LayerItem, LayerMaintainer,
|
||||||
LayerNote, LayerUpdate, Machine, Patch, Recipe,
|
LayerNote, LayerUpdate, Machine, Patch, Recipe,
|
||||||
RecipeChange, RecipeChangeset, Source, StaticBuildDep,
|
RecipeChange, RecipeChangeset, Source, StaticBuildDep,
|
||||||
Update)
|
Update, SecurityQuestion, SecurityQuestionAnswer,
|
||||||
|
UserProfile)
|
||||||
|
|
||||||
|
|
||||||
from . import simplesearch, tasks, utils
|
from . import simplesearch, tasks, utils
|
||||||
|
|
||||||
|
@ -891,6 +893,31 @@ class EditProfileFormView(SuccessMessageMixin, UpdateView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
|
|
||||||
|
if'answer_1' in form.changed_data:
|
||||||
|
# If one security answer has changed, they all have. Delete current questions and add new ones.
|
||||||
|
# Don't throw an error if we are editing the super user and they don't have security questions yet.
|
||||||
|
try:
|
||||||
|
self.user.userprofile.securityquestionanswer_set.all().delete()
|
||||||
|
user = self.user.userprofile
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
user = UserProfile.objects.create(user=self.user)
|
||||||
|
|
||||||
|
security_question_1 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_1"))
|
||||||
|
security_question_2 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_2"))
|
||||||
|
security_question_3 = SecurityQuestion.objects.get(question=form.cleaned_data.get("security_question_3"))
|
||||||
|
answer_1 = form.cleaned_data.get("answer_1").replace(" ", "").lower()
|
||||||
|
answer_2 = form.cleaned_data.get("answer_2").replace(" ", "").lower()
|
||||||
|
answer_3 = form.cleaned_data.get("answer_3").replace(" ", "").lower()
|
||||||
|
|
||||||
|
# Answers are hashed using Django's password hashing function make_password()
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_1,
|
||||||
|
answer=make_password(answer_1))
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_2,
|
||||||
|
answer=make_password(answer_2))
|
||||||
|
SecurityQuestionAnswer.objects.create(user=user, security_question=security_question_3,
|
||||||
|
answer=make_password(answer_3))
|
||||||
|
|
||||||
if 'email' in form.changed_data:
|
if 'email' in form.changed_data:
|
||||||
# Take a copy of request.user as it is about to be invalidated by logout()
|
# Take a copy of request.user as it is about to be invalidated by logout()
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
|
@ -278,6 +278,7 @@ CACHES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AXES_CACHE = "axes_cache"
|
AXES_CACHE = "axes_cache"
|
||||||
|
AXES_LOCKOUT_TEMPLATE = "registration/account_lockout.html"
|
||||||
|
|
||||||
# Full path to directory to store logs for dynamically executed tasks
|
# Full path to directory to store logs for dynamically executed tasks
|
||||||
TASK_LOG_DIR = "/tmp/layerindex-task-logs"
|
TASK_LOG_DIR = "/tmp/layerindex-task-logs"
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
{{ hidden }}
|
{{ hidden }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="form-group alert alert-danger">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for field in form.visible_fields %}
|
{% for field in form.visible_fields %}
|
||||||
{% if field.name in error_fields %}
|
{% if field.name in error_fields %}
|
||||||
<div class="form-group alert alert-danger">
|
<div class="form-group alert alert-danger">
|
||||||
|
|
6
templates/registration/account_lockout.html
Normal file
6
templates/registration/account_lockout.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "Your account has been locked out. Please contact the admin." %}</p>
|
||||||
|
{% endblock %}
|
|
@ -10,6 +10,12 @@
|
||||||
{{ hidden }}
|
{{ hidden }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="form-group alert alert-danger">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for field in form.visible_fields %}
|
{% for field in form.visible_fields %}
|
||||||
{% if field.name in form.errors %}
|
{% if field.name in form.errors %}
|
||||||
<div class="form-group alert alert-danger">
|
<div class="form-group alert alert-danger">
|
||||||
|
|
6
templates/registration/password_reset_fail.html
Normal file
6
templates/registration/password_reset_fail.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "Password reset failed. You haven't set security questions, so we cannot reset your password." %}</p>
|
||||||
|
{% endblock %}
|
17
urls.py
17
urls.py
|
@ -8,8 +8,8 @@
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.views.generic import RedirectView, TemplateView
|
from django.views.generic import RedirectView, TemplateView
|
||||||
from layerindex.auth_views import CaptchaRegistrationView, CaptchaPasswordResetView, delete_account_view
|
from layerindex.auth_views import CaptchaRegistrationView, CaptchaPasswordResetView, delete_account_view, \
|
||||||
|
PasswordResetSecurityQuestions
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import settings
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^layerindex/', include('layerindex.urls')),
|
url(r'^layerindex/', include('layerindex.urls')),
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
url(r'^accounts/password/reset/$',
|
url(r'^accounts/password_reset/$',
|
||||||
CaptchaPasswordResetView.as_view(
|
CaptchaPasswordResetView.as_view(
|
||||||
email_template_name='registration/password_reset_email.txt',
|
email_template_name='registration/password_reset_email.txt',
|
||||||
success_url=reverse_lazy('password_reset_done')),
|
success_url=reverse_lazy('password_reset_done')),
|
||||||
|
@ -31,11 +31,20 @@ urlpatterns = [
|
||||||
url(r'^accounts/reregister/$', TemplateView.as_view(
|
url(r'^accounts/reregister/$', TemplateView.as_view(
|
||||||
template_name='registration/reregister.html'),
|
template_name='registration/reregister.html'),
|
||||||
name='reregister'),
|
name='reregister'),
|
||||||
|
url(r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,3}-[0-9A-Za-z]{1,20})/$',
|
||||||
|
PasswordResetSecurityQuestions.as_view(),
|
||||||
|
name='password_reset_confirm',
|
||||||
|
),
|
||||||
|
url(r'^accounts/reset/fail/$', TemplateView.as_view(
|
||||||
|
template_name='registration/password_reset_fail.html'),
|
||||||
|
name='password_reset_fail'),
|
||||||
|
url(r'^accounts/lockout/$', TemplateView.as_view(
|
||||||
|
template_name='registration/account_lockout.html'),
|
||||||
|
name='account_lockout'),
|
||||||
url(r'^accounts/', include('django_registration.backends.activation.urls')),
|
url(r'^accounts/', include('django_registration.backends.activation.urls')),
|
||||||
url(r'^accounts/', include('django.contrib.auth.urls')),
|
url(r'^accounts/', include('django.contrib.auth.urls')),
|
||||||
url(r'^captcha/', include('captcha.urls')),
|
url(r'^captcha/', include('captcha.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if 'rrs' in settings.INSTALLED_APPS:
|
if 'rrs' in settings.INSTALLED_APPS:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
url(r'^rrs/', include('rrs.urls')),
|
url(r'^rrs/', include('rrs.urls')),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user