def _load_backend_mixin(mixin_cls, name, dryrun): # try to import pybcrypt global _pybcrypt if not _detect_pybcrypt(): # not installed, or bcrypt installed instead return False try: import bcrypt as _pybcrypt except ImportError: # pragma: no cover # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken) return False # deprecated as of 1.7.2 if not dryrun: warn( "Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; " "Please use `pip install bcrypt` instead", DeprecationWarning) # determine pybcrypt version try: version = _pybcrypt._bcrypt.__version__ except: log.warning("(trapped) error reading pybcrypt version", exc_info=True) version = "<unknown>" log.debug("detected 'pybcrypt' backend, version %r", version) # return calc function based on version vinfo = parse_version(version) or (0, 0) if vinfo < (0, 3): warn( "py-bcrypt %s has a major security vulnerability, " "you should upgrade to py-bcrypt 0.3 immediately." % version, uh.exc.PasslibSecurityWarning) if mixin_cls._calc_lock is None: import threading mixin_cls._calc_lock = threading.Lock() mixin_cls._calc_checksum = get_unbound_method_function( mixin_cls._calc_checksum_threadsafe) return mixin_cls._finalize_backend_mixin(name, dryrun)
class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport): """ Run django's hasher unittests against passlib's extension and workalike implementations """ # ================================================================== # helpers # ================================================================== # port patchAttr() helper method from passlib.tests.utils.TestCase patchAttr = get_unbound_method_function(TestCase.patchAttr) # ================================================================== # custom setup # ================================================================== def setUp(self): # --------------------------------------------------------- # install passlib.ext.django adapter, and get context # --------------------------------------------------------- self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) from passlib.ext.django.models import adapter context = adapter.context # --------------------------------------------------------- # patch tests module to use our versions of patched funcs # (which should be installed in hashers module) # --------------------------------------------------------- from django.contrib.auth import hashers for attr in [ "make_password", "check_password", "identify_hasher", "is_password_usable", "get_hasher" ]: self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr)) # --------------------------------------------------------- # django tests expect empty django_des_crypt salt field # --------------------------------------------------------- from passlib.hash import django_des_crypt self.patchAttr(django_des_crypt, "use_duplicate_salt", False) # --------------------------------------------------------- # install receiver to update scheme list if test changes settings # --------------------------------------------------------- django_to_passlib_name = DjangoTranslator().django_to_passlib_name @receiver(setting_changed, weak=False) def update_schemes(**kwds): if kwds and kwds['setting'] != 'PASSWORD_HASHERS': return assert context is adapter.context schemes = [ django_to_passlib_name(import_string(hash_path)()) for hash_path in settings.PASSWORD_HASHERS ] # workaround for a few tests that only specify hex_md5, # but test for django_salted_md5 format. if "hex_md5" in schemes and "django_salted_md5" not in schemes: schemes.append("django_salted_md5") schemes.append("django_disabled") context.update(schemes=schemes, deprecated="auto") adapter.reset_hashers() self.addCleanup(setting_changed.disconnect, update_schemes) update_schemes() # --------------------------------------------------------- # need password_context to keep up to date with django_hasher.iterations, # which is frequently patched by django tests. # # HACK: to fix this, inserting wrapper around a bunch of context # methods so that any time adapter calls them, # attrs are resynced first. # --------------------------------------------------------- def update_rounds(): """ sync django hasher config -> passlib hashers """ for handler in context.schemes(resolve=True): if 'rounds' not in handler.setting_kwds: continue hasher = adapter.passlib_to_django(handler) if isinstance(hasher, _PasslibHasherWrapper): continue rounds = getattr(hasher, "rounds", None) or \ getattr(hasher, "iterations", None) if rounds is None: continue # XXX: this doesn't modify the context, which would # cause other weirdness (since it would replace handler factories completely, # instead of just updating their state) handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds _in_update = [False] def update_wrapper(wrapped, *args, **kwds): """ wrapper around arbitrary func, that first triggers sync """ if not _in_update[0]: _in_update[0] = True try: update_rounds() finally: _in_update[0] = False return wrapped(*args, **kwds) # sync before any context call for attr in [ "schemes", "handler", "default_scheme", "hash", "verify", "needs_update", "verify_and_update" ]: self.patchAttr(context, attr, update_wrapper, wrap=True) # sync whenever adapter tries to resolve passlib hasher self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True) def tearDown(self): # NOTE: could rely on addCleanup() instead, but need py26 compat self.unload_extension() super(HashersTest, self).tearDown() # ================================================================== # skip a few methods that can't be replicated properly # *want to minimize these as much as possible* # ================================================================== def _OMIT(self): return self.skipTest("omitted by passlib") # XXX: this test registers two classes w/ same algorithm id, # something we don't support -- how does django sanely handle # that anyways? get_hashers_by_algorithm() should throw KeyError, right? test_pbkdf2_upgrade_new_hasher = _OMIT # TODO: support wrapping django's harden-runtime feature? # would help pass their tests. test_check_password_calls_harden_runtime = _OMIT test_bcrypt_harden_runtime = _OMIT test_pbkdf2_harden_runtime = _OMIT