123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- import unicodedata
- from django import forms
- from django.contrib.auth import authenticate, get_user_model, password_validation
- from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
- from django.contrib.auth.models import User
- from django.contrib.auth.tokens import default_token_generator
- from django.contrib.sites.shortcuts import get_current_site
- from django.core.exceptions import ValidationError
- from django.core.mail import EmailMultiAlternatives
- from django.template import loader
- from django.utils.encoding import force_bytes
- from django.utils.http import urlsafe_base64_encode
- from django.utils.text import capfirst
- from django.utils.translation import gettext
- from django.utils.translation import gettext_lazy as _
- UserModel = get_user_model()
- def _unicode_ci_compare(s1, s2):
- """
- Perform case-insensitive comparison of two identifiers, using the
- recommended algorithm from Unicode Technical Report 36, section
- 2.11.2(B)(2).
- """
- return (
- unicodedata.normalize("NFKC", s1).casefold()
- == unicodedata.normalize("NFKC", s2).casefold()
- )
- class ReadOnlyPasswordHashWidget(forms.Widget):
- template_name = "auth/widgets/read_only_password_hash.html"
- read_only = True
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- usable_password = value and not value.startswith(UNUSABLE_PASSWORD_PREFIX)
- summary = []
- if usable_password:
- try:
- hasher = identify_hasher(value)
- except ValueError:
- summary.append(
- {
- "label": gettext(
- "Invalid password format or unknown hashing algorithm."
- )
- }
- )
- else:
- for key, value_ in hasher.safe_summary(value).items():
- summary.append({"label": gettext(key), "value": value_})
- else:
- summary.append({"label": gettext("No password set.")})
- context["summary"] = summary
- context["button_label"] = (
- _("Reset password") if usable_password else _("Set password")
- )
- return context
- def id_for_label(self, id_):
- return None
- class ReadOnlyPasswordHashField(forms.Field):
- widget = ReadOnlyPasswordHashWidget
- def __init__(self, *args, **kwargs):
- kwargs.setdefault("required", False)
- kwargs.setdefault("disabled", True)
- super().__init__(*args, **kwargs)
- class UsernameField(forms.CharField):
- def to_python(self, value):
- value = super().to_python(value)
- if self.max_length is not None and len(value) > self.max_length:
- # Normalization can increase the string length (e.g.
- # "ff" -> "ff", "½" -> "1⁄2") but cannot reduce it, so there is no
- # point in normalizing invalid data. Moreover, Unicode
- # normalization is very slow on Windows and can be a DoS attack
- # vector.
- return value
- return unicodedata.normalize("NFKC", value)
- def widget_attrs(self, widget):
- return {
- **super().widget_attrs(widget),
- "autocapitalize": "none",
- "autocomplete": "username",
- }
- class SetPasswordMixin:
- """
- Form mixin that validates and sets a password for a user.
- """
- error_messages = {
- "password_mismatch": _("The two password fields didn’t match."),
- }
- @staticmethod
- def create_password_fields(label1=_("Password"), label2=_("Password confirmation")):
- password1 = forms.CharField(
- label=label1,
- required=False,
- strip=False,
- widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
- help_text=password_validation.password_validators_help_text_html(),
- )
- password2 = forms.CharField(
- label=label2,
- required=False,
- widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
- strip=False,
- help_text=_("Enter the same password as before, for verification."),
- )
- return password1, password2
- def validate_passwords(
- self,
- password1_field_name="password1",
- password2_field_name="password2",
- ):
- password1 = self.cleaned_data.get(password1_field_name)
- password2 = self.cleaned_data.get(password2_field_name)
- if not password1 and password1_field_name not in self.errors:
- error = ValidationError(
- self.fields[password1_field_name].error_messages["required"],
- code="required",
- )
- self.add_error(password1_field_name, error)
- if not password2 and password2_field_name not in self.errors:
- error = ValidationError(
- self.fields[password2_field_name].error_messages["required"],
- code="required",
- )
- self.add_error(password2_field_name, error)
- if password1 and password2 and password1 != password2:
- error = ValidationError(
- self.error_messages["password_mismatch"],
- code="password_mismatch",
- )
- self.add_error(password2_field_name, error)
- def validate_password_for_user(self, user, password_field_name="password2"):
- password = self.cleaned_data.get(password_field_name)
- if password:
- try:
- password_validation.validate_password(password, user)
- except ValidationError as error:
- self.add_error(password_field_name, error)
- def set_password_and_save(self, user, password_field_name="password1", commit=True):
- user.set_password(self.cleaned_data[password_field_name])
- if commit:
- user.save()
- return user
- class SetUnusablePasswordMixin:
- """
- Form mixin that allows setting an unusable password for a user.
- This mixin should be used in combination with `SetPasswordMixin`.
- """
- usable_password_help_text = _(
- "Whether the user will be able to authenticate using a password or not. "
- "If disabled, they may still be able to authenticate using other backends, "
- "such as Single Sign-On or LDAP."
- )
- @staticmethod
- def create_usable_password_field(help_text=usable_password_help_text):
- return forms.ChoiceField(
- label=_("Password-based authentication"),
- required=False,
- initial="true",
- choices={"true": _("Enabled"), "false": _("Disabled")},
- widget=forms.RadioSelect(attrs={"class": "radiolist inline"}),
- help_text=help_text,
- )
- def validate_passwords(
- self,
- *args,
- usable_password_field_name="usable_password",
- **kwargs,
- ):
- usable_password = (
- self.cleaned_data.pop(usable_password_field_name, None) != "false"
- )
- self.cleaned_data["set_usable_password"] = usable_password
- if usable_password:
- super().validate_passwords(*args, **kwargs)
- def validate_password_for_user(self, user, **kwargs):
- if self.cleaned_data["set_usable_password"]:
- super().validate_password_for_user(user, **kwargs)
- def set_password_and_save(self, user, commit=True, **kwargs):
- if self.cleaned_data["set_usable_password"]:
- user = super().set_password_and_save(user, **kwargs, commit=commit)
- else:
- user.set_unusable_password()
- if commit:
- user.save()
- return user
- class BaseUserCreationForm(SetPasswordMixin, forms.ModelForm):
- """
- A form that creates a user, with no privileges, from the given username and
- password.
- This is the documented base class for customizing the user creation form.
- It should be kept mostly unchanged to ensure consistency and compatibility.
- """
- password1, password2 = SetPasswordMixin.create_password_fields()
- class Meta:
- model = User
- fields = ("username",)
- field_classes = {"username": UsernameField}
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- if self._meta.model.USERNAME_FIELD in self.fields:
- self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
- "autofocus"
- ] = True
- def clean(self):
- self.validate_passwords()
- return super().clean()
- def _post_clean(self):
- super()._post_clean()
- # Validate the password after self.instance is updated with form data
- # by super().
- self.validate_password_for_user(self.instance)
- def save(self, commit=True):
- user = super().save(commit=False)
- user = self.set_password_and_save(user, commit=commit)
- if commit and hasattr(self, "save_m2m"):
- self.save_m2m()
- return user
- class UserCreationForm(BaseUserCreationForm):
- def clean_username(self):
- """Reject usernames that differ only in case."""
- username = self.cleaned_data.get("username")
- if (
- username
- and self._meta.model.objects.filter(username__iexact=username).exists()
- ):
- self._update_errors(
- ValidationError(
- {
- "username": self.instance.unique_error_message(
- self._meta.model, ["username"]
- )
- }
- )
- )
- else:
- return username
- class UserChangeForm(forms.ModelForm):
- password = ReadOnlyPasswordHashField(
- label=_("Password"),
- help_text=_(
- "Raw passwords are not stored, so there is no way to see "
- "the user’s password."
- ),
- )
- class Meta:
- model = User
- fields = "__all__"
- field_classes = {"username": UsernameField}
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- password = self.fields.get("password")
- if password:
- if self.instance and not self.instance.has_usable_password():
- password.help_text = _(
- "Enable password-based authentication for this user by setting a "
- "password."
- )
- user_permissions = self.fields.get("user_permissions")
- if user_permissions:
- user_permissions.queryset = user_permissions.queryset.select_related(
- "content_type"
- )
- class AuthenticationForm(forms.Form):
- """
- Base class for authenticating users. Extend this to get a form that accepts
- username/password logins.
- """
- username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True}))
- password = forms.CharField(
- label=_("Password"),
- strip=False,
- widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
- )
- error_messages = {
- "invalid_login": _(
- "Please enter a correct %(username)s and password. Note that both "
- "fields may be case-sensitive."
- ),
- "inactive": _("This account is inactive."),
- }
- def __init__(self, request=None, *args, **kwargs):
- """
- The 'request' parameter is set for custom auth use by subclasses.
- The form data comes in via the standard 'data' kwarg.
- """
- self.request = request
- self.user_cache = None
- super().__init__(*args, **kwargs)
- # Set the max length and label for the "username" field.
- self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
- username_max_length = self.username_field.max_length or 254
- self.fields["username"].max_length = username_max_length
- self.fields["username"].widget.attrs["maxlength"] = username_max_length
- if self.fields["username"].label is None:
- self.fields["username"].label = capfirst(self.username_field.verbose_name)
- def clean(self):
- username = self.cleaned_data.get("username")
- password = self.cleaned_data.get("password")
- if username is not None and password:
- self.user_cache = authenticate(
- self.request, username=username, password=password
- )
- if self.user_cache is None:
- raise self.get_invalid_login_error()
- else:
- self.confirm_login_allowed(self.user_cache)
- return self.cleaned_data
- def confirm_login_allowed(self, user):
- """
- Controls whether the given User may log in. This is a policy setting,
- independent of end-user authentication. This default behavior is to
- allow login by active users, and reject login by inactive users.
- If the given user cannot log in, this method should raise a
- ``ValidationError``.
- If the given user may log in, this method should return None.
- """
- if not user.is_active:
- raise ValidationError(
- self.error_messages["inactive"],
- code="inactive",
- )
- def get_user(self):
- return self.user_cache
- def get_invalid_login_error(self):
- return ValidationError(
- self.error_messages["invalid_login"],
- code="invalid_login",
- params={"username": self.username_field.verbose_name},
- )
- class PasswordResetForm(forms.Form):
- email = forms.EmailField(
- label=_("Email"),
- max_length=254,
- widget=forms.EmailInput(attrs={"autocomplete": "email"}),
- )
- def send_mail(
- self,
- subject_template_name,
- email_template_name,
- context,
- from_email,
- to_email,
- html_email_template_name=None,
- ):
- """
- Send a django.core.mail.EmailMultiAlternatives to `to_email`.
- """
- subject = loader.render_to_string(subject_template_name, context)
- # Email subject *must not* contain newlines
- subject = "".join(subject.splitlines())
- body = loader.render_to_string(email_template_name, context)
- email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
- if html_email_template_name is not None:
- html_email = loader.render_to_string(html_email_template_name, context)
- email_message.attach_alternative(html_email, "text/html")
- email_message.send()
- def get_users(self, email):
- """Given an email, return matching user(s) who should receive a reset.
- This allows subclasses to more easily customize the default policies
- that prevent inactive users and users with unusable passwords from
- resetting their password.
- """
- email_field_name = UserModel.get_email_field_name()
- active_users = UserModel._default_manager.filter(
- **{
- "%s__iexact" % email_field_name: email,
- "is_active": True,
- }
- )
- return (
- u
- for u in active_users
- if u.has_usable_password()
- and _unicode_ci_compare(email, getattr(u, email_field_name))
- )
- def save(
- self,
- domain_override=None,
- subject_template_name="registration/password_reset_subject.txt",
- email_template_name="registration/password_reset_email.html",
- use_https=False,
- token_generator=default_token_generator,
- from_email=None,
- request=None,
- html_email_template_name=None,
- extra_email_context=None,
- ):
- """
- Generate a one-use only link for resetting password and send it to the
- user.
- """
- email = self.cleaned_data["email"]
- if not domain_override:
- current_site = get_current_site(request)
- site_name = current_site.name
- domain = current_site.domain
- else:
- site_name = domain = domain_override
- email_field_name = UserModel.get_email_field_name()
- for user in self.get_users(email):
- user_email = getattr(user, email_field_name)
- context = {
- "email": user_email,
- "domain": domain,
- "site_name": site_name,
- "uid": urlsafe_base64_encode(force_bytes(user.pk)),
- "user": user,
- "token": token_generator.make_token(user),
- "protocol": "https" if use_https else "http",
- **(extra_email_context or {}),
- }
- self.send_mail(
- subject_template_name,
- email_template_name,
- context,
- from_email,
- user_email,
- html_email_template_name=html_email_template_name,
- )
- class SetPasswordForm(SetPasswordMixin, forms.Form):
- """
- A form that lets a user set their password without entering the old
- password
- """
- new_password1, new_password2 = SetPasswordMixin.create_password_fields(
- label1=_("New password"), label2=_("New password confirmation")
- )
- def __init__(self, user, *args, **kwargs):
- self.user = user
- super().__init__(*args, **kwargs)
- def clean(self):
- self.validate_passwords("new_password1", "new_password2")
- self.validate_password_for_user(self.user, "new_password2")
- return super().clean()
- def save(self, commit=True):
- return self.set_password_and_save(self.user, "new_password1", commit=commit)
- class PasswordChangeForm(SetPasswordForm):
- """
- A form that lets a user change their password by entering their old
- password.
- """
- error_messages = {
- **SetPasswordForm.error_messages,
- "password_incorrect": _(
- "Your old password was entered incorrectly. Please enter it again."
- ),
- }
- old_password = forms.CharField(
- label=_("Old password"),
- strip=False,
- widget=forms.PasswordInput(
- attrs={"autocomplete": "current-password", "autofocus": True}
- ),
- )
- field_order = ["old_password", "new_password1", "new_password2"]
- def clean_old_password(self):
- """
- Validate that the old_password field is correct.
- """
- old_password = self.cleaned_data["old_password"]
- if not self.user.check_password(old_password):
- raise ValidationError(
- self.error_messages["password_incorrect"],
- code="password_incorrect",
- )
- return old_password
- class AdminPasswordChangeForm(SetUnusablePasswordMixin, SetPasswordMixin, forms.Form):
- """
- A form used to change the password of a user in the admin interface.
- """
- required_css_class = "required"
- usable_password_help_text = SetUnusablePasswordMixin.usable_password_help_text + (
- '<ul id="id_unusable_warning" class="messagelist"><li class="warning">'
- "If disabled, the current password for this user will be lost.</li></ul>"
- )
- password1, password2 = SetPasswordMixin.create_password_fields()
- def __init__(self, user, *args, **kwargs):
- self.user = user
- super().__init__(*args, **kwargs)
- self.fields["password1"].widget.attrs["autofocus"] = True
- if self.user.has_usable_password():
- self.fields["usable_password"] = (
- SetUnusablePasswordMixin.create_usable_password_field(
- self.usable_password_help_text
- )
- )
- def clean(self):
- self.validate_passwords()
- self.validate_password_for_user(self.user)
- return super().clean()
- def save(self, commit=True):
- """Save the new password."""
- return self.set_password_and_save(self.user, commit=commit)
- @property
- def changed_data(self):
- data = super().changed_data
- if "set_usable_password" in data or "password1" in data and "password2" in data:
- return ["password"]
- return []
- class AdminUserCreationForm(SetUnusablePasswordMixin, UserCreationForm):
- usable_password = SetUnusablePasswordMixin.create_usable_password_field()
|