diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2012-02-08 22:38:25 -0500 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2012-02-08 22:38:25 -0500 |
| commit | 86a2dc3ed68fcdf7853f5e219541a19b5fcacfff (patch) | |
| tree | 8319e8704f04a23faab1eedfad383d50ff6d3671 /passlib/tests | |
| parent | 4c4615329b64287dabd729e3078ab03cb2bb7442 (diff) | |
| download | passlib-86a2dc3ed68fcdf7853f5e219541a19b5fcacfff.tar.gz | |
large refactor of GenericHandler internals
strict keyword
--------------
* GenericHandler's "strict" keyword had poorly defined semantics;
replaced this with "use_defaults" and "relaxed" keywords.
Most handlers' from_string() method specified strict=True.
This is now the default behavior, use_defaults=True is enabled
only for encrypt() and genconfig(). relaxed=True is enabled
only for specific handlers (and unittests) whose code requires it.
This *does* break backward compat with passlib 1.5 handlers,
but this is mostly and internal class.
* missing required settings now throws a TypeError instead of
a ValueError, to be more in line with std python behavior.
* The norm_xxx functions provided by the GenericHandler mixins
(e.g. norm_salt) have been renamed to _norm_xxx() to reflect their
private nature; and converted from class methods to instance
methods, to simplify their call signature for subclassing.
misc
----
* rewrote GenericHandler unittests to use constructor only,
instead of poking into norm_salt/norm_rounds internals.
* checksum/salt charset checks speed up using set comparison
* some small cleanups to FHSP implementation
Diffstat (limited to 'passlib/tests')
| -rw-r--r-- | passlib/tests/test_context.py | 3 | ||||
| -rw-r--r-- | passlib/tests/test_handlers.py | 45 | ||||
| -rw-r--r-- | passlib/tests/test_utils_handlers.py | 267 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 14 |
4 files changed, 211 insertions, 118 deletions
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 72ee39b..2ee966a 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -781,11 +781,12 @@ class CryptContextTest(TestCase): c2 = cc.replace(all__vary_rounds="100%") self.assert_rounds_range(c2, "bcrypt", 15, 21) - def assert_rounds_range(self, context, scheme, lower, upper, salt="."*22): + def assert_rounds_range(self, context, scheme, lower, upper): "helper to check vary_rounds covers specified range" # NOTE: this runs enough times the min and max *should* be hit, # though there's a faint chance it will randomly fail. handler = context.policy.get_handler(scheme) + salt = handler.default_salt_chars[0:1] * handler.max_salt_size seen = set() for i in irange(300): h = context.genconfig(scheme, salt=salt) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 9da90d0..98a741f 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -110,16 +110,14 @@ class _BCryptTest(HandlerCase): kwds = dict(checksum='8CIhhFCj15KqqFvo/n.Jatx8dJ92f82', salt='VlsfIX9.apXuQBr6tego0.', - rounds=12, ident="2a", strict=True) + rounds=12, ident="2a") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) - - del kwds['strict'] + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #=============================================================== @@ -149,7 +147,7 @@ class _BCryptTest(HandlerCase): self.assertWarningMatches(wlog.pop(0), message_re="^encountered a bcrypt hash with incorrectly set padding bits.*", ) - self.assertFalse(wlog) + self.assertNoWarnings(wlog) def check_padding(hash): "check bcrypt hash doesn't have salt padding bits set" @@ -174,12 +172,14 @@ class _BCryptTest(HandlerCase): with catch_warnings(record=True) as wlog: warnings.simplefilter("always") - hash = bcrypt.genconfig(salt="."*21 + "A.") + hash = bcrypt.genconfig(salt="."*21 + "A.", relaxed=True) + self.assertWarningMatches(wlog.pop(0), message_re="salt too large") check_warning(wlog) self.assertEqual(hash, "$2a$12$" + "." * 22) - hash = bcrypt.genconfig(salt="."*23) - self.assertFalse(wlog) + hash = bcrypt.genconfig(salt="."*23, relaxed=True) + self.assertWarningMatches(wlog.pop(0), message_re="salt too large") + self.assertNoWarnings(wlog) self.assertEqual(hash, "$2a$12$" + "." * 22) #=============================================================== @@ -710,14 +710,13 @@ class NTHashTest(HandlerCase): def test_idents(self): handler = self.handler - kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3", strict=True) + kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - del kwds['strict'] - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #========================================================= @@ -917,14 +916,14 @@ class PHPassTest(HandlerCase): def test_idents(self): handler = self.handler - kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', rounds=9, ident="P", strict=True) + kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', + rounds=9, ident="P") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - del kwds['strict'] - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #========================================================= @@ -1067,8 +1066,8 @@ class ScramTest(HandlerCase): def test_100_algs(self): "test parsing of 'algs' setting" - def parse(source): - return self.handler(algs=source).algs + def parse(algs, **kwds): + return self.handler(algs=algs, use_defaults=True, **kwds).algs # None -> default list self.assertEqual(parse(None), ["sha-1","sha-256","sha-512"]) @@ -1087,7 +1086,7 @@ class ScramTest(HandlerCase): self.assertRaises(ValueError, parse, ["sha-1","shaxxx-890"]) # alg & checksum mutually exclusive. - self.assertRaises(RuntimeError, self.handler, algs=['sha-1'], + self.assertRaises(RuntimeError, parse, ['sha-1'], checksum={"sha-1": b("\x00"*20)}) def test_101_extract_digest_info(self): diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py index fe2667a..def6d3e 100644 --- a/passlib/tests/test_utils_handlers.py +++ b/passlib/tests/test_utils_handlers.py @@ -13,10 +13,10 @@ import warnings from passlib.hash import ldap_md5, sha256_crypt from passlib.registry import _unload_handler_name as unload_handler_name, \ register_crypt_handler, get_crypt_handler -from passlib.exc import MissingBackendError +from passlib.exc import MissingBackendError, PasslibHandlerWarning from passlib.utils import getrandstr, JYTHON, rng, to_unicode from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \ - uascii_to_str, unicode + uascii_to_str, unicode, PY_MAX_25 import passlib.utils.handlers as uh from passlib.tests.utils import HandlerCase, TestCase, catch_warnings, \ dummy_handler_in_registry @@ -25,6 +25,21 @@ from passlib.utils.compat import u log = getLogger(__name__) #========================================================= +# utils +#========================================================= +def _makelang(alphabet, size): + "generate all strings of given size using alphabet" + def helper(size): + if size < 2: + for char in alphabet: + yield char + else: + for char in alphabet: + for tail in helper(size-1): + yield char+tail + return set(helper(size)) + +#========================================================= #test support classes - StaticHandler, GenericHandler, etc #========================================================= class SkeletonTest(TestCase): @@ -95,13 +110,13 @@ class SkeletonTest(TestCase): else: raise ValueError - #check fallback + # check fallback self.assertFalse(d1.identify(None)) self.assertFalse(d1.identify('')) self.assertTrue(d1.identify('a')) self.assertFalse(d1.identify('b')) - #check ident-based + # check ident-based d1.ident = u('!') self.assertFalse(d1.identify(None)) self.assertFalse(d1.identify('')) @@ -109,70 +124,108 @@ class SkeletonTest(TestCase): self.assertFalse(d1.identify('a')) def test_11_norm_checksum(self): - "test GenericHandler.norm_checksum()" + "test GenericHandler checksum handling" + # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = 'x' - self.assertRaises(ValueError, d1.norm_checksum, 'xxx') - self.assertEqual(d1.norm_checksum('xxxx'), 'xxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxyx') + + def norm_checksum(*a, **k): + return d1(*a, **k).checksum + + # too small + self.assertRaises(ValueError, norm_checksum, 'xxx') + + # right size + self.assertEqual(norm_checksum('xxxx'), 'xxxx') + + # too large + self.assertRaises(ValueError, norm_checksum, 'xxxxx') + + # wrong chars + self.assertRaises(ValueError, norm_checksum, 'xxyx') def test_20_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt()" - class d1(uh.HasSalt, uh.GenericHandler): - name = 'd1' - setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = 3 - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - with catch_warnings(): - warnings.filterwarnings("ignore", ".* salt string must be at (least|most) .*", UserWarning) - self.assertEqual(d1.norm_salt('aaaa'), 'aaa') - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertRaises(ValueError, d1.norm_salt, 'aaaa', strict=True) - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 3) - self.assertRaises(ValueError, d1.norm_salt, None, salt_size=5, strict=True) - - def test_21_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt() - with no max_salt_size" + "test GenericHandler + HasSalt mixin" + # setup helpers class d1(uh.HasSalt, uh.GenericHandler): name = 'd1' setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = None - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertEqual(d1.norm_salt('aaaa', strict=True), 'aaaa') - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 5) + min_salt_size = 2 + max_salt_size = 4 + default_salt_size = 3 + salt_chars = 'ab' + + def norm_salt(**k): + return d1(**k).salt + + def gen_salt(sz, **k): + return d1(use_defaults=True, salt_size=sz, **k).salt + + salts2 = _makelang('ab', 2) + salts3 = _makelang('ab', 3) + salts4 = _makelang('ab', 4) + + # check salt=None + self.assertRaises(TypeError, norm_salt) + self.assertRaises(TypeError, norm_salt, salt=None) + self.assertIn(norm_salt(use_defaults=True), salts3) + + # check explicit salts + with catch_warnings(record=True) as wlog: + + # check too-small salts + self.assertRaises(ValueError, norm_salt, salt='') + self.assertRaises(ValueError, norm_salt, salt='a') + self.assertNoWarnings(wlog) + + # check correct salts + self.assertEqual(norm_salt(salt='ab'), 'ab') + self.assertEqual(norm_salt(salt='aba'), 'aba') + self.assertEqual(norm_salt(salt='abba'), 'abba') + self.assertNoWarnings(wlog) + + # check too-large salts + self.assertRaises(ValueError, norm_salt, salt='aaaabb') + self.assertNoWarnings(wlog) + + self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa') + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + #check generated salts + with catch_warnings(record=True) as wlog: + + # check too-small salt size + self.assertRaises(ValueError, gen_salt, 0) + self.assertRaises(ValueError, gen_salt, 1) + self.assertNoWarnings(wlog) + + # check correct salt size + self.assertIn(gen_salt(2), salts2) + self.assertIn(gen_salt(3), salts3) + self.assertIn(gen_salt(4), salts4) + self.assertNoWarnings(wlog) + + # check too-large salt size + self.assertRaises(ValueError, gen_salt, 5) + self.assertNoWarnings(wlog) + + self.assertIn(gen_salt(5, relaxed=True), salts4) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # test with max_salt_size=None + del d1.max_salt_size + with catch_warnings(record=True) as wlog: + self.assertEqual(len(gen_salt(None)), 3) + self.assertEqual(len(gen_salt(5)), 5) + self.assertNoWarnings(wlog) def test_30_norm_rounds(self): - "test GenericHandler+HasRounds: .norm_rounds()" + "test GenericHandler + HasRounds mixin" + # setup helpers class d1(uh.HasRounds, uh.GenericHandler): name = 'd1' setting_kwds = ('rounds',) @@ -180,24 +233,44 @@ class SkeletonTest(TestCase): max_rounds = 3 default_rounds = 2 - #check rounds=None - self.assertEqual(d1.norm_rounds(None), 2) - self.assertRaises(ValueError, d1.norm_rounds, None, strict=True) + def norm_rounds(**k): + return d1(**k).rounds + + # check rounds=None + self.assertRaises(TypeError, norm_rounds) + self.assertRaises(TypeError, norm_rounds, rounds=None) + self.assertEqual(norm_rounds(use_defaults=True), 2) + + # check explicit rounds + with catch_warnings(record=True) as wlog: + # too small + self.assertRaises(ValueError, norm_rounds, rounds=0) + self.assertNoWarnings(wlog) + + self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # just right + self.assertEqual(norm_rounds(rounds=1), 1) + self.assertEqual(norm_rounds(rounds=2), 2) + self.assertEqual(norm_rounds(rounds=3), 3) + self.assertNoWarnings(wlog) - #check small & large rounds - with catch_warnings(): - warnings.filterwarnings("ignore", ".* does not allow (less|more) than \d rounds: .*", UserWarning) - self.assertEqual(d1.norm_rounds(0), 1) - self.assertEqual(d1.norm_rounds(4), 3) - self.assertRaises(ValueError, d1.norm_rounds, 0, strict=True) - self.assertRaises(ValueError, d1.norm_rounds, 4, strict=True) + # too large + self.assertRaises(ValueError, norm_rounds, rounds=4) + self.assertNoWarnings(wlog) - #check no default rounds + self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # check no default rounds d1.default_rounds = None - self.assertRaises(ValueError, d1.norm_rounds, None) + self.assertRaises(TypeError, norm_rounds, use_defaults=True) def test_40_backends(self): - "test GenericHandler+HasManyBackends" + "test GenericHandler + HasManyBackends mixin" class d1(uh.HasManyBackends, uh.GenericHandler): name = 'd1' setting_kwds = () @@ -249,33 +322,36 @@ class SkeletonTest(TestCase): self.assertRaises(ValueError, d1.set_backend, 'c') self.assertRaises(ValueError, d1.has_backend, 'c') - def test_50_bh_norm_ident(self): - "test GenericHandler+HasManyIdents: .norm_ident() & .identify()" + def test_50_norm_ident(self): + "test GenericHandler + HasManyIdents" + # setup helpers class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident',) + default_ident = u("!A") ident_values = [ u("!A"), u("!B") ] ident_aliases = { u("A"): u("!A")} - #check ident=None w/ no default - self.assertIs(d1.norm_ident(None), None) - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) + def norm_ident(**k): + return d1(**k).ident + + # check ident=None + self.assertRaises(TypeError, norm_ident) + self.assertRaises(TypeError, norm_ident, ident=None) + self.assertEqual(norm_ident(use_defaults=True), u('!A')) - #check ident=None w/ default - d1.default_ident = u("!A") - self.assertEqual(d1.norm_ident(None), u('!A')) - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) + # check valid idents + self.assertEqual(norm_ident(ident=u('!A')), u('!A')) + self.assertEqual(norm_ident(ident=u('!B')), u('!B')) + self.assertRaises(ValueError, norm_ident, ident=u('!C')) - #check explicit - self.assertEqual(d1.norm_ident(u('!A')), u('!A')) - self.assertEqual(d1.norm_ident(u('!B')), u('!B')) - self.assertRaises(ValueError, d1.norm_ident, u('!C')) + # check aliases + self.assertEqual(norm_ident(ident=u('A')), u('!A')) - #check aliases - self.assertEqual(d1.norm_ident(u('A')), u('!A')) - self.assertRaises(ValueError, d1.norm_ident, u('B')) + # check invalid idents + self.assertRaises(ValueError, norm_ident, ident=u('B')) - #check identify + # check identify is honoring ident system self.assertTrue(d1.identify(u("!Axxx"))) self.assertTrue(d1.identify(u("!Bxxx"))) self.assertFalse(d1.identify(u("!Cxxx"))) @@ -283,6 +359,10 @@ class SkeletonTest(TestCase): self.assertFalse(d1.identify(u(""))) self.assertFalse(d1.identify(None)) + # check default_ident missing is detected. + d1.default_ident = None + self.assertRaises(AssertionError, norm_ident, use_defaults=True) + #========================================================= #eoc #========================================================= @@ -344,7 +424,7 @@ class PrefixWrapperTest(TestCase): d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}") self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds) - if PY_25_MAX: # lacks __dir__() support + if PY_MAX_25: # __dir__() support not added until py 2.6 self.assertFalse('max_rounds' in dir(d2)) else: self.assertTrue('max_rounds' in dir(d2)) @@ -444,7 +524,7 @@ class SaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler): raise ValueError("not a salted-example hash") if isinstance(hash, bytes): hash = hash.decode("ascii") - return cls(salt=hash[5:-40], checksum=hash[-40:], strict=True) + return cls(salt=hash[5:-40], checksum=hash[-40:]) _stub_checksum = u('0') * 40 @@ -481,9 +561,6 @@ class UnsaltedHashTest(HandlerCase): # behavior, but that's a lot of effort for a non-critical # border case. so just skipping this test instead... self.assertRaises(TypeError, UnsaltedHash, salt='x') - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt=None, strict=True) - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt='xxx', strict=True) - self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1) class SaltedHashTest(HandlerCase): @@ -495,6 +572,12 @@ class SaltedHashTest(HandlerCase): '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'), ] + def test_bad_kwds(self): + self.assertRaises(TypeError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt=None) + self.assertRaises(ValueError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt='xxx') + #========================================================= #EOF #========================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 628c4ee..a89e707 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -10,6 +10,7 @@ import re import os import sys import tempfile +from passlib.exc import PasslibHandlerWarning from passlib.utils.compat import PY2, PY27, PY_MIN_32, PY3 try: @@ -870,8 +871,11 @@ class HandlerCase(TestCase): #make sure salt is truncated exactly where it should be. salt = cc * mx c1 = self.do_genconfig(salt=salt) - c2 = self.do_genconfig(salt=salt + cc) - self.assertEqual(c1,c2) + self.assertRaises(ValueError, self.do_genconfig, salt=salt + cc) + if _has_relaxed_setting(handler): + with catch_warnings(record=True): # issues passlibhandlerwarning + c2 = self.do_genconfig(salt=salt + cc, relaxed=True) + self.assertEqual(c1,c2) #if min_salt supports it, check smaller than mx is NOT truncated if handler.min_salt_size < mx: @@ -1112,6 +1116,12 @@ def _has_possible_crypt_support(handler): 'os_crypt' in handler.backends and \ not hasattr(handler, "orig_prefix") # ignore wrapper classes +def _has_relaxed_setting(handler): + # FIXME: I've been lazy, should probably just add 'relaxed' kwd + # to all handlers that derive from GenericHandler + return 'relaxed' in handler.setting_kwds or issubclass(handler, + uh.GenericHandler) + def create_backend_case(base, name, module="passlib.tests.test_handlers"): "create a test case for specific backend of a multi-backend handler" #get handler, figure out if backend should be tested |
