13

I am trying to add custom password validation to the create user and change password admin forms. I did not see anything in the django docs about how to do this. I found this post on SO: Enforcing password strength requirements with django.contrib.auth.views.password_change, which gives 2 solutions. I tried both, but neither worked for me.

Here's what I have now in my apps admin.py:

def validate_password_strength(value):
    """Validates that a password is as least 10 characters long and has at least
    2 digits and 1 Upper case letter.
    """
    min_length = 10

    if len(value) < min_length:
        raise ValidationError(_('Password must be at least {0} characters '
                                'long.').format(min_length))

    # check for 2 digits
    if sum(c.isdigit() for c in value) < 2:
        raise ValidationError(_('Password must container at least 2 digits.'))

    # check for uppercase letter
    if not any(c.isupper() for c in value):
        raise ValidationError(_('Password must container at least 1 uppercase letter.'))

class MySetPasswordForm(SetPasswordForm):
    def __init__(self, *args, **kwargs):
        super(MySetPasswordForm, self).__init__(*args, **kwargs)
        self.fields['password1'].validators.append(validate_password_strength)

But what I can't figure out is how do I get MySetPasswordForm to be used.

I've tried a few different things. First I did this:

class UserAdmin(UserAdmin):
    form = MySetPasswordForm

# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

And I got this error:

<class 'elex_apis.energy.webservice.admin.UserAdmin'>: (admin.E016) The value of 'form' must inherit from 'BaseModelForm'.

So then I changed MySetPasswordForm to inherit from BaseModelForm and then I got this error:

__init__() missing 1 required positional argument: 'user'

So then I added the user param so now MySetPasswordForm looked like this:

class MySetPasswordForm(BaseModelForm):
    def __init__(self, user, *args, **kwargs):
        super(MySetPasswordForm, self).__init__(user, *args, **kwargs)
        self.fields['password1'].validators.append(validate_password_strength)

But I still got the same error as before.

I can't believe it's this hard to add a password validator. It must be a very common need, so clearly I must be missing something simple. Can anyone please provide some assistance here.

Community
  • 1
  • 1
Larry Martell
  • 3,526
  • 6
  • 40
  • 76

2 Answers2

8

The easiest way is to inherit the original UserAdmin and just override the change_password_form.

Example:

from django.contrib.auth import models as auth_models
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import forms as auth_forms
from django.core.exceptions import ValidationError


def validate_password_strength(value):
    """Validates that a password is as least 10 characters long and has at least
    2 digits and 1 Upper case letter.
    """
    min_length = 10

    if len(value) < min_length:
        raise ValidationError(_('Password must be at least {0} characters '
                                'long.').format(min_length))

    # check for 2 digits
    if sum(c.isdigit() for c in value) < 2:
        raise ValidationError(_('Password must container at least 2 digits.'))

    # check for uppercase letter
    if not any(c.isupper() for c in value):
        raise ValidationError(_('Password must container at least 1 uppercase letter.'))

    return value


class AdminPasswordChangeForm(auth_forms.AdminPasswordChangeForm):
    def clean_password1(self):
        return validate_password_strength(self.cleaned_data['password1'])


class UserCreationForm(auth_forms.UserCreationForm):
    def clean_password1(self):
        return validate_password_strength(self.cleaned_data['password1'])


class UserAdmin(auth_admin.UserAdmin):
    change_password_form = AdminPasswordChangeForm
    add_form = UserCreationForm


# Re-register UserAdmin
admin.site.unregister(auth_models.User)
admin.site.register(auth_models.User, UserAdmin)
juan Isaza
  • 3,646
  • 3
  • 31
  • 37
Wolph
  • 78,177
  • 11
  • 137
  • 148
  • This does validate the password, but when after a valid password is entered and admin returns to the previous page, it says "No password set." However when I look in the database there is a password in auth_user. – Larry Martell Apr 13 '15 at 11:32
  • 1
    @LarryMartell: oops... forgot to add the `return value`, try with the new version :) – Wolph Apr 13 '15 at 12:21
0

Another way to do this is by creating a validators.py file and and inside it, create your own classes based on object and raise a ValidationError if the entered password fails. More on it here.

import re

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _

class NumberValidator(object):
    def validate(self, password, user=None):
        if not re.findall('\d', password):
            raise ValidationError(
               _("The password must contain at least 1 digit, 0-9."),
               code='password_no_number',
            )

    def get_help_text(self):
        return _(
           "Your password must contain at least 1 digit, 0-9."
        )

class UppercaseValidator(object):
    def validate(self, password, user=None):
        if not re.findall('[A-Z]', password):
            raise ValidationError(
                _("The password must contain at least 1 uppercase letter, A-Z."),
                code='password_no_upper',
            )

    def get_help_text(self):
        return _(
            "Your password must contain at least 1 uppercase letter, A-Z."
        )

Update the AUTH_PASSWORD_VALIDATORS setting with the correct dotted path to your validators:

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 12, }
     },
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', },
    {'NAME': 'my.project.validators.NumberValidator',
        'OPTIONS': {
            'min_digits': 3, }},
    {'NAME': 'my.project.validators.UppercaseValidator', },        
]
Apoorv Patne
  • 679
  • 7
  • 24