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
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
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)