summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-07-09 16:48:48 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-07-09 16:48:48 -0400
commit189346c45a4907479f083c854469a896dcf3b581 (patch)
treeb3a06941343342553447f7444c043bbc9324d56b
parentb64220472e0a126e84b59b431dd02604d6458695 (diff)
downloadpasslib-189346c45a4907479f083c854469a896dcf3b581.tar.gz
_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.
-rw-r--r--CHANGES19
-rw-r--r--admin/benchmarks.py4
-rw-r--r--docs/lib/passlib.context.rst4
-rw-r--r--passlib/context.py61
-rw-r--r--passlib/tests/test_context.py61
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 <context-default-option>` scheme is marked as
+ :ref:`deprecated <context-deprecated-option>`.
+ - 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')