summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2015-07-25 20:30:58 -0400
committerEli Collins <elic@assurancetechnologies.com>2015-07-25 20:30:58 -0400
commit37fcb669f2defb45ba3b637925fa9656ede5aaa7 (patch)
treec67bd9529e75ea1dd621e83bf7a9cd9071bc92f3 /passlib
parent8ed81847a86fc9a21703e17ed55798abaad3b125 (diff)
downloadpasslib-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.py20
-rw-r--r--passlib/tests/utils.py163
-rw-r--r--passlib/utils/handlers.py182
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):