From 189346c45a4907479f083c854469a896dcf3b581 Mon Sep 17 00:00:00 2001 From: Eli Collins Date: Mon, 9 Jul 2012 16:48:48 -0400 Subject: _CryptConfig now pre-calculates default scheme for each category, checks against deprecated list (closes issue 39) * also added some unittests to catch 3 cases covered in issue 39, and some others as well. --- CHANGES | 19 ++++++++++---- admin/benchmarks.py | 4 ++- docs/lib/passlib.context.rst | 4 +-- passlib/context.py | 61 ++++++++++++++++++++++++++++++++++++++----- passlib/tests/test_context.py | 61 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 133 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index 64258a3..11d9808 100644 --- a/CHANGES +++ b/CHANGES @@ -9,17 +9,26 @@ Release History Minor bugfix release - * Various :meth:`~passlib.context.CryptContext` methods + * *bugfix*: Various :class:`~passlib.context.CryptContext` methods would incorrectly raise :exc:`TypeError` if passed a :class:`!unicode` - user category under Python 2; for compatibility + user category under Python 2. For consistency they will now be treated the same as the equivalent ``utf-8`` :class:`bytes`. - * Reworked the internals of :class:`CryptContext`'s config compiler. + * *bugfix*: Reworked internals of the :class:`CryptContext` config compiler + to fix a couple of border cases (:issue:`39`): - * *bugfix*: FreeBSD 8.3 added native support for SHA512-Crypt, + - It will now throw a :exc:`ValueError` + if the :ref:`default ` scheme is marked as + :ref:`deprecated `. + - If no default scheme is specified, it will use the first + *non-deprecated* scheme. + - Finally, it will now throw a :exc:`ValueError` if all schemes + are marked as deprecated. + + * *bugfix*: FreeBSD 8.3 added native support for :class:`~passlib.hash.sha256_crypt`, updated unittests and documentation accordingly (:issue:`35`). - * *bugfix:* Fixed bug in passlib.apache unittest which caused test to fail + * *bugfix:* Fixed bug which caused passlib.apache unittest to fail if filesystem had mtime resolution >= 1 second (:issue:`35`). * Various documentation updates and corrections. diff --git a/admin/benchmarks.py b/admin/benchmarks.py index 4e4f9bb..4687f37 100644 --- a/admin/benchmarks.py +++ b/admin/benchmarks.py @@ -6,6 +6,7 @@ parsing was being sped up. it could definitely be improved. #============================================================================= # init script env #============================================================================= +import re import os, sys root = os.path.join(os.path.dirname(__file__), os.path.pardir) sys.path.insert(0, os.curdir) @@ -266,7 +267,8 @@ def main(*args): source = globals() if args: orig = source - source = dict((k,orig[k]) for k in orig if k in args) + source = dict((k,orig[k]) for k in orig + if any(re.match(arg, k) for arg in args)) helper = benchmark.run(source, maxtime=2, bestof=3) for name, secs, precision in helper: print_("%-50s %9s (%d)" % (name, benchmark.pptime(secs), precision)) diff --git a/docs/lib/passlib.context.rst b/docs/lib/passlib.context.rst index 185b183..fad4b1f 100644 --- a/docs/lib/passlib.context.rst +++ b/docs/lib/passlib.context.rst @@ -102,7 +102,7 @@ Options which directly affect the behavior of the CryptContext instance: This option controls which of the configured schemes will be used as the default when encrypting new hashes. This parameter is optional; if omitted, - the first algorithm in ``schemes`` will be used. + the first non-deprecated algorithm in ``schemes`` will be used. You can use the :meth:`~CryptContext.default_scheme` method to retreive the name of the current default scheme. As an example, the following demonstrates the effect @@ -143,7 +143,7 @@ Options which directly affect the behavior of the CryptContext instance: This may also contain a single special value, ``["auto"]``, which will configure the CryptContext instance - to deprecate *all* supported schemes except for the default. + to deprecate *all* supported schemes except for the default scheme. .. seealso:: :ref:`context-migration-example` in the tutorial diff --git a/passlib/context.py b/passlib/context.py index c83f815..867add6 100644 --- a/passlib/context.py +++ b/passlib/context.py @@ -996,6 +996,9 @@ class _CryptConfig(object): # tuple of categories in alphabetical order (not including None) categories = None + # dict mapping category -> default scheme + _default_schemes = None + # dict mapping (scheme, category) -> _CryptRecord _records = None @@ -1009,6 +1012,7 @@ class _CryptConfig(object): def __init__(self, source): self._init_scheme_list(source.get((None,None,"schemes"))) self._init_options(source) + self._init_default_schemes() self._init_records() def _init_scheme_list(self, data): @@ -1226,11 +1230,54 @@ class _CryptConfig(object): return kwds, has_cat_options #=================================================================== - # deprecated & default maps + # deprecated & default schemes #=================================================================== + def _init_default_schemes(self): + """initialize maps containing default scheme for each category. + + have to do this after _init_options(), since the default scheme + is affected by the list of deprecated schemes. + """ + # init maps & locals + get_optionmap = self.get_context_optionmap + default_map = self._default_schemes = get_optionmap("default").copy() + dep_map = get_optionmap("deprecated") + schemes = self.schemes + if not schemes: + return + + # figure out default scheme + deps = dep_map.get(None) or () + default = default_map.get(None) + if not default: + for scheme in schemes: + if scheme not in deps: + default_map[None] = scheme + break + else: + raise ValueError("must have at least one non-deprecated scheme") + elif default in deps: + raise ValueError("default scheme cannot be deprecated") + + # figure out per-category default schemes, + for cat in self.categories: + cdeps = dep_map.get(cat, deps) + cdefault = default_map.get(cat, default) + if not cdefault: + for scheme in schemes: + if scheme not in cdeps: + default_map[cat] = scheme + break + else: + raise ValueError("must have at least one non-deprecated " + "scheme for %r category" % cat) + elif cdefault in cdeps: + raise ValueError("default scheme for %r category " + "cannot be deprecated" % cat) + def default_scheme(self, category): - "return default scheme for specific category" - defaults = self.get_context_optionmap("default") + "return default scheme for specific category" + defaults = self._default_schemes try: return defaults[category] except KeyError: @@ -1238,8 +1285,8 @@ class _CryptConfig(object): if not self.schemes: raise KeyError("no hash schemes configured for this " "CryptContext instance") - return defaults.get(None, self.schemes[0]) - + return defaults[None] + def is_deprecated_with_flag(self, scheme, category): "is scheme deprecated under particular category?" depmap = self.get_context_optionmap("deprecated") @@ -1251,12 +1298,12 @@ class _CryptConfig(object): return scheme != self.default_scheme(cat) else: return scheme in source - value = test(None) + value = test(None) or False if category: alt = test(category) if alt is not None and value != alt: return alt, True - return (value or False), False + return value, False #=================================================================== # CryptRecord objects diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index f90e2b3..71c17d4 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -525,13 +525,54 @@ sha512_crypt__min_rounds = 45000 ## self.assertEqual(getdep(cc), ["md5_crypt"]) # comma sep list - cc = CryptContext(deprecated="md5_crypt,des_crypt", schemes=["md5_crypt", "des_crypt"]) + cc = CryptContext(deprecated="md5_crypt,des_crypt", schemes=["md5_crypt", "des_crypt", "sha256_crypt"]) self.assertEqual(getdep(cc), ["md5_crypt", "des_crypt"]) # values outside of schemes not allowed self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], deprecated=['md5_crypt']) + # deprecating ALL schemes should cause ValueError + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt'], + deprecated=['des_crypt']) + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__deprecated=['des_crypt', 'md5_crypt']) + + # deprecating explicit default scheme should cause ValueError + + # ... default listed as deprecated + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + deprecated="md5_crypt") + + # ... global default deprecated per-category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # ... category default deprecated globally + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + deprecated="md5_crypt") + + # ... category default deprecated in category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # category deplist should shadow default deplist + CryptContext( + schemes=['des_crypt', 'md5_crypt'], + deprecated="md5_crypt", + admin__context__default="md5_crypt", + admin__context__deprecated=[]) + # wrong type self.assertRaises(TypeError, CryptContext, deprecated=123) @@ -544,6 +585,15 @@ sha512_crypt__min_rounds = 45000 self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) self.assertEqual(getdep(cc, "admin"), ["des_crypt"]) + # blank per-category deprecated list, shadowing default list + cc = CryptContext(deprecated=["md5_crypt"], + schemes=["md5_crypt", "des_crypt"], + admin__context__deprecated=[], + ) + self.assertEqual(getdep(cc), ["md5_crypt"]) + self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) + self.assertEqual(getdep(cc, "admin"), []) + def test_23_default(self): "test 'default' context option parsing" @@ -560,6 +610,12 @@ sha512_crypt__min_rounds = 45000 ctx = CryptContext(default=hash.md5_crypt, schemes=["des_crypt", "md5_crypt"]) self.assertEqual(ctx.default_scheme(), "md5_crypt") + # implicit default should be first non-deprecated scheme + ctx = CryptContext(schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "des_crypt") + ctx.update(deprecated="des_crypt") + self.assertEqual(ctx.default_scheme(), "md5_crypt") + # error if not in scheme list self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], default='md5_crypt') @@ -998,6 +1054,9 @@ sha512_crypt__min_rounds = 45000 # border cases #-------------------------------------------------------------- + # unknown hash should throw error + self.assertRaises(ValueError, cc.verify, 'stub', '$6$232323123$1287319827') + # rejects non-string secrets cc = CryptContext(["des_crypt"]) h = refhash = cc.encrypt('stub') -- cgit v1.2.1