summaryrefslogtreecommitdiff
path: root/passlib/ext/django/utils.py
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2020-10-07 21:33:47 -0400
committerEli Collins <elic@assurancetechnologies.com>2020-10-07 21:33:47 -0400
commita4f23dd8fed25cefc93cb257e00b16502a87dbd4 (patch)
tree08cb5765baddc3c3432f956f8ba549ba6fe21077 /passlib/ext/django/utils.py
parent479d593f8b05f31b59b1448509879d6977d5d580 (diff)
downloadpasslib-a4f23dd8fed25cefc93cb257e00b16502a87dbd4.tar.gz
passlib.ext.django: Updated UTs to work with latest django release
(should fix long-standing issue 98) * test_ext_django: - Simplified "stock config" setup code. It now gets it's "sha_rounds" value from the django source, so we don't have to manually update it every time django changes their default. This should require less maintenance across minor django releases. (Should fix issue 98, and prevent recurrence) - Updated tests to account for quirks in how encoded hashes are handled. Specifically: None, "", and invalid hashes all cause subtly different behaviors across django versions. tests pass against django 1.8 - 3.1. - split "empty hash" test out from the loop it shared with "null hash" test, since the two behave differently. * tox: expanded envlist to explicitly test a bunch more django versions (1.8 - 3.1); and remove some needless "django 2.x + py2" tests * passlib.apps: reformatted django CryptContext declarations; added one for django 2.1 (which dropped "django_bcrypt" it's default list) * passlib.ext.django: - added internal "quirks" helper as central place to track minor edge-case changes between django versions. - passlib_to_django() helper now falls back to searching hasher classes directly, even if patch isn't installed. this allows it to work for django hashers that have been removed from django's default list.
Diffstat (limited to 'passlib/ext/django/utils.py')
-rw-r--r--passlib/ext/django/utils.py70
1 files changed, 55 insertions, 15 deletions
diff --git a/passlib/ext/django/utils.py b/passlib/ext/django/utils.py
index 1d1bd67..2f8a2ef 100644
--- a/passlib/ext/django/utils.py
+++ b/passlib/ext/django/utils.py
@@ -26,13 +26,29 @@ __all__ = [
"DJANGO_VERSION",
"MIN_DJANGO_VERSION",
"get_preset_config",
- "get_django_hasher",
+ "quirks",
]
#: minimum version supported by passlib.ext.django
MIN_DJANGO_VERSION = (1, 8)
#=============================================================================
+# quirk detection
+#=============================================================================
+
+class quirks:
+
+ #: django check_password() started throwing error on encoded=None
+ #: (really identify_hasher did)
+ none_causes_check_password_error = DJANGO_VERSION >= (2, 1)
+
+ #: django is_usable_password() started returning True for password = {None, ""} values.
+ empty_is_usable_password = DJANGO_VERSION >= (2, 1)
+
+ #: django is_usable_password() started returning True for non-hash strings in 2.1
+ invalid_is_usable_password = DJANGO_VERSION >= (2, 1)
+
+#=============================================================================
# default policies
#=============================================================================
@@ -235,6 +251,13 @@ class DjangoTranslator(object):
md5="MD5PasswordHasher",
)
+ if DJANGO_VERSION > (2, 1):
+ # present but disabled by default as of django 2.1; not sure when added,
+ # so not listing it by default.
+ _builtin_django_hashers.update(
+ bcrypt="BCryptPasswordHasher",
+ )
+
def _create_django_hasher(self, django_name):
"""
helper to create new django hasher by name.
@@ -244,17 +267,22 @@ class DjangoTranslator(object):
module = sys.modules.get("passlib.ext.django.models")
if module is None or not module.adapter.patched:
from django.contrib.auth.hashers import get_hasher
- return get_hasher(django_name)
-
- # We've patched django's get_hashers(), so calling django's get_hasher()
- # or get_hashers_by_algorithm() would only land us back here.
- # As non-ideal workaround, have to use original get_hashers(),
- get_hashers = module.adapter._manager.getorig("django.contrib.auth.hashers:get_hashers").__wrapped__
- for hasher in get_hashers():
- if hasher.algorithm == django_name:
- return hasher
-
- # hardcode a few for cases where get_hashers() look won't work.
+ try:
+ return get_hasher(django_name)
+ except ValueError as err:
+ if not str(err).startswith("Unknown password hashing algorithm"):
+ raise
+ else:
+ # We've patched django's get_hashers(), so calling django's get_hasher()
+ # or get_hashers_by_algorithm() would only land us back here.
+ # As non-ideal workaround, have to use original get_hashers(),
+ get_hashers = module.adapter._manager.getorig("django.contrib.auth.hashers:get_hashers").__wrapped__
+ for hasher in get_hashers():
+ if hasher.algorithm == django_name:
+ return hasher
+
+ # hardcode a few for cases where get_hashers() lookup won't work
+ # (mainly, hashers that are present in django, but disabled by their default config)
path = self._builtin_django_hashers.get(django_name)
if path:
if "." not in path:
@@ -545,12 +573,20 @@ class DjangoContextAdapter(DjangoTranslator):
"""
# XXX: this currently ignores "preferred" keyword, since its purpose
# was for hash migration, and that's handled by the context.
+ # XXX: honor "none_causes_check_password_error" quirk for django 2.2+?
+ # seems safer to return False.
if password is None or not self.is_password_usable(encoded):
return False
# verify password
context = self.context
- correct = context.verify(password, encoded)
+ try:
+ correct = context.verify(password, encoded)
+ except exc.UnknownHashError:
+ # As of django 1.5, unidentifiable hashes returns False
+ # (side-effect of django issue 18453)
+ return False
+
if not (correct and setter):
return correct
@@ -591,8 +627,12 @@ class DjangoContextAdapter(DjangoTranslator):
if not self.is_password_usable(hash):
return False
cat = self.get_user_category(user)
- ok, new_hash = self.context.verify_and_update(password, hash,
- category=cat)
+ try:
+ ok, new_hash = self.context.verify_and_update(password, hash, category=cat)
+ except exc.UnknownHashError:
+ # As of django 1.5, unidentifiable hashes returns False
+ # (side-effect of django issue 18453)
+ return False
if ok and new_hash is not None:
# migrate to new hash if needed.
user.password = new_hash