authenticators: migrate min_password_strength to password authenticator (#78232) #78
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-20 15:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('a2_rbac', '0036_delete_roleattribute'),
|
||||
('authenticators', '0013_migrate_min_password_strength'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='organizationalunit',
|
||||
name='min_password_strength',
|
||||
),
|
||||
]
|
|
@ -102,15 +102,6 @@ class OrganizationalUnit(AbstractBase):
|
|||
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
|
||||
}
|
||||
|
||||
MIN_PASSWORD_STRENGTH_CHOICES = (
|
||||
(None, _("System default")),
|
||||
(0, _("Very Weak")),
|
||||
(1, _("Weak")),
|
||||
(2, _("Fair")),
|
||||
(3, _("Good")),
|
||||
(4, _("Strong")),
|
||||
)
|
||||
|
||||
username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique'))
|
||||
email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique'))
|
||||
default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))
|
||||
|
@ -137,14 +128,6 @@ class OrganizationalUnit(AbstractBase):
|
|||
verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0
|
||||
)
|
||||
|
||||
min_password_strength = models.IntegerField(
|
||||
verbose_name=_('Minimum password strength'),
|
||||
choices=MIN_PASSWORD_STRENGTH_CHOICES,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
clean_unused_accounts_alert = models.PositiveIntegerField(
|
||||
verbose_name=_('Days after which the user receives an account deletion alert'),
|
||||
validators=[
|
||||
|
|
|
@ -195,9 +195,6 @@ default_settings = dict(
|
|||
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(
|
||||
default=False, definition='Show last character in password fields'
|
||||
),
|
||||
A2_PASSWORD_POLICY_MIN_STRENGTH=Setting(
|
||||
default=None, definition='Minimun password strength on a 0-4 scale'
|
||||
),
|
||||
A2_SUGGESTED_EMAIL_DOMAINS=Setting(
|
||||
default=[
|
||||
'gmail.com',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-22 15:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('authenticators', '0011_migrate_a2_accept_authentication_settings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='loginpasswordauthenticator',
|
||||
name='min_password_strength',
|
||||
field=models.IntegerField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(None, 'System default'),
|
||||
(0, 'Very Weak'),
|
||||
(1, 'Weak'),
|
||||
(2, 'Fair'),
|
||||
(3, 'Good'),
|
||||
(4, 'Strong'),
|
||||
],
|
||||
null=True,
|
||||
verbose_name='Minimum password strength',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-22 15:08
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_min_password_strength(apps, schema_editor):
|
||||
LoginPasswordAuthenticator = apps.get_model('authenticators', 'LoginPasswordAuthenticator')
|
||||
authenticator, _ = LoginPasswordAuthenticator.objects.get_or_create(
|
||||
slug='password-authenticator',
|
||||
defaults={'enabled': True},
|
||||
)
|
||||
|
||||
password_strength = -1
|
||||
if getattr(settings, 'A2_PASSWORD_POLICY_MIN_STRENGTH', None):
|
||||
try:
|
||||
password_strength = int(settings.A2_PASSWORD_POLICY_MIN_STRENGTH)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
password_strength = min(password_strength, 4)
|
||||
|
||||
OU = apps.get_model('a2_rbac', 'OrganizationalUnit')
|
||||
for ou in OU.objects.all():
|
||||
ou_password_strength = ou.min_password_strength if ou.min_password_strength is not None else -1
|
||||
password_strength = max(password_strength, ou_password_strength)
|
||||
|
||||
if password_strength <= -1:
|
||||
password_strength = None
|
||||
|
||||
authenticator.min_password_strength = password_strength
|
||||
authenticator.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('authenticators', '0012_loginpasswordauthenticator_min_password_strength'),
|
||||
('a2_rbac', '0036_delete_roleattribute'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_min_password_strength, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -279,6 +279,15 @@ class AddRoleAction(AuthenticatorRelatedObjectBase):
|
|||
|
||||
|
||||
class LoginPasswordAuthenticator(BaseAuthenticator):
|
||||
MIN_PASSWORD_STRENGTH_CHOICES = (
|
||||
(None, _('System default')),
|
||||
(0, _('Very Weak')),
|
||||
(1, _('Weak')),
|
||||
(2, _('Fair')),
|
||||
(3, _('Good')),
|
||||
(4, _('Strong')),
|
||||
)
|
||||
|
||||
registration_open = models.BooleanField(
|
||||
_('Registration open'), default=True, help_text=_('Allow users to create accounts.')
|
||||
)
|
||||
|
@ -305,6 +314,12 @@ class LoginPasswordAuthenticator(BaseAuthenticator):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
min_password_strength = models.IntegerField(
|
||||
verbose_name=_('Minimum password strength'),
|
||||
choices=MIN_PASSWORD_STRENGTH_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
password_min_length = models.PositiveIntegerField(_('Password minimum length'), default=8, null=True)
|
||||
password_regex = models.CharField(
|
||||
_('Regular expression for validating passwords'), max_length=512, blank=True, default=''
|
||||
|
|
|
@ -27,7 +27,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from authentic2.backends.ldap_backend import LDAPUser
|
||||
from authentic2.journal import journal
|
||||
from authentic2.passwords import get_min_password_strength
|
||||
from authentic2.utils.misc import get_password_authenticator
|
||||
|
||||
from .. import app_settings, models, validators
|
||||
from ..backends import get_user_queryset
|
||||
|
@ -265,7 +265,7 @@ class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.Set
|
|||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_min_password_strength(user)
|
||||
self.fields['new_password1'].min_strength = get_password_authenticator().min_password_strength
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
|
@ -286,7 +286,7 @@ class PasswordChangeForm(
|
|||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_min_password_strength(user)
|
||||
self.fields['new_password1'].min_strength = get_password_authenticator().min_password_strength
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
|
|
|
@ -27,7 +27,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit
|
||||
from authentic2.forms.fields import CharField, CheckPasswordField, NewPasswordField
|
||||
from authentic2.passwords import get_min_password_strength
|
||||
|
||||
from .. import app_settings, models
|
||||
from ..utils import misc as utils_misc
|
||||
|
@ -174,7 +173,7 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password1'].min_strength = get_min_password_strength(self.instance)
|
||||
self.fields['password1'].min_strength = utils_misc.get_password_authenticator().min_password_strength
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
|
|
|
@ -44,9 +44,10 @@ from authentic2.forms.fields import (
|
|||
from authentic2.forms.mixins import SlugMixin
|
||||
from authentic2.forms.profile import BaseUserForm
|
||||
from authentic2.models import APIClient, PasswordReset, Service, Setting
|
||||
from authentic2.passwords import generate_password, get_min_password_strength
|
||||
from authentic2.passwords import generate_password
|
||||
from authentic2.utils.misc import (
|
||||
RUNTIME_SETTINGS,
|
||||
get_password_authenticator,
|
||||
send_email_change_email,
|
||||
send_password_reset_mail,
|
||||
send_templated_mail,
|
||||
|
@ -257,7 +258,7 @@ class UserChangePasswordForm(CssClass, forms.ModelForm):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.fields['password1'].min_strength = get_min_password_strength(self.instance)
|
||||
self.fields['password1'].min_strength = get_password_authenticator().min_password_strength
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -654,7 +655,6 @@ class OUEditForm(SlugMixin, CssClass, forms.ModelForm):
|
|||
'check_required_on_login_attributes',
|
||||
'user_can_reset_password',
|
||||
'user_add_password_policy',
|
||||
'min_password_strength',
|
||||
'clean_unused_accounts_alert',
|
||||
'clean_unused_accounts_deletion',
|
||||
'home_url',
|
||||
|
|
|
@ -112,12 +112,6 @@ def get_password_checker(*args, **kwargs):
|
|||
return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs)
|
||||
|
||||
|
||||
def get_min_password_strength(user):
|
||||
if user.ou and user.ou.min_password_strength is not None:
|
||||
return user.ou.min_password_strength
|
||||
return app_settings.A2_PASSWORD_POLICY_MIN_STRENGTH
|
||||
|
||||
|
||||
class StrengthReport:
|
||||
def __init__(self, strength, hint):
|
||||
self.strength = strength
|
||||
|
|
|
@ -678,3 +678,48 @@ def test_password_authenticator_migration_accept_authentication_settings(migrati
|
|||
|
||||
assert authenticator.accept_email_authentication == email
|
||||
assert authenticator.accept_phone_authentication == phone
|
||||
|
||||
|
||||
@pytest.mark.parametrize('strength,expected_strength', [(None, 3), ('invalid', 3), (1, 3), (4, 4), (42, 4)])
|
||||
def test_password_authenticator_data_migration_min_password_strength(
|
||||
migration, settings, strength, expected_strength
|
||||
):
|
||||
app = 'authenticators'
|
||||
migrate_from = [
|
||||
(app, '0012_loginpasswordauthenticator_min_password_strength'),
|
||||
('a2_rbac', '0036_delete_roleattribute'),
|
||||
]
|
||||
migrate_to = [(app, '0013_migrate_min_password_strength')]
|
||||
|
||||
old_apps = migration.before(migrate_from)
|
||||
OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
|
||||
|
||||
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = strength
|
||||
|
||||
OU.objects.create(name='OU1', slug='ou1', min_password_strength=2)
|
||||
OU.objects.create(name='OU2', slug='ou2', min_password_strength=None)
|
||||
OU.objects.create(name='OU3', slug='ou3', min_password_strength=3)
|
||||
|
||||
new_apps = migration.apply(migrate_to)
|
||||
LoginPasswordAuthenticator = new_apps.get_model(app, 'LoginPasswordAuthenticator')
|
||||
authenticator = LoginPasswordAuthenticator.objects.get()
|
||||
assert authenticator.min_password_strength == expected_strength
|
||||
|
||||
|
||||
def test_password_authenticator_data_migration_min_password_strength_zero(migration, settings):
|
||||
app = 'authenticators'
|
||||
migrate_from = [
|
||||
(app, '0012_loginpasswordauthenticator_min_password_strength'),
|
||||
('a2_rbac', '0036_delete_roleattribute'),
|
||||
]
|
||||
migrate_to = [(app, '0013_migrate_min_password_strength')]
|
||||
|
||||
old_apps = migration.before(migrate_from)
|
||||
OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
|
||||
|
||||
OU.objects.create(name='OU1', slug='ou1', min_password_strength=0)
|
||||
|
||||
new_apps = migration.apply(migrate_to)
|
||||
LoginPasswordAuthenticator = new_apps.get_model(app, 'LoginPasswordAuthenticator')
|
||||
authenticator = LoginPasswordAuthenticator.objects.get()
|
||||
assert authenticator.min_password_strength == 0
|
||||
|
|
|
@ -32,6 +32,7 @@ from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP, VIEW_OP
|
|||
from authentic2.a2_rbac.models import OrganizationalUnit as OU
|
||||
from authentic2.a2_rbac.models import Permission, Role
|
||||
from authentic2.a2_rbac.utils import get_default_ou, get_operation
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.models import Service, Setting
|
||||
from authentic2.validators import EmailValidator
|
||||
|
@ -250,7 +251,7 @@ def test_manager_user_change_password_form(app, simple_user):
|
|||
assert form.fields['password1'].widget.min_strength is None
|
||||
assert 'password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
form = UserChangePasswordForm(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength == 3
|
||||
assert form.errors['password1'] == ['This password is not strong enough.']
|
||||
|
|
|
@ -80,6 +80,7 @@ def test_authenticators_password(app, superuser_or_admin, settings):
|
|||
'show_condition',
|
||||
'button_description',
|
||||
'registration_open',
|
||||
'min_password_strength',
|
||||
'password_min_length',
|
||||
'remember_me',
|
||||
'include_ou_selector',
|
||||
|
@ -187,6 +188,7 @@ def test_authenticators_password_export(app, superuser):
|
|||
'registration_open': True,
|
||||
'remember_me': None,
|
||||
'include_ou_selector': False,
|
||||
'min_password_strength': None,
|
||||
'password_min_length': 8,
|
||||
'password_regex': '',
|
||||
'password_regex_error_msg': '',
|
||||
|
|
|
@ -25,7 +25,6 @@ from django.urls import reverse
|
|||
from httmock import HTTMock, remember_called, urlmatch
|
||||
|
||||
from authentic2 import models
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
|
@ -926,16 +925,14 @@ def test_registration_completion_form(db, simple_user):
|
|||
assert form.fields['password1'].widget.min_strength is None
|
||||
assert 'password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
form = form_class(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength == 3
|
||||
assert form.errors['password1'] == ['This password is not strong enough.']
|
||||
|
||||
|
||||
def test_registration_completion(db, app, mailoutbox):
|
||||
default_ou = get_default_ou()
|
||||
default_ou.min_password_strength = 3
|
||||
default_ou.save()
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
|
||||
resp = app.get(reverse('registration_register'))
|
||||
resp.form.set('email', 'testbot@entrouvert.com')
|
||||
|
|
|
@ -130,7 +130,7 @@ def test_password_change_form(simple_user):
|
|||
assert form.fields['new_password1'].widget.min_strength is None
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
form = PasswordChangeForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
@ -146,7 +146,7 @@ def test_set_password_form(simple_user):
|
|||
assert form.fields['new_password1'].widget.min_strength is None
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
form = SetPasswordForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
|
Loading…
Reference in New Issue