Skip to content

Commit

Permalink
Merge pull request #4533 from kobotoolbox/4462-password-validation-cl…
Browse files Browse the repository at this point in the history
…asses

Validate password based on (Constance) settings in Django Admin
  • Loading branch information
noliveleger committed Jul 18, 2023
2 parents 008dbdf + f24bd92 commit c89f067
Show file tree
Hide file tree
Showing 19 changed files with 690 additions and 90 deletions.
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}'

0 comments on commit c89f067

Please sign in to comment.