diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2015-07-25 20:30:58 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2015-07-25 20:30:58 -0400 |
| commit | 37fcb669f2defb45ba3b637925fa9656ede5aaa7 (patch) | |
| tree | c67bd9529e75ea1dd621e83bf7a9cd9071bc92f3 /passlib | |
| parent | 8ed81847a86fc9a21703e17ed55798abaad3b125 (diff) | |
| download | passlib-37fcb669f2defb45ba3b637925fa9656ede5aaa7.tar.gz | |
HasRounds.using() improved, added UTs.
* added UTs for basic min/max/default options.
still needs vary_rounds & alias tests
* clarified error/warning condtions for some cases,
handled implicit min/max policy settings.
* All HasRounds.using() options now accept values as strings,
to help CryptContext.
* Replaced some dup code in _norm_rounds w/ a call to _clip_to_valid_rounds
* departing from previous CryptContext behavior,
passing an explicit rounds value to encrypt() will now override
the policy limits (w/ a warning)
Diffstat (limited to 'passlib')
| -rw-r--r-- | passlib/tests/test_context.py | 20 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 163 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 182 |
3 files changed, 280 insertions, 85 deletions
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 3140ee2..5dab88e 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -9,6 +9,7 @@ if PY3: from configparser import NoSectionError else: from ConfigParser import NoSectionError +import datetime import logging; log = logging.getLogger(__name__) import os import warnings @@ -16,7 +17,7 @@ import warnings # pkg from passlib import hash from passlib.context import CryptContext, LazyCryptContext -from passlib.exc import PasslibConfigWarning +from passlib.exc import PasslibConfigWarning, PasslibHashWarning from passlib.utils import tick, to_unicode from passlib.utils.compat import irange, u, unicode, str_to_uascii, PY2, PY26 import passlib.utils.handlers as uh @@ -977,7 +978,7 @@ sha512_crypt__min_rounds = 45000 with self.assertWarningList(PasslibConfigWarning): self.assertEqual( cc.encrypt("password", rounds=1999, salt="nacl"), - '$5$rounds=2000$nacl$9/lTZ5nrfPuz8vphznnmHuDGFuvjSNvOEDsGmGfsS97', + '$5$rounds=1999$nacl$nmfwJIxqj0csloAAvSER0B8LU0ERCAbhmMug4Twl609', ) with self.assertWarningList([]): @@ -1211,6 +1212,11 @@ sha512_crypt__min_rounds = 45000 #=================================================================== # rounds options #=================================================================== + + # TODO: now that rounds generation has moved out of _CryptRecord to HasRounds, + # this should just test that we're passing right options to handler.using()... + # and let HasRounds tests (which are a copy of this) deal with things. + # NOTE: the follow tests check how _CryptRecord handles # the min/max/default/vary_rounds options, via the output of # genconfig(). it's assumed encrypt() takes the same codepath. @@ -1228,7 +1234,7 @@ sha512_crypt__min_rounds = 45000 #-------------------------------------------------- # set below handler minimum - with self.assertWarningList([PasslibConfigWarning]*2): + with self.assertWarningList([PasslibHashWarning]*2): c2 = cc.copy(all__min_rounds=500, all__max_rounds=None, all__default_rounds=500) self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$") @@ -1237,7 +1243,7 @@ sha512_crypt__min_rounds = 45000 with self.assertWarningList(PasslibConfigWarning): self.assertEqual( cc.genconfig(rounds=1999, salt="nacl"), - '$5$rounds=2000$nacl$', + '$5$rounds=1999$nacl$', ) # equal to policy minimum @@ -1257,7 +1263,7 @@ sha512_crypt__min_rounds = 45000 #-------------------------------------------------- # set above handler max - with self.assertWarningList([PasslibConfigWarning]*2): + with self.assertWarningList([PasslibHashWarning]*2): c2 = cc.copy(all__max_rounds=int(1e9)+500, all__min_rounds=None, all__default_rounds=int(1e9)+500) @@ -1268,7 +1274,7 @@ sha512_crypt__min_rounds = 45000 with self.assertWarningList(PasslibConfigWarning): self.assertEqual( cc.genconfig(rounds=3001, salt="nacl"), - '$5$rounds=3000$nacl$' + '$5$rounds=3001$nacl$' ) # equal policy max @@ -1326,7 +1332,7 @@ sha512_crypt__min_rounds = 45000 self.assertRaises(ValueError, CryptContext, all__default_rounds='x') # test bad types rejected - bad = NotImplemented + bad = datetime.datetime.now() # picked cause can't be compared to int self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__min_rounds=bad) self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__max_rounds=bad) self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__vary_rounds=bad) diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 89600e8..5540215 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -1268,6 +1268,169 @@ class HandlerCase(TestCase): # TODO: check relaxed mode clips max+1 + def test_has_rounds_using_limits(self): + """ + HasRounds.using() -- desired rounds limits & defaults + """ + self.require_rounds_info() + + #------------------------------------- + # helpers + #------------------------------------- + handler = self.handler + def effective_rounds(cls, rounds=None): + return cls(rounds=rounds, use_defaults=True).rounds + + # create some fake values to test with + orig_min_rounds = handler.min_rounds + orig_max_rounds = handler.max_rounds + orig_default_rounds = handler.default_rounds + medium = ((orig_max_rounds or 9999) + orig_min_rounds) // 2 + if medium == orig_default_rounds: + medium += 1 + small = (orig_min_rounds + medium) // 2 + large = ((orig_max_rounds or 9999) + medium) // 2 + + # create a subclass with small/medium/large as new default desired values + with self.assertWarningList([]): + subcls = handler.using( + min_desired_rounds=small, + max_desired_rounds=large, + default_rounds=medium, + ) + + #------------------------------------- + # sanity check that .using() modified things correctly + #------------------------------------- + + # shouldn't affect original handler at all + self.assertEqual(handler.min_rounds, orig_min_rounds) + self.assertEqual(handler.max_rounds, orig_max_rounds) + self.assertEqual(handler.min_desired_rounds, None) + self.assertEqual(handler.max_desired_rounds, None) + self.assertEqual(handler.default_rounds, orig_default_rounds) + + # should affect subcls' desired value, but not hard min/max + self.assertEqual(subcls.min_rounds, orig_min_rounds) + self.assertEqual(subcls.max_rounds, orig_max_rounds) + self.assertEqual(subcls.default_rounds, medium) + self.assertEqual(subcls.min_desired_rounds, small) + self.assertEqual(subcls.max_desired_rounds, large) + + #------------------------------------- + # min_desired_rounds + #------------------------------------- + + # .using() should clip values below valid minimum, w/ warning + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(min_desired_rounds=orig_min_rounds - 1) + self.assertEqual(temp.min_desired_rounds, orig_min_rounds) + + # .using() should clip values above valid maximum, w/ warning + if orig_max_rounds: + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(min_desired_rounds=orig_max_rounds + 1) + self.assertEqual(temp.min_desired_rounds, orig_max_rounds) + + # .using() should allow values below previous desired minimum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(min_desired_rounds=small - 1) + self.assertEqual(temp.min_desired_rounds, small - 1) + + # .using() should allow values w/in previous range + temp = subcls.using(min_desired_rounds=small + 2) + self.assertEqual(temp.min_desired_rounds, small + 2) + + # .using() should allow values above previous desired maximum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(min_desired_rounds=large + 1) + self.assertEqual(temp.min_desired_rounds, large + 1) + + # encrypt() etc should allow explicit values below desired minimum, w/ warning + self.assertEqual(effective_rounds(subcls, small + 1), small + 1) + self.assertEqual(effective_rounds(subcls, small), small) + with self.assertWarningList([PasslibConfigWarning]): + self.assertEqual(effective_rounds(subcls, small - 1), small - 1) + + # TODO: test 'min_rounds' alias is honored + # TODO: test strings are accepted, bad values like 'x' rejected + + #------------------------------------- + # max_rounds + #------------------------------------- + + # .using() should clip values below valid minimum w/ warning + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(max_desired_rounds=orig_min_rounds - 1) + self.assertEqual(temp.max_desired_rounds, orig_min_rounds) + + # .using() should clip values above valid maximum, w/ warning + if orig_max_rounds: + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(max_desired_rounds=orig_max_rounds + 1) + self.assertEqual(temp.max_desired_rounds, orig_max_rounds) + + # .using() should clip values below previous minimum, w/ warning + with self.assertWarningList([PasslibConfigWarning]): + temp = subcls.using(max_desired_rounds=small - 1) + self.assertEqual(temp.max_desired_rounds, small) + + # .using() should reject explicit min > max + self.assertRaises(ValueError, subcls.using, + min_desired_rounds=medium+1, + max_desired_rounds=medium-1) + + # .using() should allow values w/in previous range + temp = subcls.using(min_desired_rounds=large - 2) + self.assertEqual(temp.min_desired_rounds, large - 2) + + # .using() should allow values above previous desired maximum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(max_desired_rounds=large + 1) + self.assertEqual(temp.max_desired_rounds, large + 1) + + # encrypt() etc should allow explicit values above desired minimum, w/ warning + self.assertEqual(effective_rounds(subcls, large - 1), large - 1) + self.assertEqual(effective_rounds(subcls, large), large) + with self.assertWarningList([PasslibConfigWarning]): + self.assertEqual(effective_rounds(subcls, large + 1), large + 1) + + # TODO: test 'max_rounds' alias is honored + # TODO: test strings are accepted, bad values like 'x' rejected + + #------------------------------------- + # default_rounds + #------------------------------------- + + # XXX: are there any other cases that need testing? + + # implicit default rounds -- increase to min_rounds + temp = subcls.using(min_rounds=medium+1) + self.assertEqual(temp.default_rounds, medium+1) + + # implicit default rounds -- decrease to max_rounds + temp = subcls.using(max_rounds=medium-1) + self.assertEqual(temp.default_rounds, medium-1) + + # explicit default rounds below desired minimum + # XXX: make this a warning if min is implicit? + self.assertRaises(ValueError, subcls.using, default_rounds=small-1) + + # explicit default rounds above desired maximum + # XXX: make this a warning if max is implicit? + if orig_max_rounds: + self.assertRaises(ValueError, subcls.using, default_rounds=large+1) + + # encrypt() etc should implicit default rounds, but get overridden + self.assertEqual(effective_rounds(subcls), medium) + self.assertEqual(effective_rounds(subcls, medium+1), medium+1) + + # TODO: test 'rounds' alias is honored + # TODO: test strings are accepted, bad values like 'x' rejected + + # TODO: HasRounds -- using() -- linear & log vary_rounds. + # borrow code from CryptContext's test_51_linear_vary_rounds & friends + #=================================================================== # idents #=================================================================== diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index be36ec6..445da0e 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -1298,100 +1298,140 @@ class HasRounds(GenericHandler): @classmethod def using(cls, # keyword only... min_desired_rounds=None, max_desired_rounds=None, - default_rounds=None, vary_rounds=None, **kwds): + default_rounds=None, vary_rounds=None, + min_rounds=None, max_rounds=None, rounds=None, # aliases used by CryptContext + **kwds): + # check for aliases used by CryptContext - if "min_rounds" in kwds: - assert min_desired_rounds is None - min_desired_rounds = kwds.pop("min_rounds") - if "max_rounds" in kwds: - assert max_desired_rounds is None - max_desired_rounds = kwds.pop("max_rounds") - if "rounds" in kwds: - assert default_rounds is None - default_rounds = kwds.pop("rounds") + if min_rounds is not None: + if min_desired_rounds is not None: + raise TypeError("'min_rounds' and 'min_desired_rounds' aliases are mutually exclusive") + min_desired_rounds = min_rounds + + if max_rounds is not None: + if max_desired_rounds is not None: + raise TypeError("'max_rounds' and 'max_desired_rounds' aliases are mutually exclusive") + max_desired_rounds = max_rounds + + if rounds is not None: + if default_rounds is not None: + raise TypeError("'rounds' and 'default_rounds' aliases are mutually exclusive") + default_rounds = rounds # generate new subclass subcls = super(HasRounds, cls).using(**kwds) - assert issubclass(subcls, cls) # replace min_desired_rounds - if min_desired_rounds is not None: - # TODO: support string coercion + if min_desired_rounds is None: + explicit_min_rounds = False + min_desired_rounds = cls.min_desired_rounds + else: + explicit_min_rounds = True + if isinstance(min_desired_rounds, native_string_types): + min_desired_rounds = int(min_desired_rounds) if min_desired_rounds < 0: - raise ValueError("%s: min_desired_rounds must be >= 0, got %r" % + raise ValueError("%s: min_desired_rounds (%r) below 0" % (subcls.name, min_desired_rounds)) subcls.min_desired_rounds = subcls._clip_to_valid_rounds(min_desired_rounds, param="min_desired_rounds") # replace max_desired_rounds - if max_desired_rounds is not None: - # TODO: support string coercion + if max_desired_rounds is None: + explicit_max_rounds = False + max_desired_rounds = cls.max_desired_rounds + else: + explicit_max_rounds = True + if isinstance(max_desired_rounds, native_string_types): + max_desired_rounds = int(max_desired_rounds) if min_desired_rounds and max_desired_rounds < min_desired_rounds: - raise ValueError("%s: max_desired_rounds must be >= min_desired_rounds (%r), " - "got %r" % (subcls.name, min_desired_rounds, max_desired_rounds)) + msg = "%s: max_desired_rounds (%r) below min_desired_rounds (%r)" % \ + (subcls.name, max_desired_rounds, min_desired_rounds) + if explicit_min_rounds: + raise ValueError(msg) + else: + warn(msg, PasslibConfigWarning) + max_desired_rounds = min_desired_rounds elif max_desired_rounds < 0: - raise ValueError("%s: max_desired_rounds must be >= 0, got %r" % + raise ValueError("%s: max_desired_rounds (%r) below 0" % (subcls.name, max_desired_rounds)) subcls.max_desired_rounds = subcls._clip_to_valid_rounds(max_desired_rounds, param="max_desired_rounds") # replace default_rounds if default_rounds is not None: - # TODO: support string coercion + if isinstance(default_rounds, native_string_types): + default_rounds = int(default_rounds) if min_desired_rounds and default_rounds < min_desired_rounds: - raise ValueError("%s: default_rounds must be >= min_desired_rounds (%r), got %r" % - (subcls.name, min_desired_rounds, default_rounds)) + raise ValueError("%s: default_rounds (%r) below min_desired_rounds (%r)" % + (subcls.name, default_rounds, min_desired_rounds)) elif max_desired_rounds and default_rounds > max_desired_rounds: - raise ValueError("%s: default_rounds must be <= max_desired_rounds (%r), got %r" % - (subcls.name, max_desired_rounds, default_rounds)) + raise ValueError("%s: default_rounds (%r) above max_desired_rounds (%r)" % + (subcls.name, default_rounds, max_desired_rounds)) subcls.default_rounds = subcls._clip_to_valid_rounds(default_rounds, param="default_rounds") - elif subcls.default_rounds is not None: - # clip existing default rounds to new limits. + + # clip default rounds to new limits. + if subcls.default_rounds is not None: subcls.default_rounds = subcls._clip_to_desired_rounds(subcls.default_rounds) # replace / set vary_rounds if vary_rounds is not None: - # TODO: support string coercion + if isinstance(vary_rounds, native_string_types): + if vary_rounds.endswith("%"): + vary_rounds = float(vary_rounds[:-1]) * 0.01 + else: + vary_rounds = int(vary_rounds) if vary_rounds < 0: - raise ValueError("%s: vary_rounds must be >= 0, got %r" % + raise ValueError("%s: vary_rounds (%r) below 0" % (subcls.name, vary_rounds)) elif isinstance(vary_rounds, float): # TODO: deprecate / disallow vary_rounds=1.0 if vary_rounds > 1: - raise ValueError("%s: vary_rounds must be < 1.0: %r" % + raise ValueError("%s: vary_rounds (%r) above 1.0" % (subcls.name, vary_rounds)) elif not isinstance(vary_rounds, int): raise TypeError("vary_rounds must be int or float") subcls.vary_rounds = vary_rounds - # XXX: could cache _calc_vary_rounds_range() here if needed. - + # XXX: could cache _calc_vary_rounds_range() here if needed, + # but would need to handle user manually changing .default_rounds return subcls @classmethod - def _clip_to_valid_rounds(cls, rounds, param=None): + def _clip_to_valid_rounds(cls, rounds, param="rounds", relaxed=True): """ - helper for :meth:`using` -- - clip rounds value to handle limits. - if param specified, issues warning if clipping is performed. + internal helper -- + clip rounds value to handler's absolute limits (min_rounds / max_rounds) + + :param relaxed: + if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. + if ``False``, raises a ValueError instead. + + :param param: + optional name of parameter to insert into error/warning messages. :returns: - bool indicating if within limits. + clipped rounds value """ - # XXX: could accept strict=True flag to turn this into ValueErrors / - # or relaxed=True to enable warning rather than ValueError behavior. + # check minimum mn = cls.min_rounds if rounds < mn: - if param: - warn("%s: %s value is below handler minimum %d: %d" % - (cls.name, param, mn, rounds), exc.PasslibConfigWarning) - return mn + msg = "%s: %s (%r) below min_rounds (%d)" % (cls.name, param, rounds, mn) + if relaxed: + warn(msg, PasslibHashWarning) + rounds = mn + else: + raise ValueError(msg) + + # check maximum mx = cls.max_rounds if mx and rounds > mx: - if param: - warn("%s: %s value is above handler maximum %d: %d" % - (cls.name, param, cls.max_rounds, rounds), exc.PasslibConfigWarning) - return mx + msg = "%s: %s (%r) above max_rounds (%d)" % (cls.name, param, rounds, mx) + if relaxed: + warn(msg, PasslibHashWarning) + rounds = mx + else: + raise ValueError(msg) + return rounds @classmethod @@ -1400,12 +1440,17 @@ class HasRounds(GenericHandler): helper for :meth:`_generate_rounds` -- clips rounds value to desired min/max set by class (if any) """ + # NOTE: min/max_desired_rounds are None if unset. + # check minimum mnd = cls.min_desired_rounds or 0 if rounds < mnd: return mnd + + # check maximum mxd = cls.max_desired_rounds if mxd and rounds > mxd: return mxd + return rounds @classmethod @@ -1456,9 +1501,10 @@ class HasRounds(GenericHandler): self.rounds = self._norm_rounds(rounds) def _norm_rounds(self, rounds): - """helper routine for normalizing rounds + """ + helper for normalizing rounds value. - :arg rounds: ``None``, or integer cost parameter. + :arg rounds: ``None``, or an integer cost parameter. :raises TypeError: * if ``use_defaults=False`` and no rounds is specified @@ -1475,16 +1521,18 @@ class HasRounds(GenericHandler): :returns: normalized rounds value """ - # fill in default + + # init rounds attr, using default_rounds (etc) if needed explicit = False if rounds is None: if not self.use_defaults: raise TypeError("no rounds specified") - rounds = self._generate_rounds() # NOTE: will throw ValueError if default not set + rounds = self._generate_rounds() # NOTE: will throw ValueError if default not set assert isinstance(rounds, int_types) elif self.use_defaults: - # if rounds is present, was explicitly provided by caller; - # whereas if use_defaults=False, we got here from verify() / genhash() + # warn if rounds is outside desired bounds only if user provided explicit rounds + # to .encrypt() -- hence the .use_defaults check, which will be false if we're + # coming from .verify() / .genhash() explicit = True # check type @@ -1492,41 +1540,19 @@ class HasRounds(GenericHandler): raise exc.ExpectedTypeError(rounds, "integer", "rounds") # check valid bounds - # XXX: combine this with cls._clip_to_valid_rounds() ? - mn = self.min_rounds - if rounds < mn: - msg = "rounds too low (%s requires >= %d rounds)" % (self.name, mn) - if self.relaxed: - warn(msg, PasslibHashWarning) - rounds = mn - else: - raise ValueError(msg) + rounds = self._clip_to_valid_rounds(rounds, relaxed=self.relaxed) - mx = self.max_rounds - if mx and rounds > mx: - msg = "rounds too high (%s requires <= %d rounds)" % (self.name, mx) - if self.relaxed: - warn(msg, PasslibHashWarning) - rounds = mx - else: - raise ValueError(msg) - - # if rounds explicitly specified, warn if outside desired bounds + # if rounds explicitly specified, warn if outside desired bounds, but use it if explicit: - # XXX: combine this with _clip_to_desired_rounds()? mnd = self.min_desired_rounds if mnd and rounds < mnd: - warn("rounds below desired minimum (%d): %d" % (mnd, rounds), + warn("using rounds value (%r) below desired minimum (%d)" % (rounds, mnd), exc.PasslibConfigWarning) - # XXX: remove clipping behavior, and use undesired value if requested to? - rounds = mnd mxd = self.max_desired_rounds if mxd and rounds > mxd: - warn("rounds above desired maximum (%d): %d" % (mxd, rounds), + warn("using rounds value (%r) above desired maximum (%d)" % (rounds, mxd), exc.PasslibConfigWarning) - rounds = mxd - return rounds def _generate_rounds(self): |
