Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Password Validation Configuration #4482

Merged
merged 22 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6b2805e
Add constance settings for password validation
JacquelineMorrissette Jun 8, 2023
998fe31
Add custom classes for password validation
noliveleger Jun 28, 2023
396cdef
Update django-constance to 2.9.1
noliveleger Jun 29, 2023
3c07cfc
Add password validation classes
noliveleger Jun 29, 2023
00c01d0
Unit tests
noliveleger Jul 3, 2023
6308aeb
Fix error when validating passwords with user's attributes
noliveleger Jul 4, 2023
7ebc783
Merge branch 'feature/password-complexity' into 4462-configure-custom…
noliveleger Jul 4, 2023
b59cab7
Merge branch '4462-configure-custom-reqs' into 4462-password-validati…
noliveleger Jul 4, 2023
1de51a8
Void lru cache after uploading a new custom passwords file
noliveleger Jul 6, 2023
0d35bf1
Make /me endpoint use Django password validation
noliveleger Jul 7, 2023
365e69a
Add unit test for /me endpoint for password validation
noliveleger Jul 7, 2023
008dbdf
Merge branch 'feature/password-complexity' into 4462-configure-custom…
noliveleger Jul 7, 2023
12a4623
Merge branch '4462-configure-custom-reqs' into 4462-password-validati…
noliveleger Jul 7, 2023
82778a5
Fix migrations conflict
noliveleger Jul 7, 2023
31ebcdc
Deactivate password validation in unit tests for django-allauth emails
noliveleger Jul 7, 2023
dfa1b8c
Use ConfigurationFileSlug.choices() instead of ConfigurationFile cons…
noliveleger Jul 7, 2023
9fb66ce
Deactivate password validation in unit tests for registration
noliveleger Jul 7, 2023
2a13ec5
Fix caching in unit tests
noliveleger Jul 7, 2023
f24bd92
Apply requested changes for PR#4533
noliveleger Jul 18, 2023
c89f067
Merge pull request #4533 from kobotoolbox/4462-password-validation-cl…
noliveleger Jul 18, 2023
56c0b57
Merge branch 'feature/password-complexity' into 4462-configure-custom…
noliveleger Jul 18, 2023
3c674fd
Merge branch '4462-configure-custom-reqs' of github.com:kobotoolbox/k…
noliveleger Jul 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion dependencies/pip/dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ django-braces==1.15.0
# via -r dependencies/pip/requirements.in
django-celery-beat==2.2.1
# via -r dependencies/pip/requirements.in
django-constance[database]==2.8.0
django-constance[database]==2.9.1
# via -r dependencies/pip/requirements.in
django-cors-headers==3.11.0
# via -r dependencies/pip/requirements.in
Expand Down Expand Up @@ -468,6 +468,8 @@ redis==4.2.2
# django-redis
# django-redis-sessions
# kobo-service-account
regex==2023.6.3
# via -r dependencies/pip/requirements.in
requests==2.27.1
# via
# -r dependencies/pip/requirements.in
Expand Down
1 change: 1 addition & 0 deletions dependencies/pip/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pymongo==3.12.3
python-dateutil
pyxform==1.9.0
requests
regex
responses
shortuuid
sqlparse
Expand Down
4 changes: 3 additions & 1 deletion dependencies/pip/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ django-braces==1.15.0
# via -r dependencies/pip/requirements.in
django-celery-beat==2.2.1
# via -r dependencies/pip/requirements.in
django-constance[database]==2.8.0
django-constance[database]==2.9.1
# via -r dependencies/pip/requirements.in
django-cors-headers==3.11.0
# via -r dependencies/pip/requirements.in
Expand Down Expand Up @@ -385,6 +385,8 @@ redis==4.2.2
# django-redis
# django-redis-sessions
# kobo-service-account
regex==2023.6.3
# via -r dependencies/pip/requirements.in
requests==2.27.1
# via
# -r dependencies/pip/requirements.in
Expand Down
24 changes: 24 additions & 0 deletions hub/migrations/0012_add_common_passwords_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.15 on 2023-06-28 19:32

from django.db import migrations, models
import hub.models


class Migration(migrations.Migration):

dependencies = [
('hub', '0011_extrauserdetail_private_data'),
]

operations = [
migrations.AlterField(
model_name='configurationfile',
name='content',
field=models.FileField(help_text='Stored in a PUBLIC location where authentication is NOT required for access', upload_to=hub.models._configuration_file_upload_to),
),
migrations.AlterField(
model_name='configurationfile',
name='slug',
field=models.CharField(choices=[('logo', 'logo'), ('logo_small', 'logo_small'), ('login_background', 'login_background'), ('common_passwords_file', 'common_passwords_file')], max_length=32, unique=True),
),
]
18 changes: 18 additions & 0 deletions hub/migrations/0013_alter_configurationfile_slug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.15 on 2023-06-28 19:35

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('hub', '0012_add_common_passwords_file'),
]

operations = [
migrations.AlterField(
model_name='configurationfile',
name='slug',
field=models.CharField(choices=[('logo', 'Logo'), ('logo_small', 'Small Logo'), ('login_background', 'Login background'), ('common_passwords_file', 'Common passwords file')], max_length=32, unique=True),
),
]
37 changes: 24 additions & 13 deletions hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import get_default_password_validators
from django.core.exceptions import FieldError, ValidationError
from django.db import models
from django.db.models.signals import post_save
Expand All @@ -21,6 +22,15 @@
from kpi.utils.object_permission import get_database_user


def _configuration_file_upload_to(instance, filename):
if instance.slug == ConfigurationFileSlug.COMMON_PASSWORDS_FILE:
# Void lru cache to reload the file at the next password validation.
get_default_password_validators.cache_clear()
return f'__django_files/{instance.slug}/{filename}'

return f'{settings.PUBLIC_MEDIA_PATH}/{instance.slug}/{filename}'


class SitewideMessage(models.Model):
slug = models.CharField(max_length=50)
body = MarkupField()
Expand All @@ -29,23 +39,24 @@ def __str__(self):
return self.slug


class ConfigurationFileSlug(models.TextChoices):

LOGO = 'logo', 'Logo'
LOGO_SMALL = 'logo_small', 'Small Logo'
LOGIN_BACKGROUND = 'login_background', 'Login background'
COMMON_PASSWORDS_FILE = 'common_passwords_file', 'Common passwords file'


class ConfigurationFile(models.Model):
LOGO = 'logo'
LOGO_SMALL = 'logo_small'
LOGIN_BACKGROUND = 'login_background'

SLUG_CHOICES = (
(LOGO, LOGO),
(LOGO_SMALL, LOGO_SMALL),
(LOGIN_BACKGROUND, LOGIN_BACKGROUND),
)

slug = models.CharField(max_length=32, choices=SLUG_CHOICES, unique=True)
slug = models.CharField(
max_length=32, choices=ConfigurationFileSlug.choices, unique=True
)
content = models.FileField(
upload_to=settings.PUBLIC_MEDIA_PATH,
upload_to=_configuration_file_upload_to,
help_text=(
'Stored in a PUBLIC location where authentication is '
'NOT required for access'
'Stored in a PUBLIC location where authentication is NOT required '
'**to** access common passwords file.'
),
)

Expand Down
6 changes: 3 additions & 3 deletions hub/tests/test_configurationfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.files.base import ContentFile
from django.template import Template, RequestContext

from hub.models import ConfigurationFile
from hub.models import ConfigurationFile, ConfigurationFileSlug

sample_svg = b'''<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 420 666">
Expand Down Expand Up @@ -39,7 +39,7 @@ class ConfigurationFileTestCase(LiveServerTestCase):

def setUp(self):
cfg_file = ConfigurationFile()
cfg_file.slug = cfg_file.LOGO
cfg_file.slug = ConfigurationFileSlug.LOGO
# FileField.save() also saves the model instance
cfg_file.content.save("sample.svg", ContentFile(sample_svg))
self.cfg_file = cfg_file
Expand All @@ -57,7 +57,7 @@ def test_configurationfile_url(self):
def test_template_context_processor(self):
context = RequestContext(HttpRequest()) # NB: empty request
template = Template(
'{{{{ config.{logo} }}}}'.format(logo=self.cfg_file.LOGO)
'{{{{ config.{logo} }}}}'.format(logo=ConfigurationFileSlug.LOGO)
)
result = template.render(context)
self.assertEqual(result, self.cfg_file.url)
48 changes: 44 additions & 4 deletions kobo/apps/accounts/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import json

import constance
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.forms import LoginForm as BaseLoginForm
from allauth.account.forms import SignupForm as BaseSignupForm
from allauth.account.utils import (
get_user_model,
user_email,
user_username,
)
from allauth.socialaccount.forms import SignupForm as BaseSocialSignupForm
from django import forms
from django.contrib.auth import get_user_model
from django.conf import settings
from django.utils.translation import gettext_lazy as t

Expand All @@ -23,9 +31,9 @@
class LoginForm(BaseLoginForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["login"].widget.attrs["placeholder"] = ""
self.fields["password"].widget.attrs["placeholder"] = ""
self.label_suffix = ""
self.fields['login'].widget.attrs['placeholder'] = ''
self.fields['password'].widget.attrs['placeholder'] = ''
self.label_suffix = ''


class KoboSignupMixin(forms.Form):
Expand Down Expand Up @@ -128,7 +136,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.UNSAFE_SSO_REGISTRATION_EMAIL_DISABLE:
self.fields['email'].widget.attrs['readonly'] = True
self.label_suffix = ""
self.label_suffix = ''


class SignupForm(KoboSignupMixin, BaseSignupForm):
Expand All @@ -140,3 +148,35 @@ class SignupForm(KoboSignupMixin, BaseSignupForm):
'sector',
'country',
]

def clean(self):
"""
Override parent form to pass extra user's attributes to validation.
"""
super(BaseSignupForm, self).clean()

User = get_user_model() # noqa
dummy_user = User()
user_username(dummy_user, self.cleaned_data.get('username'))
user_email(dummy_user, self.cleaned_data.get('email'))
setattr(dummy_user, 'organization', self.cleaned_data.get('organization', ''))
setattr(dummy_user, 'full_name', self.cleaned_data.get('name', ''))

password = self.cleaned_data.get('password1')
if password:
try:
get_adapter().clean_password(password, user=dummy_user)
except forms.ValidationError as e:
self.add_error('password1', e)

if (
app_settings.SIGNUP_PASSWORD_ENTER_TWICE
and 'password1' in self.cleaned_data
and 'password2' in self.cleaned_data
):
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
self.add_error(
'password2',
t('You must type the same password each time.'),
)
return self.cleaned_data
42 changes: 38 additions & 4 deletions kobo/apps/accounts/tests/test_email_content.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import random

from constance.test import override_config
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import override_settings, TestCase
Expand All @@ -23,6 +22,15 @@ def setUp(self) -> None:
'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
}
)
# use `override_config` decorator to deactivate all password validators
# to let this test use a simple password.
@override_config(
ENABLE_PASSWORD_MINIMUM_LENGTH_VALIDATION=False,
ENABLE_PASSWORD_USER_ATTRIBUTE_SIMILARITY_VALIDATION=False,
ENABLE_MOST_RECENT_PASSWORD_VALIDATION=False,
ENABLE_COMMON_PASSWORD_VALIDATION=False,
ENABLE_PASSWORD_CUSTOM_CHARACTER_RULES_VALIDATION=False,
)
def test_custom_activation_email_template(self):
email_content = EmailContent.objects.create(
email_name='email_confirmation_signup_message',
Expand Down Expand Up @@ -51,7 +59,20 @@ def test_custom_activation_email_template(self):
assert mail.outbox[0].subject == email_subject.content
assert email_content.content in mail.outbox[0].body

@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
@override_settings(
CACHES={
'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
}
)
# use `override_config` decorator to deactivate all password validators
# to let this test use a simple password.
@override_config(
ENABLE_PASSWORD_MINIMUM_LENGTH_VALIDATION=False,
ENABLE_PASSWORD_USER_ATTRIBUTE_SIMILARITY_VALIDATION=False,
ENABLE_MOST_RECENT_PASSWORD_VALIDATION=False,
ENABLE_COMMON_PASSWORD_VALIDATION=False,
ENABLE_PASSWORD_CUSTOM_CHARACTER_RULES_VALIDATION=False,
)
def test_custom_activation_email_template_blank_content(self):
email_content = EmailContent.objects.create(
email_name='email_confirmation_signup_message',
Expand Down Expand Up @@ -97,7 +118,20 @@ def test_custom_activation_email_template_blank_content(self):
assert email_content.content in mail.outbox[0].body
assert default not in mail.outbox[0].body

@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
@override_settings(
CACHES={
'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
}
)
# use `override_config` decorator to deactivate all password validators
# to let this test use a simple password.
@override_config(
ENABLE_PASSWORD_MINIMUM_LENGTH_VALIDATION=False,
ENABLE_PASSWORD_USER_ATTRIBUTE_SIMILARITY_VALIDATION=False,
ENABLE_MOST_RECENT_PASSWORD_VALIDATION=False,
ENABLE_COMMON_PASSWORD_VALIDATION=False,
ENABLE_PASSWORD_CUSTOM_CHARACTER_RULES_VALIDATION=False,
)
def test_default_activation_email_template(self):
username = 'user003'
email = username + '@example.com'
Expand Down
1 change: 1 addition & 0 deletions kobo/apps/subsequences/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
# Processing time is not audio length, but it's an estimate
GOOGLE_CACHE_TIMEOUT = 28800 # 8 hours


def make_async_cache_key(user_id: int, submission_id: str, xpath: str, source: str):
return f'{SUBSEQUENCES_ASYNC_CACHE_KEY}-{user_id}-{submission_id}-{xpath}-{source}'