diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2020-10-07 21:33:47 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2020-10-07 21:33:47 -0400 |
commit | a4f23dd8fed25cefc93cb257e00b16502a87dbd4 (patch) | |
tree | 08cb5765baddc3c3432f956f8ba549ba6fe21077 /passlib/ext/django/utils.py | |
parent | 479d593f8b05f31b59b1448509879d6977d5d580 (diff) | |
download | passlib-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.py | 70 |
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 |