summaryrefslogtreecommitdiff
path: root/passlib/ext
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2016-02-09 11:39:09 -0500
committerEli Collins <elic@assurancetechnologies.com>2016-02-09 11:39:09 -0500
commit79441fd7909b3d158e6432a28791c6689e00a43a (patch)
treeb5e276405a60bb60a5634bb696da45ccecec9b41 /passlib/ext
parentb57e3887ba030a163ee1c98024218dda27b635a1 (diff)
parentbd44009b79cd2c0c2a232d9f0c7fef7e2521f41a (diff)
downloadpasslib-79441fd7909b3d158e6432a28791c6689e00a43a.tar.gz
Merge with stable
Diffstat (limited to 'passlib/ext')
-rw-r--r--passlib/ext/django/models.py29
-rw-r--r--passlib/ext/django/utils.py170
2 files changed, 169 insertions, 30 deletions
diff --git a/passlib/ext/django/models.py b/passlib/ext/django/models.py
index 9ef4188..e60a9cf 100644
--- a/passlib/ext/django/models.py
+++ b/passlib/ext/django/models.py
@@ -113,8 +113,18 @@ def _apply_patch():
if password is None or not is_password_usable(encoded):
return False
ok = password_context.verify(password, encoded)
- if ok and setter and password_context.needs_update(encoded):
- setter(password)
+ if ok and setter:
+ # django's check_password() won't call setter() on a legacy alg if it's explicitly
+ # chosen via 'preferred' kwd (unless alg itself says hash needs updating, of course).
+ # as a hack to replicate this behavior, the following makes a temp copy of the
+ # active context, to ensure the preferred scheme isn't deprecated.
+ test_context = password_context
+ if preferred != "default":
+ scheme = hasher_to_passlib_name(preferred)
+ if password_context._is_deprecated_scheme(scheme):
+ test_context = password_context.copy(default=scheme)
+ if test_context.needs_update(encoded):
+ setter(password)
return ok
#
@@ -145,6 +155,21 @@ def _apply_patch():
kwds['salt'] = salt
return password_context.encrypt(password, **kwds)
+ if VERSION >= (1, 8):
+ from django.utils import lru_cache
+ from passlib.utils.compat import lmap
+
+ @_manager.monkeypatch(HASHERS_PATH)
+ @lru_cache.lru_cache()
+ def get_hashers():
+ """passlib replacement for get_hashers()"""
+ return lmap(get_passlib_hasher, password_context.schemes(resolve=True))
+
+ # NOTE: leaving get_hashers_by_algorithm() unpatched, since it just
+ # proxies get_hashers(). but we do want to wipe it's cache...
+ from django.contrib.auth.hashers import reset_hashers
+ reset_hashers(setting="PASSWORD_HASHERS")
+
@_manager.monkeypatch(HASHERS_PATH)
@_manager.monkeypatch(FORMS_PATH)
def get_hasher(algorithm="default"):
diff --git a/passlib/ext/django/utils.py b/passlib/ext/django/utils.py
index 8e70a51..511427c 100644
--- a/passlib/ext/django/utils.py
+++ b/passlib/ext/django/utils.py
@@ -18,8 +18,8 @@ except ImportError:
from passlib.context import CryptContext
from passlib.exc import PasslibRuntimeWarning
from passlib.registry import get_crypt_handler, list_crypt_handlers
-from passlib.utils import classproperty
-from passlib.utils.compat import get_method_function, iteritems, OrderedDict
+from passlib.utils import memoized_property
+from passlib.utils.compat import get_method_function, iteritems, OrderedDict, native_string_types
# local
__all__ = [
"DJANGO_VERSION",
@@ -153,18 +153,99 @@ def hasher_to_passlib_name(hasher_name):
#=============================================================================
_GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--"
-class _HasherWrapper(object):
- """helper for wrapping passlib handlers in Hasher-compatible class."""
+class ProxyProperty(object):
+ """helper that proxies another attribute"""
- # filled in by subclass, drives the other methods.
- passlib_handler = None
- iterations = None
+ def __init__(self, attr):
+ self.attr = attr
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ cls = obj
+ return getattr(obj, self.attr)
+
+ def __set__(self, obj, value):
+ setattr(obj, self.attr, value)
- @classproperty
- def algorithm(cls):
- assert not hasattr(cls.passlib_handler, "django_name")
- return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name
+ def __delete__(self, obj):
+ delattr(obj, self.attr)
+
+class _PasslibHasherWrapper(object):
+ """
+ adapter which which wraps a :cls:`passlib.ifc.PasswordHash` class,
+ and provides an interface compatible with the Django hasher API.
+ :param passlib_handler:
+ passlib hash handler (e.g. :cls:`passlib.hash.sha256_crypt`.
+ """
+
+ #=====================================================================
+ # instance attrs
+ #=====================================================================
+
+ #: passlib handler that we're adapting.
+ passlib_handler = None
+
+ # NOTE: 'rounds' attr will store variable rounds, IF handler supports it.
+ # 'iterations' will act as proxy, for compatibility with django pbkdf2 hashers.
+ # rounds = None
+ # iterations = None
+
+ #=====================================================================
+ # init
+ #=====================================================================
+ def __init__(self, passlib_handler):
+ # init handler
+ assert not hasattr(passlib_handler, "django_name"), \
+ "bug in get_passlib_hasher() -- handlers that reflect an official django hasher " \
+ "should be used directly"
+ self.passlib_handler = passlib_handler
+
+ # init rounds support
+ if self._has_rounds:
+ self.rounds = passlib_handler.default_rounds
+ self.iterations = ProxyProperty("rounds")
+
+ #=====================================================================
+ # internal methods
+ #=====================================================================
+ def __repr__(self):
+ return "<PasslibHasherWrapper handler=%r>" % self.passlib_handler
+
+ #=====================================================================
+ # internal properties
+ #=====================================================================
+
+ @memoized_property
+ def __name__(self):
+ return "Passlib_%s_PasswordHasher" % self.passlib_handler.name.title()
+
+ @memoized_property
+ def _has_rounds(self):
+ return "rounds" in self.passlib_handler.setting_kwds
+
+ @memoized_property
+ def _translate_kwds(self):
+ """
+ internal helper for safe_summary() --
+ used to translate passlib hash options -> django keywords
+ """
+ out = dict(checksum="hash")
+ if self._has_rounds and "pbkdf2" in self.passlib_handler.name:
+ out['rounds'] = 'iterations'
+ return out
+
+ #=====================================================================
+ # hasher properties
+ #=====================================================================
+
+ @memoized_property
+ def algorithm(self):
+ return PASSLIB_HASHER_PREFIX + self.passlib_handler.name
+
+ #=====================================================================
+ # hasher api
+ #=====================================================================
def salt(self):
# NOTE: passlib's handler.encrypt() should generate new salt each time,
# so this just returns a special constant which tells
@@ -174,18 +255,21 @@ class _HasherWrapper(object):
def verify(self, password, encoded):
return self.passlib_handler.verify(password, encoded)
- def encode(self, password, salt=None, iterations=None):
+ def encode(self, password, salt=None, rounds=None, iterations=None):
kwds = {}
if salt is not None and salt != _GEN_SALT_SIGNAL:
kwds['salt'] = salt
- if iterations is not None:
- kwds['rounds'] = iterations
- elif self.iterations is not None:
- kwds['rounds'] = self.iterations
+ if self._has_rounds:
+ if rounds is not None:
+ kwds['rounds'] = rounds
+ elif iterations is not None:
+ kwds['rounds'] = iterations
+ else:
+ kwds['rounds'] = self.rounds
+ elif rounds is not None or iterations is not None:
+ warn("%s.encrypt(): 'rounds' and 'iterations' are ignored" % self.__name__)
return self.passlib_handler.encrypt(password, **kwds)
- _translate_kwds = dict(checksum="hash", rounds="iterations")
-
def safe_summary(self, encoded):
from django.contrib.auth.hashers import mask_hash
from django.utils.translation import ugettext_noop as _
@@ -204,15 +288,32 @@ class _HasherWrapper(object):
# added in django 1.6
def must_update(self, encoded):
- # TODO: would like to do something useful here,
- # but would require access to password context,
- # which would mean a serious recoding of this ext.
+ # TODO: would like access CryptContext, would need caller to pass it to get_passlib_hasher().
+ # for now (as of passlib 1.6.6), replicating django policy that this returns True
+ # if 'encoded' hash has different rounds value from self.rounds
+ if self._has_rounds:
+ handler = self.passlib_handler
+ if hasattr(handler, "parse_rounds"):
+ rounds = handler.parse_rounds(encoded)
+ if rounds != self.rounds:
+ return True
+ # TODO: for passlib 1.7, could check .needs_update() method.
+ # could also have this whole class create a handler subclass,
+ # which we can proxy the .rounds attr for. this would allow
+ # replacing entirety of the (above) rounds check
return False
+ #=====================================================================
+ # eoc
+ #=====================================================================
+
+#: legacy alias for < 1.6.6
+_HasherWrapper = _PasslibHasherWrapper
+
# cache of hasher wrappers generated by get_passlib_hasher()
_hasher_cache = WeakKeyDictionary()
-def get_passlib_hasher(handler, algorithm=None):
+def get_passlib_hasher(handler, algorithm=None, native_only=False):
"""create *Hasher*-compatible wrapper for specified passlib hash.
This takes in the name of a passlib hash (or the handler object itself),
@@ -226,7 +327,7 @@ def get_passlib_hasher(handler, algorithm=None):
so will probably not be compatible with Django's algorithm format,
so the monkeypatch provided by this plugin must have been applied.
"""
- if isinstance(handler, str):
+ if isinstance(handler, native_string_types):
handler = get_crypt_handler(handler)
if hasattr(handler, "django_name"):
# return native hasher instance
@@ -238,14 +339,15 @@ def get_passlib_hasher(handler, algorithm=None):
# we want to resolve to correct django hasher.
name = algorithm
return _get_hasher(name)
+ if native_only:
+ # caller doesn't want any wrapped hashers.
+ return None
if handler.name == "django_disabled":
raise ValueError("can't wrap unusable-password handler")
try:
return _hasher_cache[handler]
except KeyError:
- name = "Passlib_%s_PasswordHasher" % handler.name.title()
- cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler))
- hasher = _hasher_cache[handler] = cls()
+ hasher = _hasher_cache[handler] = _PasslibHasherWrapper(handler)
return hasher
def _get_hasher(algorithm):
@@ -255,11 +357,23 @@ def _get_hasher(algorithm):
if module is None:
# we haven't patched django, so just import directly
from django.contrib.auth.hashers import get_hasher
- else:
+ return get_hasher(algorithm)
+ elif DJANGO_VERSION < (1,8):
+ # django < 1.8
# we've patched django, so have to use patch manager to retrieve
# original get_hasher() function...
get_hasher = module._manager.getorig("django.contrib.auth.hashers:get_hasher")
- return get_hasher(algorithm)
+ return get_hasher(algorithm)
+ else:
+ # django >= 1.8
+ # we've patched django, but patched at get_hashers() level...
+ # calling original get_hasher() would only land us back here via patched get_hashers().
+ # as non-ideal workaround, have to use original get_hashers()
+ get_hashers = module._manager.getorig("django.contrib.auth.hashers:get_hashers")
+ for hasher in get_hashers():
+ if hasher.algorithm == algorithm:
+ return hasher
+ raise ValueError("unknown hasher: %r" % algorithm)
#=============================================================================
# adapting django hashers -> passlib handlers