diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2016-02-09 11:39:09 -0500 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2016-02-09 11:39:09 -0500 |
commit | 79441fd7909b3d158e6432a28791c6689e00a43a (patch) | |
tree | b5e276405a60bb60a5634bb696da45ccecec9b41 /passlib/ext | |
parent | b57e3887ba030a163ee1c98024218dda27b635a1 (diff) | |
parent | bd44009b79cd2c0c2a232d9f0c7fef7e2521f41a (diff) | |
download | passlib-79441fd7909b3d158e6432a28791c6689e00a43a.tar.gz |
Merge with stable
Diffstat (limited to 'passlib/ext')
-rw-r--r-- | passlib/ext/django/models.py | 29 | ||||
-rw-r--r-- | passlib/ext/django/utils.py | 170 |
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 |