Exemple #1
0
def _init_default_schemes():

    #: pick strongest one for host
    host_best = None
    for name in ["bcrypt", "sha256_crypt"]:
        if registry.has_os_crypt_support(name):
            host_best = name
            break

    # check if we have a bcrypt backend -- otherwise issue warning
    # XXX: would like to not spam this unless the user *requests* apache 24
    bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None
    _warn_no_bcrypt.clear()
    if not bcrypt:
        _warn_no_bcrypt.update([
            "portable_apache_24", "host_apache_24", "linux_apache_24",
            "portable", "host"
        ])

    defaults = dict(
        # strongest hash builtin to specific apache version
        portable_apache_24=bcrypt or "apr_md5_crypt",
        portable_apache_22="apr_md5_crypt",

        # strongest hash across current host & specific apache version
        host_apache_24=bcrypt or host_best or "apr_md5_crypt",
        host_apache_22=host_best or "apr_md5_crypt",

        # strongest hash on a linux host
        linux_apache_24=bcrypt or "sha256_crypt",
        linux_apache_22="sha256_crypt",
    )

    # set latest-apache version aliases
    # XXX: could check for apache install, and pick correct host 22/24 default?
    #      could reuse _detect_htpasswd() helper in UTs
    defaults.update(
        portable=defaults['portable_apache_24'],
        host=defaults['host_apache_24'],
    )
    return defaults
Exemple #2
0
def _init_default_schemes():

    #: pick strongest one for host
    host_best = None
    for name in ["bcrypt", "sha256_crypt"]:
        if registry.has_os_crypt_support(name):
            host_best = name
            break

    # check if we have a bcrypt backend -- otherwise issue warning
    # XXX: would like to not spam this unless the user *requests* apache 24
    bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None
    _warn_no_bcrypt.clear()
    if not bcrypt:
        _warn_no_bcrypt.update(["portable_apache_24", "host_apache_24",
                                "linux_apache_24", "portable", "host"])

    defaults = dict(
        # strongest hash builtin to specific apache version
        portable_apache_24=bcrypt or "apr_md5_crypt",
        portable_apache_22="apr_md5_crypt",

        # strongest hash across current host & specific apache version
        host_apache_24=bcrypt or host_best or "apr_md5_crypt",
        host_apache_22=host_best or "apr_md5_crypt",

        # strongest hash on a linux host
        linux_apache_24=bcrypt or "sha256_crypt",
        linux_apache_22="sha256_crypt",
    )

    # set latest-apache version aliases
    # XXX: could check for apache install, and pick correct host 22/24 default?
    defaults.update(
        portable=defaults['portable_apache_24'],
        host=defaults['host_apache_24'],
    )
    return defaults
Exemple #3
0
class HtpasswdFileTest(TestCase):
    """test HtpasswdFile class"""
    descriptionPrefix = "HtpasswdFile"

    # sample with 4 users
    sample_01 = (b'user2:2CHkkwa2AtqGs\n'
                 b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
                 b'user4:pass4\n'
                 b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n')

    # sample 1 with user 1, 2 deleted; 4 changed
    sample_02 = b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\n'

    # sample 1 with user2 updated, user 1 first entry removed, and user 5 added
    sample_03 = (b'user2:pass2x\n'
                 b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
                 b'user4:pass4\n'
                 b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
                 b'user5:pass5\n')

    # standalone sample with 8-bit username
    sample_04_utf8 = b'user\xc3\xa6:2CHkkwa2AtqGs\n'
    sample_04_latin1 = b'user\xe6:2CHkkwa2AtqGs\n'

    sample_dup = b'user1:pass1\nuser1:pass2\n'

    # sample with bcrypt & sha256_crypt hashes
    sample_05 = (
        b'user2:2CHkkwa2AtqGs\n'
        b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
        b'user4:pass4\n'
        b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
        b'user5:$2a$12$yktDxraxijBZ360orOyCOePFGhuis/umyPNJoL5EbsLk.s6SWdrRO\n'
        b'user6:$5$rounds=110000$cCRp/xUUGVgwR4aP$'
        b'p0.QKFS5qLNRqw1/47lXYiAcgIjJK.WjCO8nrEKuUK.\n')

    def test_00_constructor_autoload(self):
        """test constructor autoload"""
        # check with existing file
        path = self.mktemp()
        set_file(path, self.sample_01)
        ht = apache.HtpasswdFile(path)
        self.assertEqual(ht.to_string(), self.sample_01)
        self.assertEqual(ht.path, path)
        self.assertTrue(ht.mtime)

        # check changing path
        ht.path = path + "x"
        self.assertEqual(ht.path, path + "x")
        self.assertFalse(ht.mtime)

        # check new=True
        ht = apache.HtpasswdFile(path, new=True)
        self.assertEqual(ht.to_string(), b"")
        self.assertEqual(ht.path, path)
        self.assertFalse(ht.mtime)

        # check autoload=False (deprecated alias for new=True)
        with self.assertWarningList("``autoload=False`` is deprecated"):
            ht = apache.HtpasswdFile(path, autoload=False)
        self.assertEqual(ht.to_string(), b"")
        self.assertEqual(ht.path, path)
        self.assertFalse(ht.mtime)

        # check missing file
        os.remove(path)
        self.assertRaises(IOError, apache.HtpasswdFile, path)

        # NOTE: "default_scheme" option checked via set_password() test, among others

    def test_00_from_path(self):
        path = self.mktemp()
        set_file(path, self.sample_01)
        ht = apache.HtpasswdFile.from_path(path)
        self.assertEqual(ht.to_string(), self.sample_01)
        self.assertEqual(ht.path, None)
        self.assertFalse(ht.mtime)

    def test_01_delete(self):
        """test delete()"""
        ht = apache.HtpasswdFile.from_string(self.sample_01)
        self.assertTrue(ht.delete("user1"))  # should delete both entries
        self.assertTrue(ht.delete("user2"))
        self.assertFalse(ht.delete("user5"))  # user not present
        self.assertEqual(ht.to_string(), self.sample_02)

        # invalid user
        self.assertRaises(ValueError, ht.delete, "user:"******"user1")
        self.assertEqual(get_file(path), sample)

        ht = apache.HtpasswdFile(path, autosave=True)
        ht.delete("user1")
        self.assertEqual(get_file(path), b"user2:pass2\n")

    def test_02_set_password(self):
        """test set_password()"""
        ht = apache.HtpasswdFile.from_string(self.sample_01,
                                             default_scheme="plaintext")
        self.assertTrue(ht.set_password("user2", "pass2x"))
        self.assertFalse(ht.set_password("user5", "pass5"))
        self.assertEqual(ht.to_string(), self.sample_03)

        # test legacy default kwd
        with self.assertWarningList("``default`` is deprecated"):
            ht = apache.HtpasswdFile.from_string(self.sample_01,
                                                 default="plaintext")
        self.assertTrue(ht.set_password("user2", "pass2x"))
        self.assertFalse(ht.set_password("user5", "pass5"))
        self.assertEqual(ht.to_string(), self.sample_03)

        # invalid user
        self.assertRaises(ValueError, ht.set_password, "user:"******"pass")

        # test that legacy update() still works
        with self.assertWarningList("update\(\) is deprecated"):
            ht.update("user2", "test")
        self.assertTrue(ht.check_password("user2", "test"))

    def test_02_set_password_autosave(self):
        path = self.mktemp()
        sample = b'user1:pass1\n'
        set_file(path, sample)

        ht = apache.HtpasswdFile(path)
        ht.set_password("user1", "pass2")
        self.assertEqual(get_file(path), sample)

        ht = apache.HtpasswdFile(path,
                                 default_scheme="plaintext",
                                 autosave=True)
        ht.set_password("user1", "pass2")
        self.assertEqual(get_file(path), b"user1:pass2\n")

    def test_02_set_password_default_scheme(self):
        """test set_password() -- default_scheme"""
        def check(scheme):
            ht = apache.HtpasswdFile(default_scheme=scheme)
            ht.set_password("user1", "pass1")
            return ht.context.identify(ht.get_hash("user1"))

        # explicit scheme
        self.assertEqual(check("sha256_crypt"), "sha256_crypt")
        self.assertEqual(check("des_crypt"), "des_crypt")

        # unknown scheme
        self.assertRaises(KeyError, check, "xxx")

        # alias resolution
        self.assertEqual(check("portable"),
                         apache.htpasswd_defaults["portable"])
        self.assertEqual(check("portable_apache_22"),
                         apache.htpasswd_defaults["portable_apache_22"])
        self.assertEqual(check("host_apache_22"),
                         apache.htpasswd_defaults["host_apache_22"])

        # default
        self.assertEqual(check(None),
                         apache.htpasswd_defaults["portable_apache_22"])

    def test_03_users(self):
        """test users()"""
        ht = apache.HtpasswdFile.from_string(self.sample_01)
        ht.set_password("user5", "pass5")
        ht.delete("user3")
        ht.set_password("user3", "pass3")
        self.assertEqual(sorted(ht.users()),
                         ["user1", "user2", "user3", "user4", "user5"])

    def test_04_check_password(self):
        """test check_password()"""
        ht = apache.HtpasswdFile.from_string(self.sample_05)
        self.assertRaises(TypeError, ht.check_password, 1, 'pass9')
        self.assertTrue(ht.check_password("user9", "pass9") is None)

        # users 1..6 of sample_01 run through all the main hash formats,
        # to make sure they're recognized.
        for i in irange(1, 7):
            i = str(i)
            try:
                self.assertTrue(ht.check_password("user" + i, "pass" + i))
                self.assertTrue(
                    ht.check_password("user" + i, "pass9") is False)
            except MissingBackendError:
                if i == "5":
                    # user5 uses bcrypt, which is apparently not available right now
                    continue
                raise

        self.assertRaises(ValueError, ht.check_password, "user:"******"pass")

        # test that legacy verify() still works
        with self.assertWarningList(["verify\(\) is deprecated"] * 2):
            self.assertTrue(ht.verify("user1", "pass1"))
            self.assertFalse(ht.verify("user1", "pass2"))

    def test_05_load(self):
        """test load()"""
        # setup empty file
        path = self.mktemp()
        set_file(path, "")
        backdate_file_mtime(path, 5)
        ha = apache.HtpasswdFile(path, default_scheme="plaintext")
        self.assertEqual(ha.to_string(), b"")

        # make changes, check load_if_changed() does nothing
        ha.set_password("user1", "pass1")
        ha.load_if_changed()
        self.assertEqual(ha.to_string(), b"user1:pass1\n")

        # change file
        set_file(path, self.sample_01)
        ha.load_if_changed()
        self.assertEqual(ha.to_string(), self.sample_01)

        # make changes, check load() overwrites them
        ha.set_password("user5", "pass5")
        ha.load()
        self.assertEqual(ha.to_string(), self.sample_01)

        # test load w/ no path
        hb = apache.HtpasswdFile()
        self.assertRaises(RuntimeError, hb.load)
        self.assertRaises(RuntimeError, hb.load_if_changed)

        # test load w/ dups and explicit path
        set_file(path, self.sample_dup)
        hc = apache.HtpasswdFile()
        hc.load(path)
        self.assertTrue(hc.check_password('user1', 'pass1'))

    # NOTE: load_string() tested via from_string(), which is used all over this file

    def test_06_save(self):
        """test save()"""
        # load from file
        path = self.mktemp()
        set_file(path, self.sample_01)
        ht = apache.HtpasswdFile(path)

        # make changes, check they saved
        ht.delete("user1")
        ht.delete("user2")
        ht.save()
        self.assertEqual(get_file(path), self.sample_02)

        # test save w/ no path
        hb = apache.HtpasswdFile(default_scheme="plaintext")
        hb.set_password("user1", "pass1")
        self.assertRaises(RuntimeError, hb.save)

        # test save w/ explicit path
        hb.save(path)
        self.assertEqual(get_file(path), b"user1:pass1\n")

    def test_07_encodings(self):
        """test 'encoding' kwd"""
        # test bad encodings cause failure in constructor
        self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16")

        # check sample utf-8
        ht = apache.HtpasswdFile.from_string(self.sample_04_utf8,
                                             encoding="utf-8",
                                             return_unicode=True)
        self.assertEqual(ht.users(), [u("user\u00e6")])

        # test deprecated encoding=None
        with self.assertWarningList("``encoding=None`` is deprecated"):
            ht = apache.HtpasswdFile.from_string(self.sample_04_utf8,
                                                 encoding=None)
        self.assertEqual(ht.users(), [b'user\xc3\xa6'])

        # check sample latin-1
        ht = apache.HtpasswdFile.from_string(self.sample_04_latin1,
                                             encoding="latin-1",
                                             return_unicode=True)
        self.assertEqual(ht.users(), [u("user\u00e6")])

    def test_08_get_hash(self):
        """test get_hash()"""
        ht = apache.HtpasswdFile.from_string(self.sample_01)
        self.assertEqual(ht.get_hash("user3"),
                         b"{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=")
        self.assertEqual(ht.get_hash("user4"), b"pass4")
        self.assertEqual(ht.get_hash("user5"), None)

        with self.assertWarningList("find\(\) is deprecated"):
            self.assertEqual(ht.find("user4"), b"pass4")

    def test_09_to_string(self):
        """test to_string"""

        # check with known sample
        ht = apache.HtpasswdFile.from_string(self.sample_01)
        self.assertEqual(ht.to_string(), self.sample_01)

        # test blank
        ht = apache.HtpasswdFile()
        self.assertEqual(ht.to_string(), b"")

    def test_10_repr(self):
        ht = apache.HtpasswdFile("fakepath",
                                 autosave=True,
                                 new=True,
                                 encoding="latin-1")
        repr(ht)

    def test_11_malformed(self):
        self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
                          b'realm:user1:pass1\n')
        self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
                          b'pass1\n')

    def test_12_from_string(self):
        # forbid path kwd
        self.assertRaises(TypeError,
                          apache.HtpasswdFile.from_string,
                          b'',
                          path=None)

    def test_13_whitespace(self):
        """whitespace & comment handling"""

        # per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c),
        # lines that match "^\s*(#.*)?$" should be ignored
        source = to_bytes('\n'
                          'user2:pass2\n'
                          'user4:pass4\n'
                          'user7:pass7\r\n'
                          ' \t \n'
                          'user1:pass1\n'
                          ' # legacy users\n'
                          '#user6:pass6\n'
                          'user5:pass5\n\n')

        # loading should see all users (except user6, who was commented out)
        ht = apache.HtpasswdFile.from_string(source)
        self.assertEqual(sorted(ht.users()),
                         ["user1", "user2", "user4", "user5", "user7"])

        # update existing user
        ht.set_hash("user4", "althash4")
        self.assertEqual(sorted(ht.users()),
                         ["user1", "user2", "user4", "user5", "user7"])

        # add a new user
        ht.set_hash("user6", "althash6")
        self.assertEqual(
            sorted(ht.users()),
            ["user1", "user2", "user4", "user5", "user6", "user7"])

        # delete existing user
        ht.delete("user7")
        self.assertEqual(sorted(ht.users()),
                         ["user1", "user2", "user4", "user5", "user6"])

        # re-serialization should preserve whitespace
        target = to_bytes('\n'
                          'user2:pass2\n'
                          'user4:althash4\n'
                          ' \t \n'
                          'user1:pass1\n'
                          ' # legacy users\n'
                          '#user6:pass6\n'
                          'user5:pass5\n'
                          'user6:althash6\n')
        self.assertEqual(ht.to_string(), target)

    @requires_htpasswd_cmd
    def test_htpasswd_cmd_verify(self):
        """
        verify "htpasswd" command can read output
        """
        path = self.mktemp()
        ht = apache.HtpasswdFile(path=path, new=True)

        def hash_scheme(pwd, scheme):
            return ht.context.handler(scheme).hash(pwd)

        # base scheme
        ht.set_hash("user1", hash_scheme("password", "apr_md5_crypt"))

        # 2.2-compat scheme
        host_no_bcrypt = apache.htpasswd_defaults["host_apache_22"]
        ht.set_hash("user2", hash_scheme("password", host_no_bcrypt))

        # 2.4-compat scheme
        host_best = apache.htpasswd_defaults["host"]
        ht.set_hash("user3", hash_scheme("password", host_best))

        # unsupported scheme -- should always fail to verify
        ht.set_hash("user4", "$xxx$foo$bar$baz")

        # make sure htpasswd properly recognizes hashes
        ht.save()

        self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
        self.assertFalse(_call_htpasswd_verify(path, "user2", "wrong"))
        self.assertFalse(_call_htpasswd_verify(path, "user3", "wrong"))
        self.assertFalse(_call_htpasswd_verify(path, "user4", "wrong"))

        self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
        self.assertTrue(_call_htpasswd_verify(path, "user2", "password"))
        self.assertTrue(_call_htpasswd_verify(path, "user3", "password"))

    @requires_htpasswd_cmd
    @unittest.skipUnless(registry.has_backend("bcrypt"),
                         "bcrypt support required")
    def test_htpasswd_cmd_verify_bcrypt(self):
        """
        verify "htpasswd" command can read bcrypt format

        this tests for regression of issue 95, where we output "$2b$" instead of "$2y$";
        fixed in v1.7.2.
        """
        path = self.mktemp()
        ht = apache.HtpasswdFile(path=path, new=True)

        def hash_scheme(pwd, scheme):
            return ht.context.handler(scheme).hash(pwd)

        ht.set_hash("user1", hash_scheme("password", "bcrypt"))
        ht.save()
        self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
        if HAVE_HTPASSWD_BCRYPT:
            self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
        else:
            # apache2.2 should fail, acting like it's an unknown hash format
            self.assertFalse(_call_htpasswd_verify(path, "user1", "password"))
    def test_config(self):
        """test hashing interface

        this function is run against both the actual django code, to
        verify the assumptions of the unittests are correct;
        and run against the passlib extension, to verify it matches
        those assumptions.
        """
        patched, config = self.patched, self.config
        # this tests the following methods:
        #   User.set_password()
        #   User.check_password()
        #   make_password() -- 1.4 only
        #   check_password()
        #   identify_hasher()
        #   User.has_usable_password()
        #   User.set_unusable_password()
        # XXX: this take a while to run. what could be trimmed?

        #  TODO: get_hasher()

        #=======================================================
        # setup helpers & imports
        #=======================================================
        ctx = self.context
        setter = create_mock_setter()
        PASS1 = "toomanysecrets"
        WRONG1 = "letmein"

        from django.contrib.auth.hashers import (check_password, make_password,
                                                 is_password_usable, identify_hasher)

        #=======================================================
        # make sure extension is configured correctly
        #=======================================================
        if patched:
            # contexts should match
            from passlib.ext.django.models import password_context
            self.assertEqual(password_context.to_dict(resolve=True),
                             ctx.to_dict(resolve=True))

            # should have patched both places
            from django.contrib.auth.models import check_password as check_password2
            self.assertEqual(check_password2, check_password)

        #=======================================================
        # default algorithm
        #=======================================================
        # User.set_password() should use default alg
        user = FakeUser()
        user.set_password(PASS1)
        self.assertTrue(ctx.handler().verify(PASS1, user.password))
        self.assert_valid_password(user)

        # User.check_password() - n/a

        # make_password() should use default alg
        hash = make_password(PASS1)
        self.assertTrue(ctx.handler().verify(PASS1, hash))

        # check_password() - n/a

        #=======================================================
        # empty password behavior
        #=======================================================

        # User.set_password() should use default alg
        user = FakeUser()
        user.set_password('')
        hash = user.password
        self.assertTrue(ctx.handler().verify('', hash))
        self.assert_valid_password(user, hash)

        # User.check_password() should return True
        self.assertTrue(user.check_password(""))
        self.assert_valid_password(user, hash)

        # no make_password()

        # check_password() should return True
        self.assertTrue(check_password("", hash))

        #=======================================================
        # 'unusable flag' behavior
        #=======================================================

        # sanity check via user.set_unusable_password()
        user = FakeUser()
        user.set_unusable_password()
        self.assert_unusable_password(user)

        # ensure User.set_password() sets unusable flag
        user = FakeUser()
        user.set_password(None)
        self.assert_unusable_password(user)

        # User.check_password() should always fail
        self.assertFalse(user.check_password(None))
        self.assertFalse(user.check_password('None'))
        self.assertFalse(user.check_password(''))
        self.assertFalse(user.check_password(PASS1))
        self.assertFalse(user.check_password(WRONG1))
        self.assert_unusable_password(user)

        # make_password() should also set flag
        self.assertTrue(make_password(None).startswith("!"))

        # check_password() should return False (didn't handle disabled under 1.3)
        self.assertFalse(check_password(PASS1, '!'))

        # identify_hasher() and is_password_usable() should reject it
        self.assertFalse(is_password_usable(user.password))
        self.assertRaises(ValueError, identify_hasher, user.password)

        #=======================================================
        # hash=None
        #=======================================================
        # User.set_password() - n/a

        # User.check_password() - returns False
        user = FakeUser()
        user.password = None
        self.assertFalse(user.check_password(PASS1))
        self.assertFalse(user.has_usable_password())

        # make_password() - n/a

        # check_password() - error
        self.assertFalse(check_password(PASS1, None))

        # identify_hasher() - error
        self.assertRaises(TypeError, identify_hasher, None)

        #=======================================================
        # empty & invalid hash values
        # NOTE: django 1.5 behavior change due to django ticket 18453
        # NOTE: passlib integration tries to match current django version
        #=======================================================
        for hash in ("", # empty hash
                     "$789$foo", # empty identifier
                     ):
            # User.set_password() - n/a

            # User.check_password()
            # As of django 1.5, blank OR invalid hash returns False
            user = FakeUser()
            user.password = hash
            self.assertFalse(user.check_password(PASS1))

            # verify hash wasn't changed/upgraded during check_password() call
            self.assertEqual(user.password, hash)
            self.assertEqual(user.pop_saved_passwords(), [])

            # User.has_usable_password()
            self.assertFalse(user.has_usable_password())

            # make_password() - n/a

            # check_password()
            self.assertFalse(check_password(PASS1, hash))

            # identify_hasher() - throws error
            self.assertRaises(ValueError, identify_hasher, hash)

        #=======================================================
        # run through all the schemes in the context,
        # testing various bits of per-scheme behavior.
        #=======================================================
        for scheme in ctx.schemes():
            #-------------------------------------------------------
            # setup constants & imports, pick a sample secret/hash combo
            #-------------------------------------------------------
            handler = ctx.handler(scheme)
            deprecated = ctx.handler(scheme).deprecated
            assert not deprecated or scheme != ctx.default_scheme()
            try:
                testcase = get_handler_case(scheme)
            except exc.MissingBackendError:
                assert scheme in conditionally_available_hashes
                continue
            assert handler_derived_from(handler, testcase.handler)
            if handler.is_disabled:
                continue
            if not registry.has_backend(handler):
                # TODO: move this above get_handler_case(),
                #       and omit MissingBackendError check.
                assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \
                    "%r scheme should always have active backend" % scheme
                continue
            try:
                secret, hash = sample_hashes[scheme]
            except KeyError:
                get_sample_hash = testcase("setUp").get_sample_hash
                while True:
                    secret, hash = get_sample_hash()
                    if secret:  # don't select blank passwords
                        break
            other = 'dontletmein'

            # User.set_password() - n/a

            #-------------------------------------------------------
            # User.check_password()+migration against known hash
            #-------------------------------------------------------
            user = FakeUser()
            user.password = hash

            # check against invalid password
            self.assertFalse(user.check_password(None))
            ##self.assertFalse(user.check_password(''))
            self.assertFalse(user.check_password(other))
            self.assert_valid_password(user, hash)

            # check against valid password
            self.assertTrue(user.check_password(secret))

            # check if it upgraded the hash
            # NOTE: needs_update kept separate in case we need to test rounds.
            needs_update = deprecated
            if needs_update:
                self.assertNotEqual(user.password, hash)
                self.assertFalse(handler.identify(user.password))
                self.assertTrue(ctx.handler().verify(secret, user.password))
                self.assert_valid_password(user, saved=user.password)
            else:
                self.assert_valid_password(user, hash)

            # don't need to check rest for most deployments
            if TEST_MODE(max="default"):
                continue

            #-------------------------------------------------------
            # make_password() correctly selects algorithm
            #-------------------------------------------------------
            alg = DjangoTranslator().passlib_to_django_name(scheme)
            hash2 = make_password(secret, hasher=alg)
            self.assertTrue(handler.verify(secret, hash2))

            #-------------------------------------------------------
            # check_password()+setter against known hash
            #-------------------------------------------------------
            # should call setter only if it needs_update
            self.assertTrue(check_password(secret, hash, setter=setter))
            self.assertEqual(setter.popstate(), [secret] if needs_update else [])

            # should not call setter
            self.assertFalse(check_password(other, hash, setter=setter))
            self.assertEqual(setter.popstate(), [])

            ### check preferred kwd is ignored (feature we don't currently support fully)
            ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey'))
            ##self.assertEqual(setter.popstate(), [secret])

            # TODO: get_hasher()

            #-------------------------------------------------------
            # identify_hasher() recognizes known hash
            #-------------------------------------------------------
            self.assertTrue(is_password_usable(hash))
            name = DjangoTranslator().django_to_passlib_name(identify_hasher(hash).algorithm)
            self.assertEqual(name, scheme)
Exemple #5
0
    def test_config(self):
        """test hashing interface

        this function is run against both the actual django code, to
        verify the assumptions of the unittests are correct;
        and run against the passlib extension, to verify it matches
        those assumptions.
        """
        patched, config = self.patched, self.config
        # this tests the following methods:
        #   User.set_password()
        #   User.check_password()
        #   make_password() -- 1.4 only
        #   check_password()
        #   identify_hasher()
        #   User.has_usable_password()
        #   User.set_unusable_password()
        # XXX: this take a while to run. what could be trimmed?

        #  TODO: get_hasher()

        #=======================================================
        # setup helpers & imports
        #=======================================================
        ctx = self.context
        setter = create_mock_setter()
        PASS1 = "toomanysecrets"
        WRONG1 = "letmein"

        from django.contrib.auth.hashers import (check_password, make_password,
                                                 is_password_usable, identify_hasher)

        #=======================================================
        # make sure extension is configured correctly
        #=======================================================
        if patched:
            # contexts should match
            from passlib.ext.django.models import password_context
            self.assertEqual(password_context.to_dict(resolve=True),
                             ctx.to_dict(resolve=True))

            # should have patched both places
            from django.contrib.auth.models import check_password as check_password2
            self.assertEqual(check_password2, check_password)

        #=======================================================
        # default algorithm
        #=======================================================
        # User.set_password() should use default alg
        user = FakeUser()
        user.set_password(PASS1)
        self.assertTrue(ctx.handler().verify(PASS1, user.password))
        self.assert_valid_password(user)

        # User.check_password() - n/a

        # make_password() should use default alg
        hash = make_password(PASS1)
        self.assertTrue(ctx.handler().verify(PASS1, hash))

        # check_password() - n/a

        #=======================================================
        # empty password behavior
        #=======================================================

        # User.set_password() should use default alg
        user = FakeUser()
        user.set_password('')
        hash = user.password
        self.assertTrue(ctx.handler().verify('', hash))
        self.assert_valid_password(user, hash)

        # User.check_password() should return True
        self.assertTrue(user.check_password(""))
        self.assert_valid_password(user, hash)

        # no make_password()

        # check_password() should return True
        self.assertTrue(check_password("", hash))

        #=======================================================
        # 'unusable flag' behavior
        #=======================================================

        # sanity check via user.set_unusable_password()
        user = FakeUser()
        user.set_unusable_password()
        self.assert_unusable_password(user)

        # ensure User.set_password() sets unusable flag
        user = FakeUser()
        user.set_password(None)
        self.assert_unusable_password(user)

        # User.check_password() should always fail
        self.assertFalse(user.check_password(None))
        self.assertFalse(user.check_password('None'))
        self.assertFalse(user.check_password(''))
        self.assertFalse(user.check_password(PASS1))
        self.assertFalse(user.check_password(WRONG1))
        self.assert_unusable_password(user)

        # make_password() should also set flag
        self.assertTrue(make_password(None).startswith("!"))

        # check_password() should return False (didn't handle disabled under 1.3)
        self.assertFalse(check_password(PASS1, '!'))

        # identify_hasher() and is_password_usable() should reject it
        self.assertFalse(is_password_usable(user.password))
        self.assertRaises(ValueError, identify_hasher, user.password)

        #=======================================================
        # hash=None
        #=======================================================
        # User.set_password() - n/a

        # User.check_password() - returns False
        user = FakeUser()
        user.password = None
        self.assertFalse(user.check_password(PASS1))
        self.assertFalse(user.has_usable_password())

        # make_password() - n/a

        # check_password() - error
        self.assertFalse(check_password(PASS1, None))

        # identify_hasher() - error
        self.assertRaises(TypeError, identify_hasher, None)

        #=======================================================
        # empty & invalid hash values
        # NOTE: django 1.5 behavior change due to django ticket 18453
        # NOTE: passlib integration tries to match current django version
        #=======================================================
        for hash in ("", # empty hash
                     "$789$foo", # empty identifier
                     ):
            # User.set_password() - n/a

            # User.check_password()
            # As of django 1.5, blank OR invalid hash returns False
            user = FakeUser()
            user.password = hash
            self.assertFalse(user.check_password(PASS1))

            # verify hash wasn't changed/upgraded during check_password() call
            self.assertEqual(user.password, hash)
            self.assertEqual(user.pop_saved_passwords(), [])

            # User.has_usable_password()
            self.assertFalse(user.has_usable_password())

            # make_password() - n/a

            # check_password()
            self.assertFalse(check_password(PASS1, hash))

            # identify_hasher() - throws error
            self.assertRaises(ValueError, identify_hasher, hash)

        #=======================================================
        # run through all the schemes in the context,
        # testing various bits of per-scheme behavior.
        #=======================================================
        for scheme in ctx.schemes():
            #-------------------------------------------------------
            # setup constants & imports, pick a sample secret/hash combo
            #-------------------------------------------------------
            handler = ctx.handler(scheme)
            deprecated = ctx.handler(scheme).deprecated
            assert not deprecated or scheme != ctx.default_scheme()
            try:
                testcase = get_handler_case(scheme)
            except exc.MissingBackendError:
                assert scheme in conditionally_available_hashes
                continue
            assert handler_derived_from(handler, testcase.handler)
            if handler.is_disabled:
                continue
            if not registry.has_backend(handler):
                # TODO: move this above get_handler_case(),
                #       and omit MissingBackendError check.
                assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \
                    "%r scheme should always have active backend" % scheme
                continue
            try:
                secret, hash = sample_hashes[scheme]
            except KeyError:
                get_sample_hash = testcase("setUp").get_sample_hash
                while True:
                    secret, hash = get_sample_hash()
                    if secret:  # don't select blank passwords
                        break
            other = 'dontletmein'

            # User.set_password() - n/a

            #-------------------------------------------------------
            # User.check_password()+migration against known hash
            #-------------------------------------------------------
            user = FakeUser()
            user.password = hash

            # check against invalid password
            self.assertFalse(user.check_password(None))
            ##self.assertFalse(user.check_password(''))
            self.assertFalse(user.check_password(other))
            self.assert_valid_password(user, hash)

            # check against valid password
            self.assertTrue(user.check_password(secret))

            # check if it upgraded the hash
            # NOTE: needs_update kept separate in case we need to test rounds.
            needs_update = deprecated
            if needs_update:
                self.assertNotEqual(user.password, hash)
                self.assertFalse(handler.identify(user.password))
                self.assertTrue(ctx.handler().verify(secret, user.password))
                self.assert_valid_password(user, saved=user.password)
            else:
                self.assert_valid_password(user, hash)

            # don't need to check rest for most deployments
            if TEST_MODE(max="default"):
                continue

            #-------------------------------------------------------
            # make_password() correctly selects algorithm
            #-------------------------------------------------------
            alg = DjangoTranslator().passlib_to_django_name(scheme)
            hash2 = make_password(secret, hasher=alg)
            self.assertTrue(handler.verify(secret, hash2))

            #-------------------------------------------------------
            # check_password()+setter against known hash
            #-------------------------------------------------------
            # should call setter only if it needs_update
            self.assertTrue(check_password(secret, hash, setter=setter))
            self.assertEqual(setter.popstate(), [secret] if needs_update else [])

            # should not call setter
            self.assertFalse(check_password(other, hash, setter=setter))
            self.assertEqual(setter.popstate(), [])

            ### check preferred kwd is ignored (feature we don't currently support fully)
            ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey'))
            ##self.assertEqual(setter.popstate(), [secret])

            # TODO: get_hasher()

            #-------------------------------------------------------
            # identify_hasher() recognizes known hash
            #-------------------------------------------------------
            self.assertTrue(is_password_usable(hash))
            name = DjangoTranslator().django_to_passlib_name(identify_hasher(hash).algorithm)
            self.assertEqual(name, scheme)