authenticators: migrate min_password_strength to password authenticator (#78232) #78

Merged
vdeniaud merged 1 commits from wip/78232-Deplacer-une-partie-des-parametr into main 2023-06-26 15:58:25 +02:00
15 changed files with 164 additions and 42 deletions

View File

@ -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',
),
]

View File

@ -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=[

View File

@ -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',

View File

@ -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',
),
),
]

View File

@ -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),
]

View File

@ -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=''

View File

@ -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')

View File

@ -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):
"""

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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.']

View File

@ -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': '',

View File

@ -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')

View File

@ -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.']