"""A Django password validator using the Pwned Passwords API to check forcompromised passwords."""# SPDX-License-Identifier: BSD-3-Clauseimportloggingimporttypingfromdjango.contrib.auth.base_userimportAbstractBaseUserfromdjango.contrib.auth.password_validationimportCommonPasswordValidatorfromdjango.core.exceptionsimportValidationErrorfromdjango.utils.functionalimportPromisefromdjango.utils.translationimportgettext_lazyas_fromdjango.utils.translationimportngettextfromdjango.views.decorators.debugimportsensitive_variablesfrom.importapi,exceptionslogger=logging.getLogger(__name__)# A value that is either a string, or a lazy Promise object that will resolve to a# string.Message=typing.Union[str,Promise]# A value that is either a Message, or a 2-tuple of Message providing singular and# plural forms.PluralMessage=typing.Union[Message,typing.Tuple[Message,Message]]
[docs]classPwnedPasswordsValidator:""" Password validator which checks the Pwned Passwords database. """default_error_message=_("This password is too common.")def__init__(self,error_message:typing.Optional[PluralMessage]=None,help_message:typing.Optional[Message]=None,api_client:api.PwnedPasswords=api.default_client,)->None:self.fallback_validator=CommonPasswordValidator()self.help_message=help_messageorself.fallback_validator.get_help_text()error_message=error_messageorself.default_error_messageself.api_client=api_client# If there is no plural, use the same message for both forms.ifisinstance(error_message,(str,Promise)):singular,plural=error_message,error_messageelse:singular,plural=error_messageself.error_message={"singular":singular,"plural":plural}
[docs]@sensitive_variables()defvalidate(self,password:str,user:typing.Optional[AbstractBaseUser]=None):""" Check a proposed password against Pwned Passwords and reject the password if it has been compromised. This method is called by most high-level account-creation and account-editing operations in Django. :raises django.core.exceptions.ValidationError: when the proposed password is compromised. """# pylint: disable=unused-argumenttry:amount=self.api_client.check_password(password)exceptexceptions.PwnedPasswordsError:# HIBP API failure. Instead of allowing a potentially compromised# password, check Django's list of common passwords generated from# the same database.logger.error("Falling back to Django CommonPasswordValidator due ""to error contacting Pwned Passwords.")self.fallback_validator.validate(password)else:ifamount:raiseValidationError(ngettext(self.error_message["singular"],self.error_message["plural"],amount,),params={"amount":amount},code="password_compromised",)
defget_help_text(self)->Message:""" Return help text for this validator. """returnself.help_messagedefdeconstruct(self)->typing.Tuple[str,tuple,dict]:""" Deconstruct this validator instance for serialization by the Django ORM migration framework. Note that only :attr:`error_message` and :attr:`help_message` are serialized and stored in migrations when this validator is attached to a model field; :attr:`api_client`, if present, will not be serialized or stored in migrations, and so in migrations will always be the default :data:`~pwned_passwords_django.api.client` instance. """return("pwned_passwords_django.validators.PwnedPasswordsValidator",(),{"help_message":self.help_message,"error_message":self.error_message},)def__eq__(self,other:object)->bool:""" Equality check for this validator; this method is required to allow this validator to be serializable by the Django ORM migration framework. Note that only :attr:`error_message` and :attr:`help_message` are considered here; two validator instances which differ only in their :attr:`api_client` will compare equal. """ifnotisinstance(other,PwnedPasswordsValidator):returnNotImplementedreturn(self.error_message==other.error_messageandself.help_message==other.help_message)