예제 #1
0
    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