summaryrefslogtreecommitdiff
path: root/passlib/context.py
diff options
context:
space:
mode:
Diffstat (limited to 'passlib/context.py')
-rw-r--r--passlib/context.py2554
1 files changed, 1725 insertions, 829 deletions
diff --git a/passlib/context.py b/passlib/context.py
index 2eb1a4f..5f84202 100644
--- a/passlib/context.py
+++ b/passlib/context.py
@@ -16,27 +16,57 @@ from time import sleep
from warnings import warn
#site
#libs
-from passlib.exc import PasslibConfigWarning, ExpectedStringError
+from passlib.exc import PasslibConfigWarning, ExpectedStringError, ExpectedTypeError
from passlib.registry import get_crypt_handler, _validate_handler_name
from passlib.utils import is_crypt_handler, rng, saslprep, tick, to_bytes, \
to_unicode
-from passlib.utils.compat import bytes, is_mapping, iteritems, num_types, \
+from passlib.utils.compat import bytes, iteritems, num_types, \
PY3, PY_MIN_32, unicode, SafeConfigParser, \
NativeStringIO, BytesIO, base_string_types
#pkg
#local
__all__ = [
- 'CryptPolicy',
'CryptContext',
+ 'LazyCryptContext',
+ 'CryptPolicy',
]
#=========================================================
-#crypt policy
+# support
#=========================================================
-# NOTE: doing this for security purposes, why would you ever want a fixed salt?
-#: hash settings which aren't allowed to be set via policy
-_forbidden_hash_options = frozenset([ "salt" ])
+# private object to detect unset params
+_UNSET = object()
+
+def _coerce_vary_rounds(value):
+ "parse vary_rounds string to percent as [0,1) float, or integer"
+ if value.endswith("%"):
+ # XXX: deprecate this in favor of raw float?
+ return float(value.rstrip("%"))*.01
+ try:
+ return int(value)
+ except ValueError:
+ return float(value)
+
+# set of options which aren't allowed to be set via policy
+_forbidden_scheme_options = set(["salt"])
+ # 'salt' - not allowed since a fixed salt would defeat the purpose.
+
+# dict containing funcs used to coerce strings to correct type
+# for scheme option keys.
+_coerce_scheme_options = dict(
+ min_rounds=int,
+ max_rounds=int,
+ default_rounds=int,
+ vary_rounds=_coerce_vary_rounds,
+ salt_size=int,
+)
+
+# dict mapping passprep policy name -> implementation
+_passprep_funcs = dict(
+ saslprep=saslprep,
+ raw=lambda s: s,
+)
def _splitcomma(source):
"split comma-separated string into list of strings"
@@ -47,18 +77,37 @@ def _splitcomma(source):
return []
return [ elem.strip() for elem in source.split(",") ]
-#--------------------------------------------------------
-#policy class proper
-#--------------------------------------------------------
+def _is_handler_registered(handler):
+ """detect if handler is registered or a custom handler"""
+ return get_crypt_handler(handler.name, None) is handler
+
+#=========================================================
+# crypt policy
+#=========================================================
+_preamble = ("The CryptPolicy class has been deprecated as of "
+ "Passlib 1.6, and will be removed in Passlib 1.8. ")
+
class CryptPolicy(object):
- """stores configuration options for a CryptContext object.
+ """This class has been deprecated, and will be removed in Passlib 1.8.
+
+ This class previously stored the configuration options for the
+ CryptContext class. In the interest of interface simplification,
+ all of this class' functionality has been rolled into the CryptContext
+ class itself.
- The CryptPolicy class constructor accepts a dictionary
- of keywords, which can include all the options
- listed in the :ref:`list of crypt context options <cryptcontext-options>`.
+ The documentation for this class is now focused on documenting how to
+ migrate to the new api. Additionally, where possible, the deprecation
+ warnings issued by the CryptPolicy methods will list the replacement call
+ that should be used.
Constructors
============
+ CryptPolicy objects can be constructed directly using any of
+ the keywords accepted by :class:`CryptContext`. Direct uses of the
+ :class:`!CryptPolicy` constructor should either pass the keywords
+ directly into the CryptContext constructor, or to :meth:`CryptContext.update`
+ if the policy object was being used to update an existing context object.
+
In addition to passing in keywords directly,
CryptPolicy objects can be constructed by the following methods:
@@ -70,6 +119,9 @@ class CryptPolicy(object):
Introspection
=============
+ All of the informational methods provided by this class have been deprecated
+ by identical or similar methods in the :class:`CryptContext` class:
+
.. automethod:: has_schemes
.. automethod:: schemes
.. automethod:: iter_handlers
@@ -86,660 +138,468 @@ class CryptPolicy(object):
.. automethod:: to_string
.. note::
- Instances of CryptPolicy should be treated as immutable.
+ CryptPolicy are immutable.
Use the :meth:`replace` method to mutate existing instances.
- """
+ .. deprecated:: 1.6
+ """
#=========================================================
#class methods
#=========================================================
-
- # NOTE: CryptPolicy always uses native strings for keys.
- # thus the from_path/from_string methods always treat files as utf-8
- # by default, leave the keys alone under py2, but decode to unicode
- # under py3.
-
@classmethod
def from_path(cls, path, section="passlib", encoding="utf-8"):
- """create new policy from specified section of an ini file.
+ """create a CryptPolicy instance from a local file.
- :arg path: path to ini file
- :param section: option name of section to read from.
- :arg encoding: optional encoding (defaults to utf-8)
+ .. deprecated:: 1.6
- :raises EnvironmentError: if the file cannot be read
+ Creating a new CryptContext from a file, which was previously done via
+ ``CryptContext(policy=CryptPolicy.from_path(path))``, can now be
+ done via ``CryptContext.from_path(path)``.
+ See :meth:`CryptContext.from_path` for details.
- :returns: new CryptPolicy instance.
+ Updating an existing CryptContext from a file, which was previously done
+ ``context.policy = CryptPolicy.from_path(path)``, can now be
+ done via ``context.load_path(path)``.
+ See :meth:`CryptContext.load_path` for details.
"""
- if PY3:
- # for python 3, need to provide a unicode stream,
- # so policy object's keys will be native str type (unicode).
- with open(path, "rt", encoding=encoding) as stream:
- return cls._from_stream(stream, section, path)
- elif encoding in ["utf-8", "ascii"]:
- # for python 2, need to provide utf-8 stream,
- # so policy object's keys will be native str type (utf-8 bytes)
- with open(path, "rb") as stream:
- return cls._from_stream(stream, section, path)
- else:
- # for python 2, need to transcode to utf-8 stream,
- # so policy object's keys will be native str type (utf-8 bytes)
- with open(path, "rb") as fh:
- stream = BytesIO(fh.read().decode(encoding).encode("utf-8"))
- return cls._from_stream(stream, section, path)
+ warn(_preamble +
+ "Instead of ``CryptPolicy.from_path(path)``, "
+ "use ``CryptContext.from_path(path)`` "
+ " or ``context.load_path(path)`` for an existing CryptContext.",
+ DeprecationWarning, stacklevel=2)
+ return cls(_internal_context=CryptContext.from_path(path, section,
+ encoding))
@classmethod
def from_string(cls, source, section="passlib", encoding="utf-8"):
- """create new policy from specified section of an ini-formatted string.
+ """create a CryptPolicy instance from a string.
- :arg source: bytes/unicode string containing ini-formatted content.
- :param section: option name of section to read from.
- :arg encoding: optional encoding if source is bytes (defaults to utf-8)
+ .. deprecated:: 1.6
- :returns: new CryptPolicy instance.
- """
- if PY3:
- source = to_unicode(source, encoding, errname="source")
- else:
- source = to_bytes(source, "utf-8", source_encoding=encoding,
- errname="source")
- return cls._from_stream(NativeStringIO(source), section, "<???>")
+ Creating a new CryptContext from a string, which was previously done via
+ ``CryptContext(policy=CryptPolicy.from_string(data))``, can now be
+ done via ``CryptContext.from_string(data)``.
+ See :meth:`CryptContext.from_string` for details.
- @classmethod
- def _from_stream(cls, stream, section, filename=None):
- "helper for from_string / from_path"
- p = SafeConfigParser()
- if PY_MIN_32:
- # python 3.2 deprecated readfp in favor of read_file
- p.read_file(stream, filename or "<???>")
- else:
- p.readfp(stream, filename or "<???>")
- return cls(dict(p.items(section)))
+ Updating an existing CryptContext from a string, which was previously done
+ ``context.policy = CryptPolicy.from_string(data)``, can now be
+ done via ``context.load(data)``.
+ See :meth:`CryptContext.load` for details.
+ """
+ warn(_preamble +
+ "Instead of ``CryptPolicy.from_string(source)``, "
+ "use ``CryptContext.from_string(source)`` or "
+ "``context.load(source)`` for an existing CryptContext.",
+ DeprecationWarning, stacklevel=2)
+ return cls(_internal_context=CryptContext.from_string(source, section,
+ encoding))
@classmethod
- def from_source(cls, source):
- """create new policy from input.
-
- :arg source:
- source may be a dict, CryptPolicy instance, filepath, or raw string.
-
- the exact type will be autodetected, and the appropriate constructor called.
-
- :raises TypeError: if source cannot be identified.
-
- :returns: new CryptPolicy instance.
+ def from_source(cls, source, _warn=True):
+ """create a CryptPolicy instance from some source.
+
+ this method autodetects the source type, and invokes
+ the appropriate constructor automatically. it attempts
+ to detect whether the source is a configuration string, a filepath,
+ a dictionary, or an existing CryptPolicy instance.
+
+ .. deprecated:: 1.6
+
+ Create a new CryptContext, which could previously be done via
+ ``CryptContext(policy=CryptPolicy.from_source(source))``, should
+ now be done using an explicit method: the :class:`CryptContext`
+ constructor itself, :meth:`CryptContext.from_path`,
+ or :meth:`CryptContext.from_string`.
+
+ Updating an existing CryptContext, which could previously be done via
+ ``context.policy = CryptPolicy.from_source(source)``, should
+ now be done using an explicit method: :meth:`CryptContext.update`,
+ or :meth:`CryptContext.load`.
"""
+ if _warn:
+ warn(_preamble +
+ "Instead of ``CryptPolicy.from_source()``, "
+ "use ``CryptContext.from_string(path)`` "
+ " or ``CryptContext.from_path(source)``, as appropriate.",
+ DeprecationWarning, stacklevel=2)
if isinstance(source, CryptPolicy):
- # NOTE: can just return source unchanged,
- # since we're treating CryptPolicy objects as read-only
return source
-
elif isinstance(source, dict):
- return cls(source)
-
- elif isinstance(source, (bytes,unicode)):
- # FIXME: this autodetection makes me uncomfortable...
- # it assumes none of these chars should be in filepaths,
- # but should be in config string, in order to distinguish them.
- if any(c in source for c in "\n\r\t") or \
- not source.strip(" \t./\;:"):
- return cls.from_string(source)
-
- # other strings should be filepath
- else:
- return cls.from_path(source)
+ return cls(_internal_context=CryptContext(**source))
+ elif not isinstance(source, (bytes,unicode)):
+ raise TypeError("source must be CryptPolicy, dict, config string, "
+ "or file path: %r" % (type(source),))
+ elif any(c in source for c in "\n\r\t") or not source.strip(" \t./\;:"):
+ return cls(_internal_context=CryptContext.from_string(source))
else:
- raise TypeError("source must be CryptPolicy, dict, config string, or file path: %r" % (type(source),))
+ return cls(_internal_context=CryptContext.from_path(source))
@classmethod
- def from_sources(cls, sources):
- """create new policy from list of existing policy objects.
+ def from_sources(cls, sources, _warn=True):
+ """create a CryptPolicy instance by merging multiple sources.
- this method takes multiple sources and composites them on top
- of eachother, returning a single resulting CryptPolicy instance.
- this allows default policies to be specified, and then overridden
- on a per-context basis.
+ each source is interpreted as by :meth:`from_source`,
+ and the results are merged together.
- :arg sources: list of sources to build policy from, elements may be any type accepted by :meth:`from_source`.
+ .. deprecated:: 1.6
- :returns: new CryptPolicy instance
+ Instead of using this method to merge multiple policies together,
+ a :class:`CryptContext` instance should be created, and then
+ the multiple sources merged together via :meth:`CryptContext.load`.
"""
- # check for no sources - should we return blank policy in that case?
+ if _warn:
+ warn(_preamble +
+ "Instead of ``CryptPolicy.from_sources()``, "
+ "use the various CryptContext constructors "
+ " followed by ``context.update()``.",
+ DeprecationWarning, stacklevel=2)
if len(sources) == 0:
- # XXX: er, would returning an empty policy be the right thing here?
raise ValueError("no sources specified")
-
- # check if only one source
if len(sources) == 1:
- return cls.from_source(sources[0])
-
- # else create policy from first source, update options, and rebuild.
- result = _UncompiledCryptPolicy()
- target = result._kwds
+ return cls.from_source(sources[0], _warn=False)
+ kwds = {}
for source in sources:
- policy = _UncompiledCryptPolicy.from_source(source)
- target.update(policy._kwds)
-
- #build new policy
- result._force_compile()
- return result
+ kwds.update(cls.from_source(source, _warn=False)._context.to_dict(resolve=True))
+ return cls(_internal_context=CryptContext(**kwds))
def replace(self, *args, **kwds):
- """return copy of policy, with specified options replaced by new values.
+ """create a new CryptPolicy, optionally updating parts of the
+ existing configuration.
- this is essentially a convience record around :meth:`from_sources`,
- except that it always inserts the current policy
- as the first element in the list;
- this allows easily making minor changes from an existing policy object.
+ .. deprecated:: 1.6
- :param \*args: optional list of sources as accepted by :meth:`from_sources`.
- :param \*\*kwds: optional specific options to override in the new policy.
-
- :returns: new CryptPolicy instance
+ Callers of this method should :meth:`CryptContext.update` or
+ :meth:`CryptContext.copy` instead.
"""
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.replace()``, "
+ "use ``context.update()`` or ``context.copy()``.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "Instead of ``CryptPolicy().replace()``, "
+ "create a CryptContext instance and "
+ "use ``context.update()`` or ``context.copy()``.",
+ DeprecationWarning, stacklevel=2)
sources = [ self ]
if args:
sources.extend(args)
if kwds:
sources.append(kwds)
- return CryptPolicy.from_sources(sources)
+ return CryptPolicy.from_sources(sources, _warn=False)
#=========================================================
#instance attrs
#=========================================================
- #: dict of (category,scheme,key) -> value, representing the original
- # raw keywords passed into constructor. the rest of the policy's data
- # structures are derived from this attribute via _compile()
- _kwds = None
- #: list of user categories in sorted order; first entry is always `None`
- _categories = None
-
- #: list of all schemes specified by `context.schemes`
- _schemes = None
-
- #: list of all handlers specified by `context.schemes`
- _handlers = None
+ # internal CryptContext we're wrapping to handle everything
+ # until this class is removed.
+ _context = None
- #: double-nested dict mapping key -> category -> normalized value.
- _context_options = None
-
- #: triply-nested dict mapping scheme -> category -> key -> normalized value.
- _scheme_options = None
+ # flag indicating this is wrapper generated by the CryptContext.policy
+ # attribute, rather than one created independantly by the application.
+ _stub_policy = False
#=========================================================
# init
#=========================================================
def __init__(self, *args, **kwds):
- if args:
- if len(args) != 1:
- raise TypeError("only one positional argument accepted")
- if kwds:
- raise TypeError("cannot specify positional arg and kwds")
- kwds = args[0]
- # XXX: type check, and accept strings for from_source ?
- parse = self._parse_option_key
- self._kwds = dict((parse(key), value) for key, value in
- iteritems(kwds))
- self._compile()
-
- @staticmethod
- def _parse_option_key(ckey):
- "helper to expand policy keys into ``(category, name, option)`` tuple"
- ##if isinstance(ckey, tuple):
- ## assert len(ckey) == 3, "keys must have 3 parts: %r" % (ckey,)
- ## return ckey
- parts = ckey.split("." if "." in ckey else "__")
- count = len(parts)
- if count == 1:
- return None, None, parts[0]
- elif count == 2:
- scheme, key = parts
- if scheme == "context":
- scheme = None
- return None, scheme, key
- elif count == 3:
- cat, scheme, key = parts
- if cat == "default":
- cat = None
- if scheme == "context":
- scheme = None
- return cat, scheme, key
+ context = kwds.pop("_internal_context", None)
+ if context:
+ assert isinstance(context, CryptContext)
+ self._context = context
+ self._stub_policy = kwds.pop("_stub_policy", False)
+ assert not (args or kwds), "unexpected args: %r %r" % (args,kwds)
else:
- raise TypeError("keys must have less than 3 separators: %r" %
- (ckey,))
+ if args:
+ if len(args) != 1:
+ raise TypeError("only one positional argument accepted")
+ if kwds:
+ raise TypeError("cannot specify positional arg and kwds")
+ kwds = args[0]
+ warn(_preamble +
+ "Instead of constructing a CryptPolicy instance, "
+ "create a CryptContext directly, or use ``context.update()`` "
+ "and ``context.load()`` to reconfigure existing CryptContext "
+ "instances.",
+ DeprecationWarning, stacklevel=2)
+ self._context = CryptContext(**kwds)
#=========================================================
- # compile internal data structures
+ # public interface for examining options
#=========================================================
- def _compile(self):
- "compile internal caches from :attr:`_kwds`"
- source = self._kwds
-
- # build list of handlers & schemes
- handlers = self._handlers = []
- schemes = self._schemes = []
- data = source.get((None,None,"schemes"))
- if isinstance(data, str):
- data = _splitcomma(data)
- if data:
- for elem in data:
- #resolve & validate handler
- if hasattr(elem, "name"):
- handler = elem
- scheme = handler.name
- _validate_handler_name(scheme)
- elif isinstance(elem, str):
- handler = get_crypt_handler(elem)
- scheme = handler.name
- else:
- raise TypeError("scheme must be name or crypt handler, "
- "not %r" % type(elem))
-
- #check scheme hasn't been re-used
- if scheme in schemes:
- raise KeyError("multiple handlers with same name: %r" %
- (scheme,))
-
- #add to handler list
- handlers.append(handler)
- schemes.append(scheme)
-
- # run through all other values in source, normalize them, and store in
- # scheme/context option dictionaries.
- scheme_options = self._scheme_options = {}
- context_options = self._context_options = {}
- norm_scheme_option = self._normalize_scheme_option
- norm_context_option = self._normalize_context_option
- cats = set()
- add_cat = cats.add
- for (cat, scheme, key), value in iteritems(source):
- add_cat(cat)
- if scheme:
- value = norm_scheme_option(key, value)
- if scheme in scheme_options:
- config = scheme_options[scheme]
- if cat in config:
- config[cat][key] = value
- else:
- config[cat] = {key: value}
- else:
- scheme_options[scheme] = {cat: {key: value}}
- elif key == "schemes":
- if cat:
- raise KeyError("'schemes' context option is not allowed "
- "per category")
- continue
- else:
- value = norm_context_option(key, value)
- if key in context_options:
- context_options[key][cat] = value
- else:
- context_options[key] = {cat: value}
-
- # store list of categories
- cats.discard(None)
- self._categories = [None] + sorted(cats)
+ def has_schemes(self):
+ """return True if policy defines *any* schemes for use.
- @staticmethod
- def _normalize_scheme_option(key, value):
- # some hash options can't be specified in the policy, e.g. 'salt'
- if key in _forbidden_hash_options:
- raise KeyError("Passlib does not permit %r handler option "
- "to be set via a policy object" % (key,))
-
- # for hash options, try to coerce everything to an int,
- # since most things are (e.g. the `*_rounds` options).
- elif isinstance(value, str):
- try:
- value = int(value)
- except ValueError:
- pass
- return value
-
- def _normalize_context_option(self, key, value):
- "validate & normalize option value"
- if key == "default":
- if hasattr(value, "name"):
- value = value.name
- schemes = self._schemes
- if schemes and value not in schemes:
- raise KeyError("default scheme not found in policy")
-
- elif key == "deprecated":
- if isinstance(value, str):
- value = _splitcomma(value)
- schemes = self._schemes
- if schemes:
- # if schemes are defined, do quick validation first.
- for scheme in value:
- if scheme not in schemes:
- raise KeyError("deprecated scheme not found "
- "in policy: %r" % (scheme,))
-
- elif key == "min_verify_time":
- warn("'min_verify_time' is deprecated as of Passlib 1.6, will be "
- "ignored in 1.7, and removed in 1.8.", DeprecationWarning)
- value = float(value)
- if value < 0:
- raise ValueError("'min_verify_time' must be >= 0")
+ .. deprecated:: 1.6
+ applications should use ``bool(context.schemes())`` instead.
+ see :meth:`CryptContext.schemes`.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.has_schemes()``, "
+ "use ``bool(context.schemes())``.",
+ DeprecationWarning, stacklevel=2)
else:
- raise KeyError("unknown context keyword: %r" % (key,))
-
- return value
+ warn(_preamble +
+ "Instead of ``CryptPolicy().has_schemes()``, "
+ "create a CryptContext instance and "
+ "use ``bool(context.schemes())``.",
+ DeprecationWarning, stacklevel=2)
+ return bool(self._context.schemes())
- #=========================================================
- # private helpers for reading options
- #=========================================================
- def _get_option(self, scheme, category, key, default=None):
- "get specific option value, without inheritance"
- try:
- if scheme:
- return self._scheme_options[scheme][category][key]
- else:
- return self._context_options[key][category]
- except KeyError:
- return default
+ def iter_handlers(self):
+ """return iterator over handlers defined in policy.
- def _get_handler_options(self, scheme, category):
- "return composite dict of handler options for given scheme + category"
- scheme_options = self._scheme_options
- has_cat_options = False
+ .. deprecated:: 1.6
- # start with options common to all schemes
- common_kwds = scheme_options.get("all")
- if common_kwds is None:
- kwds = {}
+ applications should use ``context.schemes(resolve=True))`` instead.
+ see :meth:`CryptContext.schemes`.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.iter_handlers()``, "
+ "use ``context.schemes(resolve=True)``.",
+ DeprecationWarning, stacklevel=2)
else:
- # start with global options
- tmp = common_kwds.get(None)
- kwds = tmp.copy() if tmp is not None else {}
-
- # add category options
- if category:
- tmp = common_kwds.get(category)
- if tmp is not None:
- kwds.update(tmp)
- has_cat_options = True
-
- # add scheme-specific options
- scheme_kwds = scheme_options.get(scheme)
- if scheme_kwds is not None:
- # add global options
- tmp = scheme_kwds.get(None)
- if tmp is not None:
- kwds.update(tmp)
+ warn(_preamble +
+ "Instead of ``CryptPolicy().iter_handlers()``, "
+ "create a CryptContext instance and "
+ "use ``context.schemes(resolve=True)``.",
+ DeprecationWarning, stacklevel=2)
+ return self._context.schemes(resolve=True)
- # add category options
- if category:
- tmp = scheme_kwds.get(category)
- if tmp is not None:
- kwds.update(tmp)
- has_cat_options = True
+ def schemes(self, resolve=False):
+ """return list of schemes defined in policy.
- # add context options
- context_options = self._context_options
- if context_options is not None:
- # add deprecated flag
- dep_map = context_options.get("deprecated")
- if dep_map:
- deplist = dep_map.get(None)
- dep = (deplist is not None and scheme in deplist)
- if category:
- deplist = dep_map.get(category)
- if deplist is not None:
- value = (scheme in deplist)
- if value != dep:
- dep = value
- has_cat_options = True
- if dep:
- kwds['deprecated'] = True
-
- # add min_verify_time flag
- mvt_map = context_options.get("min_verify_time")
- if mvt_map:
- mvt = mvt_map.get(None)
- if category:
- value = mvt_map.get(category)
- if value is not None and value != mvt:
- mvt = value
- has_cat_options = True
- if mvt:
- kwds['min_verify_time'] = mvt
+ .. deprecated:: 1.6
- return kwds, has_cat_options
+ applications should use :meth:`CryptContext.schemes` instead.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.schemes()``, "
+ "use ``context.schemes()``.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "Instead of ``CryptPolicy().schemes()``, "
+ "create a CryptContext instance and "
+ "use ``context.schemes()``.",
+ DeprecationWarning, stacklevel=2)
+ return list(self._context.schemes(resolve=resolve))
- #=========================================================
- # public interface for examining options
- #=========================================================
- def has_schemes(self):
- "check if policy supports *any* schemes; returns True/False"
- return len(self._handlers) > 0
+ def get_handler(self, name=None, category=None, required=False):
+ """return handler as specified by name, or default handler.
- def iter_handlers(self):
- "iterate through handlers for all schemes in policy"
- return iter(self._handlers)
+ .. deprecated:: 1.6
- def schemes(self, resolve=False):
- "return list of supported schemes; if resolve=True, returns list of handlers instead"
- if resolve:
- return list(self._handlers)
+ applications should use :meth:`CryptContext.handler` instead,
+ though note that the ``required`` keyword has been removed,
+ and the new method will always act as if ``required=True``.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.get_handler()``, "
+ "use ``context.handler()``.",
+ DeprecationWarning, stacklevel=2)
else:
- return list(self._schemes)
-
- def get_handler(self, name=None, category=None, required=False):
- """given the name of a scheme, return handler which manages it.
+ warn(_preamble +
+ "Instead of ``CryptPolicy().get_handler()``, "
+ "create a CryptContext instance and "
+ "use ``context.handler()``.",
+ DeprecationWarning, stacklevel=2)
+ if not name: # CryptContext.handler() uses different default value
+ name = "default"
+ # CryptContext.handler() doesn't support required=False,
+ # so wrapping it in try/except
+ try:
+ return self._context.handler(name, category)
+ except KeyError:
+ if required:
+ raise
+ else:
+ return None
- :arg name: name of scheme, or ``None``
- :param category: optional user category
- :param required: if ``True``, raises KeyError if name not found, instead of returning ``None``.
+ def get_min_verify_time(self, category=None):
+ """get min_verify_time setting for policy.
- if name is not specified, attempts to return default handler.
- if returning default, and category is specified, returns category-specific default if set.
+ .. deprecated:: 1.6
- :returns: handler attached to specified name or None
+ min_verify_time will be removed entirely in passlib 1.8
"""
- if name is None:
- name = self._get_option(None, category, "default")
- if not name and category:
- name = self._get_option(None, None, "default")
- if not name and self._handlers:
- return self._handlers[0]
- if not name:
- if required:
- raise KeyError("no crypt algorithms found in policy")
- else:
- return None
- for handler in self._handlers:
- if handler.name == name:
- return handler
- if required:
- raise KeyError("crypt algorithm not found in policy: %r" % (name,))
- else:
- return None
+ warn("get_min_verify_time() and min_verify_time option is deprecated, "
+ "and will be removed in Passlib 1.8", DeprecationWarning,
+ stacklevel=2)
+ mvtmap = self._context._mvtmap
+ if category:
+ try:
+ return mvtmap[category]
+ except KeyError:
+ pass
+ return mvtmap.get(None) or 0
def get_options(self, name, category=None):
- """return dict of options for specified scheme
+ """return dictionary of options specific to a given handler.
- :arg name: name of scheme, or handler instance itself
- :param category: optional user category whose options should be returned
+ .. deprecated:: 1.6
- :returns: dict of options for CryptContext internals which are relevant to this name/category combination.
+ this method has no direct replacement in the 1.6 api, as there
+ is not a clearly defined use-case. however, examining the output of
+ :meth:`CryptContext.to_dict` should serve as the closest alternative.
"""
- # XXX: deprecate / enhance this function ?
+ # XXX: might make a public replacement, but need more study of the use cases.
+ if self._stub_policy:
+ warn(_preamble +
+ "``context.policy.get_options()`` will no longer be available.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "``CryptPolicy().get_options()`` will no longer be available.",
+ DeprecationWarning, stacklevel=2)
if hasattr(name, "name"):
name = name.name
- return self._get_handler_options(name, category)[0]
+ return self._context._get_record_options(name, category)[0]
def handler_is_deprecated(self, name, category=None):
- "check if scheme is marked as deprecated according to this policy; returns True/False"
- # XXX: deprecate this function ?
+ """check if handler has been deprecated by policy.
+
+ .. deprecated:: 1.6
+
+ this method has no direct replacement in the 1.6 api, as there
+ is not a clearly defined use-case. however, examining the output of
+ :meth:`CryptContext.to_dict` should serve as the closest alternative.
+ """
+ # XXX: might make a public replacement, but need more study of the use cases.
+ if self._stub_policy:
+ warn(_preamble +
+ "``context.policy.handler_is_deprecated()`` will no longer be available.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "``CryptPolicy().handler_is_deprecated()`` will no longer be available.",
+ DeprecationWarning, stacklevel=2)
if hasattr(name, "name"):
name = name.name
- kwds = self._get_handler_options(name, category)[0]
- return bool(kwds.get("deprecated"))
-
- def get_min_verify_time(self, category=None):
- warn("get_min_verify_time is deprecated, and will be removed in "
- "Passlib 1.8", DeprecationWarning)
- kwds = self._get_handler_options("all", category)[0]
- return kwds.get("min_verify_time") or 0
+ return self._context._is_deprecated_scheme(name, category)
#=========================================================
# serialization
#=========================================================
- ##def __iter__(self):
- ## return self.iter_config(resolve=True)
-
def iter_config(self, ini=False, resolve=False):
- """iterate through key/value pairs of policy configuration
-
- :param ini:
- If ``True``, returns data formatted for insertion
- into INI file. Keys use ``.`` separator instead of ``__``;
- lists of handlers are returned as comma-separated strings.
+ """iterate over key/value pairs representing the policy object.
- :param resolve:
- If ``True``, returns handler objects instead of handler
- names where appropriate. Ignored if ``ini=True``.
+ .. deprecated:: 1.6
- :returns:
- iterator which yields (key,value) pairs.
+ applications should use :meth:`CryptContext.to_dict` instead.
"""
- #
- #prepare formatting functions
- #
- sep = "." if ini else "__"
-
- def format_key(cat, name, key):
- if cat:
- return sep.join([cat, name or "context", key])
- if name:
- return sep.join([name, key])
- return key
-
- def encode_list(hl):
- if ini:
- return ", ".join(hl)
- else:
- return list(hl)
-
- #
- #run through contents of internal configuration
- #
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.iter_config()``, "
+ "use ``context.to_dict().items()``.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "Instead of ``CryptPolicy().iter_config()``, "
+ "create a CryptContext instance and "
+ "use ``context.to_dict().items()``.",
+ DeprecationWarning, stacklevel=2)
+ # hacked code that renders keys & values in manner that approximates
+ # old behavior. context.to_dict() is much cleaner.
+ context = self._context
+ if ini:
+ def render_key(key):
+ return context._render_config_key(key).replace("__", ".")
+ def render_value(value):
+ if isinstance(value, (list,tuple)):
+ value = ", ".join(value)
+ return value
+ resolve = False
+ else:
+ render_key = context._render_config_key
+ render_value = lambda value: value
+ return (
+ (render_key(key), render_value(value))
+ for key, value in context._iter_config(resolve)
+ )
- # write list of handlers at start
- if (None,None,"schemes") in self._kwds:
- if resolve and not ini:
- value = self._handlers
- else:
- value = self._schemes
- yield format_key(None, None, "schemes"), encode_list(value)
+ def to_dict(self, resolve=False):
+ """export policy object as dictionary of options.
- # then per-category elements
- scheme_items = sorted(iteritems(self._scheme_options))
- get_option = self._get_option
- for cat in self._categories:
+ .. deprecated:: 1.6
- # write deprecated list (if any)
- value = get_option(None, cat, "deprecated")
- if value is not None:
- yield format_key(cat, None, "deprecated"), encode_list(value)
+ applications should use :meth:`CryptContext.to_dict` instead.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.to_dict()``, "
+ "use ``context.to_dict()``.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "Instead of ``CryptPolicy().to_dict()``, "
+ "create a CryptContext instance and "
+ "use ``context.to_dict()``.",
+ DeprecationWarning, stacklevel=2)
+ return self._context.to_dict(resolve)
- # write default declaration (if any)
- value = get_option(None, cat, "default")
- if value is not None:
- yield format_key(cat, None, "default"), value
+ def to_file(self, stream, section="passlib"):
+ """export policy to file.
- # write mvt (if any)
- value = get_option(None, cat, "min_verify_time")
- if value is not None:
- yield format_key(cat, None, "min_verify_time"), value
+ .. deprecated:: 1.6
- # write configs for all schemes
- for scheme, config in scheme_items:
- if cat in config:
- kwds = config[cat]
- for key in sorted(kwds):
- yield format_key(cat, scheme, key), kwds[key]
+ applications should use :meth:`CryptContext.to_string` instead,
+ and then write the output to a file as desired.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.to_file(stream)``, "
+ "use ``stream.write(context.to_string())``.",
+ DeprecationWarning, stacklevel=2)
+ else:
+ warn(_preamble +
+ "Instead of ``CryptPolicy().to_file(stream)``, "
+ "create a CryptContext instance and "
+ "use ``stream.write(context.to_string())``.",
+ DeprecationWarning, stacklevel=2)
+ out = self._context.to_string(section=section)
+ if PY2:
+ out = out.encode("utf-8")
+ stream.write(out)
- def to_dict(self, resolve=False):
- "return policy as dictionary of keywords"
- return dict(self.iter_config(resolve=resolve))
-
- def _escape_ini_pair(self, k, v):
- if isinstance(v, str):
- v = v.replace("%", "%%") #escape any percent signs.
- elif isinstance(v, num_types):
- v = str(v)
- return k,v
-
- def _write_to_parser(self, parser, section):
- "helper for to_string / to_file"
- parser.add_section(section)
- for k,v in self.iter_config(ini=True):
- k,v = self._escape_ini_pair(k,v)
- parser.set(section, k,v)
+ def to_string(self, section="passlib", encoding=None):
+ """export policy to file.
- #XXX: rename as "to_stream" or "write_to_stream" ?
- def to_file(self, stream, section="passlib"):
- "serialize to INI format and write to specified stream"
- p = SafeConfigParser()
- self._write_to_parser(p, section)
- p.write(stream)
+ .. deprecated:: 1.6
- def to_string(self, section="passlib", encoding=None):
- "render to INI string; inverse of from_string() constructor"
- buf = NativeStringIO()
- self.to_file(buf, section)
- out = buf.getvalue()
- if not PY3:
- out = out.decode("utf-8")
- if encoding:
- return out.encode(encoding)
+ applications should use :meth:`CryptContext.to_string` instead.
+ """
+ if self._stub_policy:
+ warn(_preamble +
+ "Instead of ``context.policy.to_string()``, "
+ "use ``context.to_string()``.",
+ DeprecationWarning, stacklevel=2)
else:
- return out
-
- ##def to_path(self, path, section="passlib", update=False):
- ## "write to INI file"
- ## p = ConfigParser()
- ## if update and os.path.exists(path):
- ## if not p.read([path]):
- ## raise EnvironmentError("failed to read existing file")
- ## p.remove_section(section)
- ## self._write_to_parser(p, section)
- ## fh = file(path, "w")
- ## p.write(fh)
- ## fh.close()
+ warn(_preamble +
+ "Instead of ``CryptPolicy().to_string()``, "
+ "create a CryptContext instance and "
+ "use ``context.to_string()``.",
+ DeprecationWarning, stacklevel=2)
+ out = self._context.to_string(section=section)
+ if encoding:
+ out = out.encode(encoding)
+ return out
#=========================================================
- #eoc
+ # eoc
#=========================================================
-class _UncompiledCryptPolicy(CryptPolicy):
- """helper class which parses options but doesn't compile them,
- used by CryptPolicy.from_sources() to efficiently merge policy objects.
- """
-
- def _compile(self):
- "convert to actual policy"
- pass
-
- def _force_compile(self):
- "convert to real policy and compile"
- self.__class__ = CryptPolicy
- self._compile()
-
#=========================================================
-# helpers for CryptContext
+# _CryptRecord helper class
#=========================================================
-_passprep_funcs = dict(
- saslprep=saslprep,
- raw=lambda s: s,
-)
-
class _CryptRecord(object):
"""wraps a handler and automatically applies various options.
@@ -757,23 +617,26 @@ class _CryptRecord(object):
# informational attrs
handler = None # handler instance this is wrapping
category = None # user category this applies to
+ deprecated = False # set if handler itself has been deprecated in config
- # rounds management
- _has_rounds = False # if handler has variable cost parameter
- _has_rounds_bounds = False # if min_rounds / max_rounds set
+ # rounds management - filled in by _init_rounds_options()
+ _has_rounds_options = False # if _has_rounds_bounds OR _generate_rounds is set
+ _has_rounds_bounds = False # if either min_rounds or max_rounds set
_min_rounds = None #: minimum rounds allowed by policy, or None
_max_rounds = None #: maximum rounds allowed by policy, or None
+ _generate_rounds = None # rounds generation function, or None
# encrypt()/genconfig() attrs
- _settings = None # subset of options to be used as encrypt() defaults.
+ settings = None # options to be passed directly to encrypt()
# verify() attrs
_min_verify_time = None
# hash_needs_update() attrs
- _has_rounds_introspection = False
+ _is_deprecated_by_handler = None # optional callable used by bcrypt/scram
+ _has_rounds_introspection = False # if rounds can be extract from hash
- # cloned from handler
+ # cloned directly from handler, not affected by config options.
identify = None
genhash = None
@@ -784,142 +647,173 @@ class _CryptRecord(object):
min_rounds=None, max_rounds=None, default_rounds=None,
vary_rounds=None, min_verify_time=None, passprep=None,
**settings):
+ # store basic bits
self.handler = handler
self.category = category
- self._compile_rounds(min_rounds, max_rounds, default_rounds,
- vary_rounds, 'rounds' in settings)
- self._compile_encrypt(settings)
- self._compile_verify(min_verify_time)
- self._compile_deprecation(deprecated)
+ self.deprecated = deprecated
+ self.settings = settings
+
+ # validate & normalize rounds options
+ self._init_rounds_options(min_rounds, max_rounds, default_rounds,
+ vary_rounds)
- # these aren't modified by the record, so just copy them directly
+ # init wrappers for handler methods we modify args to
+ self._init_encrypt_and_genconfig()
+ self._init_verify(min_verify_time)
+ self._init_hash_needs_update()
+
+ # these aren't wrapped by _CryptRecord, copy them directly from handler.
self.identify = handler.identify
self.genhash = handler.genhash
- # let stringprep code wrap genhash/encrypt if needed
- self._compile_passprep(passprep)
+ # let stringprep code wrap genhash/encrypt/verify if needed
+ self._init_passprep(passprep)
+ #================================================================
+ # virtual attrs
+ #================================================================
@property
def scheme(self):
return self.handler.name
@property
- def _ident(self):
+ def _errprefix(self):
"string used to identify record in error messages"
handler = self.handler
category = self.category
if category:
- return "%s %s policy" % (handler.name, category)
+ return "%s %s config" % (handler.name, category)
else:
- return "%s policy" % (handler.name,)
+ return "%s config" % (handler.name,)
+
+ def __repr__(self):
+ return "<_CryptRecord 0x%x for %s>" % (id(self), self._errprefix)
#================================================================
# rounds generation & limits - used by encrypt & deprecation code
#================================================================
- def _compile_rounds(self, mn, mx, df, vr, fixed):
+ def _init_rounds_options(self, mn, mx, df, vr):
"parse options and compile efficient generate_rounds function"
+ #----------------------------------------------------
+ # extract hard limits from handler itself
+ #----------------------------------------------------
handler = self.handler
if 'rounds' not in handler.setting_kwds:
+ # doesn't even support rounds keyword.
return
hmn = getattr(handler, "min_rounds", None)
hmx = getattr(handler, "max_rounds", None)
- def hcheck(value, name):
- "issue warnings if value outside of handler limits"
+ def check_against_handler(value, name):
+ "issue warning if value outside handler limits"
if hmn is not None and value < hmn:
warn("%s: %s value is below handler minimum %d: %d" %
- (self._ident, name, hmn, value), PasslibConfigWarning)
+ (self._errprefix, name, hmn, value), PasslibConfigWarning)
if hmx is not None and value > hmx:
warn("%s: %s value is above handler maximum %d: %d" %
- (self._ident, name, hmx, value), PasslibConfigWarning)
-
- def clip(value):
- "clip value to policy & handler limits"
- if mn is not None and value < mn:
- value = mn
- if hmn is not None and value < hmn:
- value = hmn
- if mx is not None and value > mx:
- value = mx
- if hmx is not None and value > hmx:
- value = hmx
- return value
+ (self._errprefix, name, hmx, value), PasslibConfigWarning)
#----------------------------------------------------
- # validate inputs
+ # set policy limits
#----------------------------------------------------
if mn is not None:
if mn < 0:
- raise ValueError("%s: min_rounds must be >= 0" % self._ident)
- hcheck(mn, "min_rounds")
+ raise ValueError("%s: min_rounds must be >= 0" % self._errprefix)
+ check_against_handler(mn, "min_rounds")
+ self._min_rounds = mn
+ self._has_rounds_bounds = True
if mx is not None:
if mn is not None and mx < mn:
raise ValueError("%s: max_rounds must be "
- ">= min_rounds" % self._ident)
+ ">= min_rounds" % self._errprefix)
elif mx < 0:
- raise ValueError("%s: max_rounds must be >= 0" % self._ident)
- hcheck(mx, "max_rounds")
-
- if vr is not None:
- if isinstance(vr, str):
- assert vr.endswith("%")
- vr = float(vr.rstrip("%"))
- if vr < 0:
- raise ValueError("%s: vary_rounds must be >= '0%%'" %
- self._ident)
- elif vr > 100:
- raise ValueError("%s: vary_rounds must be <= '100%%'" %
- self._ident)
- vr_is_pct = True
- else:
- assert isinstance(vr, int)
- if vr < 0:
- raise ValueError("%s: vary_rounds must be >= 0" %
- self._ident)
- vr_is_pct = False
-
- if df is None:
- # fallback to handler's default if available
- if vr or mx or mn:
- df = getattr(handler, "default_rounds", None) or mx or mn
- else:
+ raise ValueError("%s: max_rounds must be >= 0" % self._errprefix)
+ check_against_handler(mx, "max_rounds")
+ self._max_rounds = mx
+ self._has_rounds_bounds = True
+
+ #----------------------------------------------------
+ # validate default_rounds
+ #----------------------------------------------------
+ if df is not None:
if mn is not None and df < mn:
raise ValueError("%s: default_rounds must be "
- ">= min_rounds" % self._ident)
+ ">= min_rounds" % self._errprefix)
if mx is not None and df > mx:
raise ValueError("%s: default_rounds must be "
- "<= max_rounds" % self._ident)
- hcheck(df, "default_rounds")
+ "<= max_rounds" % self._errprefix)
+ check_against_handler(df, "default_rounds")
+ elif vr or mx or mn:
+ # need an explicit default to work with
+ df = getattr(handler, "default_rounds", None) or mx or mn
+ assert df is not None, "couldn't find fallback default_rounds"
+ else:
+ # no need for rounds generation
+ self._has_rounds_options = self._has_rounds_bounds
+ return
- #----------------------------------------------------
- # set policy limits
- #----------------------------------------------------
- self._has_rounds_bounds = (mn is not None) or (mx is not None)
- self._min_rounds = mn
- self._max_rounds = mx
+ # clip default to handler & policy limits *before* vary rounds
+ # is calculated, so that proportion vr values are scaled against
+ # the effective default.
+ def clip(value):
+ "clip value to intersection of policy + handler limits"
+ if mn is not None and value < mn:
+ value = mn
+ if hmn is not None and value < hmn:
+ value = hmn
+ if mx is not None and value > mx:
+ value = mx
+ if hmx is not None and value > hmx:
+ value = hmx
+ return value
+ df = clip(df)
#----------------------------------------------------
- # setup rounds generation function
+ # validate vary_rounds,
+ # coerce df/vr to linear scale,
+ # and setup scale_value() to undo coercion
#----------------------------------------------------
- if df is None or fixed:
- self._generate_rounds = None
- self._has_rounds = self._has_rounds_bounds
- elif vr:
- scale_value = lambda v,uf: v
- if vr_is_pct:
- scale = getattr(handler, "rounds_cost", "linear")
- assert scale in ["log2", "linear"]
- if scale == "log2":
+ # NOTE: vr=0 same as if vr not set
+ if vr:
+ if vr < 0:
+ raise ValueError("%s: vary_rounds must be >= 0" %
+ self._errprefix)
+ def scale_value(value, upper):
+ return value
+ if isinstance(vr, float):
+ # vr is value from 0..1 expressing fraction of default rounds.
+ if vr > 1:
+ # XXX: deprecate 1.0 ?
+ raise ValueError("%s: vary_rounds must be < 1.0" %
+ self._errprefix)
+ # calculate absolute vr value based on df & rounds_cost
+ cost_scale = getattr(handler, "rounds_cost", "linear")
+ assert cost_scale in ["log2", "linear"]
+ if cost_scale == "log2":
+ # convert df & vr to linear scale for limit calc,
+ # but define scale_value() to convert back to log2.
df = 1<<df
- def scale_value(v, uf):
- if v <= 0:
+ def scale_value(value, upper):
+ if value <= 0:
return 0
- elif uf:
- return int(logb(v,2))
+ elif upper:
+ return int(logb(value,2))
else:
- return int(ceil(logb(v,2)))
- vr = int(df*vr/100)
+ return int(ceil(logb(value,2)))
+ vr = int(df*vr)
+ elif not isinstance(vr, int):
+ raise TypeError("vary_rounds must be int or float")
+ # else: vr is explicit number of rounds to vary df by.
+
+ #----------------------------------------------------
+ # set up rounds generation function.
+ #----------------------------------------------------
+ if not vr:
+ # fixed rounds value
+ self._generate_rounds = lambda : df
+ else:
+ # randomly generate rounds in range df +/- vr
lower = clip(scale_value(df-vr,False))
upper = clip(scale_value(df+vr,True))
if lower == upper:
@@ -927,77 +821,93 @@ class _CryptRecord(object):
else:
assert lower < upper
self._generate_rounds = lambda: rng.randint(lower, upper)
- self._has_rounds = True
- else:
- df = clip(df)
- self._generate_rounds = lambda: df
- self._has_rounds = True
- # filled in by _compile_rounds_settings()
- _generate_rounds = None
+ # hack for bsdi_crypt - want to avoid even-valued rounds
+ # NOTE: this technically might generate a rounds value 1 larger
+ # than the requested upper bound - but better to err on side of safety.
+ if getattr(handler, "_avoid_even_rounds", False):
+ gen = self._generate_rounds
+ self._generate_rounds = lambda : gen()|1
+
+ self._has_rounds_options = True
#================================================================
# encrypt() / genconfig()
#================================================================
- def _compile_encrypt(self, settings):
+ def _init_encrypt_and_genconfig(self):
+ "initialize genconfig/encrypt wrapper methods"
+ settings = self.settings
handler = self.handler
- skeys = handler.setting_kwds
+
+ # check no invalid settings are being set
+ keys = handler.setting_kwds
for key in settings:
- if key not in skeys:
+ if key not in keys:
raise KeyError("keyword not supported by %s handler: %r" %
(handler.name, key))
- self._settings = settings
- if not (settings or self._has_rounds):
- # bypass prepare settings entirely.
+ # if _prepare_settings() has nothing to do, bypass our wrappers
+ # with reference to original methods.
+ if not (settings or self._has_rounds_options):
self.genconfig = handler.genconfig
self.encrypt = handler.encrypt
def genconfig(self, **kwds):
+ "wrapper for handler.genconfig() which adds custom settings/rounds"
self._prepare_settings(kwds)
return self.handler.genconfig(**kwds)
def encrypt(self, secret, **kwds):
+ "wrapper for handler.encrypt() which adds custom settings/rounds"
self._prepare_settings(kwds)
return self.handler.encrypt(secret, **kwds)
def _prepare_settings(self, kwds):
- "normalize settings for handler according to context configuration"
+ "add default values to settings for encrypt & genconfig"
#load in default values for any settings
- settings = self._settings
- for k in settings:
- if k not in kwds:
- kwds[k] = settings[k]
+ if kwds:
+ for k,v in iteritems(self.settings):
+ if k not in kwds:
+ kwds[k] = v
+ else:
+ # faster, and the common case
+ kwds.update(self.settings)
- #handle rounds
- if self._has_rounds:
+ # handle rounds
+ if self._has_rounds_options:
rounds = kwds.get("rounds")
if rounds is None:
+ # fill in default rounds value
gen = self._generate_rounds
if gen:
kwds['rounds'] = gen()
elif self._has_rounds_bounds:
+ # check bounds for application-provided rounds value.
# XXX: should this raise an error instead of warning ?
# NOTE: stackdepth=4 is so that error matches
# where ctx.encrypt() was called by application code.
mn = self._min_rounds
if mn is not None and rounds < mn:
warn("%s requires rounds >= %d, increasing value from %d" %
- (self._ident, mn, rounds), PasslibConfigWarning, 4)
+ (self._errprefix, mn, rounds), PasslibConfigWarning, 4)
rounds = mn
mx = self._max_rounds
if mx and rounds > mx:
warn("%s requires rounds <= %d, decreasing value from %d" %
- (self._ident, mx, rounds), PasslibConfigWarning, 4)
+ (self._errprefix, mx, rounds), PasslibConfigWarning, 4)
rounds = mx
kwds['rounds'] = rounds
#================================================================
# verify()
#================================================================
- def _compile_verify(self, mvt):
+ # TODO: once min_verify_time is removed, this will just be a clone
+ # of handler.verify()
+
+ def _init_verify(self, mvt):
+ "initialize verify() wrapper - implements min_verify_time"
if mvt:
- assert mvt > 0, "CryptPolicy should catch this"
+ assert isinstance(mvt, (int,float)) and mvt > 0, "CryptPolicy should catch this"
self._min_verify_time = mvt
else:
# no mvt wrapper needed, so just use handler.verify directly
@@ -1006,18 +916,17 @@ class _CryptRecord(object):
def verify(self, secret, hash, **context):
"verify helper - adds min_verify_time delay"
mvt = self._min_verify_time
- assert mvt > 0
+ assert mvt > 0, "wrapper should have been replaced for mvt=0"
start = tick()
- ok = self.handler.verify(secret, hash, **context)
- if ok:
+ if self.handler.verify(secret, hash, **context):
return True
end = tick()
delta = mvt + start - end
if delta > 0:
sleep(delta)
elif delta < 0:
- #warn app they exceeded bounds (this might reveal
- #relative costs of different hashes if under migration)
+ # warn app they exceeded bounds (this might reveal
+ # relative costs of different hashes if under migration)
warn("CryptContext: verify exceeded min_verify_time: "
"scheme=%r min_verify_time=%r elapsed=%r" %
(self.scheme, mvt, end-start), PasslibConfigWarning)
@@ -1026,8 +935,10 @@ class _CryptRecord(object):
#================================================================
# hash_needs_update()
#================================================================
- def _compile_deprecation(self, deprecated):
- if deprecated:
+ def _init_hash_needs_update(self):
+ """initialize state for hash_needs_update()"""
+ # if handler has been deprecated, replace wrapper and skip other checks
+ if self.deprecated:
self.hash_needs_update = lambda hash: True
return
@@ -1038,25 +949,28 @@ class _CryptRecord(object):
#
# NOTE: this interface is still private, because it was hacked in
# for the sake of bcrypt & scram, and is subject to change.
- #
handler = self.handler
const = getattr(handler, "_deprecation_detector", None)
if const:
- self._hash_needs_update = const(**self._settings)
+ self._is_deprecated_by_handler = const(**self.settings)
# XXX: what about a "min_salt_size" deprecator?
- # check if there are rounds, rounds limits, and if we can
- # parse the rounds from the handler. if that's the case...
+ # set flag if we can extract rounds from hash, allowing
+ # hash_needs_update() to check for rounds that are outside of
+ # the configured range.
if self._has_rounds_bounds and hasattr(handler, "from_string"):
self._has_rounds_introspection = True
def hash_needs_update(self, hash):
- # NOTE: this is replaced by _compile_deprecation() if self.deprecated
+ # init replaces this method entirely for this case.
+ ### check if handler has been deprecated
+ ##if self.deprecated:
+ ## return True
# check handler's detector if it provided one.
- hnu = self._hash_needs_update
- if hnu and hnu(hash):
+ check = self._is_deprecated_by_handler
+ if check and check(hash):
return True
# if we can parse rounds parameter, check if it's w/in bounds.
@@ -1065,11 +979,12 @@ class _CryptRecord(object):
try:
rounds = hash_obj.rounds
except AttributeError:
- # XXX: hash_obj should generally have rounds attr
- # should a warning be raised here?
+ # XXX: hash_obj should generally have rounds attr,
+ # so should a warning be raised here?
pass
else:
- if rounds < self._min_rounds:
+ mn = self._min_rounds
+ if mn is not None and rounds < mn:
return True
mx = self._max_rounds
if mx and rounds > mx:
@@ -1077,13 +992,10 @@ class _CryptRecord(object):
return False
- # filled in by init from handler._hash_needs_update
- _hash_needs_update = None
-
#================================================================
# password stringprep
#================================================================
- def _compile_passprep(self, value):
+ def _init_passprep(self, value):
# NOTE: all of this code assumes secret uses utf-8 encoding if bytes.
if not value:
return
@@ -1134,185 +1046,1134 @@ class _CryptRecord(object):
#================================================================
#=========================================================
-# context classes
+# main CryptContext class
#=========================================================
class CryptContext(object):
"""Helper for encrypting passwords using different algorithms.
- :param \*\*kwds:
-
- ``schemes`` and all other keywords are passed to the CryptPolicy constructor,
- or to :meth:`CryptPolicy.replace`, if a policy has also been specified.
-
- :param policy:
- Optionally you can pass in an existing CryptPolicy instance,
- which allows loading the policy from a configuration file,
- combining multiple policies together, and other features.
-
- The options from this policy will be used as defaults,
- which will be overridden by any keywords passed in explicitly.
-
- .. automethod:: replace
+ Instances of this class allow applications to choose a specific
+ set of hash algorithms which they wish to support, set limits and defaults
+ for the rounds and salt sizes those algorithms should use, flag
+ which algorithms should be deprecated, and automatically handle
+ migrating users to stronger hashes when they log in.
- Configuration
- =============
- .. attribute:: policy
+ This class can be created one of three ways: directly through it's
+ constructor via keywords, loaded from a configuration string,
+ or loaded from a file. Configuration strings / files can be created by hand;
+ or automatically created by serializing an existing CryptContext, using
+ :meth:``to_string``.
- This exposes the :class:`CryptPolicy` instance
- which contains the configuration used by this context object.
+ :param \*\*kwds:
- This attribute may be written to (replacing it with another CryptPolicy instance),
- in order to reconfigure a CryptContext while an application is running.
- However, this should only be done for context instances created by the application,
- and NOT for context instances provided by PassLib.
+ CryptContext instances accept a wide number of keywords as possible
+ options. Common keywords include ``schemes`` and ``default``.
+ See :doc:`/lib/passlib.context-options` for a full list.
Main Interface
==============
- .. automethod:: identify
+ Most applications will only need to make use two methods in a CryptContext
+ instance:
+
.. automethod:: encrypt
.. automethod:: verify
- Migration Helpers
- =================
+ Applications which want to detect and re-encrypt deprecated
+ hashes will want to use one of the following methods:
+
.. automethod:: hash_needs_update
.. automethod:: verify_and_update
+
+ Additionally, the main interface offers a few helper methods,
+ useful for certain border cases:
+
+ .. automethod:: identify
+ .. automethod:: genhash
+ .. automethod:: genconfig
+
+ Alternate Constructors
+ ======================
+ In addition to the main class constructor, which accepts a configuration
+ as a set of keywords, there are the following alternate constructors:
+
+ .. automethod:: from_string
+ .. automethod:: from_path
+ .. automethod:: copy
+
+ Updating the Configuration
+ ==========================
+ CryptContext objects can have their configuration replaced or updated
+ on the fly, and from a variety of sources (keywords, strings, files).
+ This is done through two methods:
+
+ .. automethod:: update(\*\*kwds)
+ .. automethod:: load
+ .. automethod:: load_path
+
+ Examining the Configuration
+ ===========================
+ The CryptContext object also supports some basic inspection of it's
+ current configuration:
+
+ .. automethod:: schemes
+ .. automethod:: default_scheme
+ .. automethod:: handler
+
+ More detailed inspection can be done through the serialization methods:
+
+ .. automethod:: to_dict
+ .. automethod:: to_string
"""
+ # FIXME: altering the configuration of this object isn't threadsafe,
+ # but is generally only done during application init, so not a major
+ # issue (just yet).
+
#===================================================================
#instance attrs
#===================================================================
- _policy = None # policy object governing context - access via :attr:`policy`
- _records = None # map of (category,scheme) -> _CryptRecord instance
- _record_lists = None # map of category -> records for category, in order
+
+ # tuple of handlers (from 'schemes' keyword)
+ _handlers = None
+
+ # tuple of scheme names (in same order as handlers)
+ _schemes = None
+
+ # tuple of extra category names (in alpha order, omits ``None``)
+ _categories = None
+
+ # triple-nested-dict which maps scheme -> category -> option -> value
+ _scheme_options = None
+
+ # dict mapping category -> default scheme
+ _default_schemes = None
+
+ # dict mapping category -> set of deprecated schemes
+ _deprecated_schemes = None
+
+ # dict mapping category -> min_verify_time
+ _mvtmap = None
+
+ # dict mapping (scheme,category) -> _CryptRecord instance.
+ # initial values populated by load(), but extra keys
+ # such as scheme=None for default record are populated on demand
+ # by _get_record()
+ _records = None
+
+ # dict mapping category -> list of _CryptRecord instances for that category,
+ # in order of schemes(). populated on demand by _get_record_list()
+ _record_lists = None
#===================================================================
- #init
+ # secondary constructors
#===================================================================
- def __init__(self, schemes=None, policy=None, **kwds):
- # XXX: add a name for the contexts, to help out repr?
- # XXX: add ability to make policy readonly for certain instances,
- # eg the builtin passlib ones?
- if schemes:
- kwds['schemes'] = schemes
- if not policy:
- policy = CryptPolicy(**kwds)
- elif kwds:
- policy = policy.replace(**kwds)
- self.policy = policy
+ @classmethod
+ def from_string(cls, source, section="passlib", encoding="utf-8"):
+ """create new CryptContext instance from an INI-formatted string.
- def __repr__(self):
- # XXX: *could* have proper repr(), but would have to render policy
- # object options, and it'd be *really* long
- names = [ handler.name for handler in self.policy._handlers ]
- return "<CryptContext %0xd schemes=%r>" % (id(self), names)
+ :arg source:
+ bytes/unicode string containing INI-formatted content.
- #XXX: make an update() method that just updates policy?
+ :param section:
+ option name of section to read from, defaults to ``"passlib"``.
- def replace(self, **kwds):
- """return mutated CryptContext instance
+ :arg encoding:
+ optional encoding used when source is bytes, defaults to ``"utf-8"``.
+
+ :returns:
+ new CryptContext instance.
+
+ usage example::
+
+ >>> from passlib.context import CryptContext
+ >>> context = CryptContext.from_string('''
+ ... [passlib]
+ ... schemes = sha256_crypt, des_crypt
+ ... sha256_crypt__default_rounds = 30000
+ ... ''')
+
+ .. versionadded:: 1.6
+ """
+ if not isinstance(source, base_string_types):
+ raise ExpectedTypeError(source, "unicode or bytes", "source")
+ self = cls(_autoload=False)
+ self.load(source, section=section, encoding=encoding)
+ return self
+
+ @classmethod
+ def from_path(cls, path, section="passlib", encoding="utf-8"):
+ """create new CryptContext instance from an INI-formatted file.
- this function operates much like :meth:`datetime.replace()` - it returns
- a new CryptContext instance whose configuration is exactly the
- same as the original, with the exception of any keywords
- specificed taking precedence over the original settings.
+ this functions exactly the same as :meth:`from_string`,
+ except that it loads from a local file.
- this is identical to the operation ``CryptContext(policy=self.policy.replace(**kwds))``,
- see :meth:`CryptPolicy.replace` for more details.
+ :arg source:
+ path to local file containing INI-formatted config.
+
+ :param section:
+ option name of section to read from, defaults to ``"passlib"``.
+
+ :arg encoding:
+ encoding used to load file, defaults to ``"utf-8"``.
+
+ :returns:
+ new CryptContext instance.
+
+ .. versionadded:: 1.6
"""
- return CryptContext(policy=self.policy.replace(**kwds))
+ self = cls(_autoload=False)
+ self.load_path(path, section=section, encoding=encoding)
+ return self
+
+ def copy(self, **kwds):
+ """return copy of existing CryptContext instance.
+
+ this function returns a new CryptContext instance whose configuration
+ is exactly the same as the original, with the exception that any keywords
+ passed in will take precedence over the original settings.
+
+ usage example::
+
+ >>> # given an existing context, e.g...
+ >>> from passlib.apps import custom_app_context
- #XXX: make an update() method that just updates policy?
- ##def update(self, **kwds):
- ## self.policy = self.policy.replace(**kwds)
+ >>> # copy can be used to make a clone
+ >>> my_context = custom_app_context.copy(default="sha512_crypt")
+
+ >>> # and the original will be unaffected by the change
+ >>> my_context.default_scheme()
+ "sha512_crypt"
+ >>> custom_app_context.default_scheme()
+ "sha256_crypt"
+
+ .. seealso:: :meth:`update`
+
+ .. versionchanged:: 1.6
+ This method was previously named ``replace``, that name
+ is still supported, but deprecated, and will be removed
+ in Passlib 1.8.
+ """
+ other = CryptContext(**self.to_dict(resolve=True))
+ if kwds:
+ other.load(kwds, update=True)
+ return other
+
+ def replace(self, **kwds):
+ "deprecated alias of :meth:`copy`"
+ warn("CryptContext().replace() has been deprecated in Passlib 1.6, "
+ "and will be removed in Passlib 1.8, "
+ "it has been renamed to CryptContext().copy()",
+ DeprecationWarning, stacklevel=2)
+ return self.copy(**kwds)
#===================================================================
- # policy management
+ #init
#===================================================================
+ def __init__(self, schemes=None,
+ # keyword only...
+ policy=_UNSET, # <-- deprecated
+ _autoload=True, **kwds):
+ # XXX: add ability to make flag certain contexts as immutable,
+ # e.g. the builtin passlib ones?
+ # XXX: add a name or import path for the contexts, to help out repr?
+ if schemes is not None:
+ kwds['schemes'] = schemes
+ if policy is not _UNSET:
+ warn("The CryptContext ``policy`` keyword has been deprecated as of Passlib 1.6, "
+ "and will be removed in Passlib 1.8; please use "
+ "``CryptContext.from_string()` or "
+ "``CryptContext.from_path()`` instead.",
+ DeprecationWarning)
+ if policy is None:
+ self.load(kwds)
+ elif isinstance(policy, CryptPolicy):
+ self.load(policy._context)
+ self.update(kwds)
+ else:
+ raise TypeError("policy must be a CryptPolicy instance")
+ elif _autoload:
+ self.load(kwds)
+ else:
+ assert not kwds, "_autoload=False and kwds are mutually exclusive"
+
+ def __str__(self):
+ if PY3:
+ return self.to_string()
+ else:
+ return self.to_string().encode("utf-8")
+
+ def __repr__(self):
+ return "<CryptContext 0x%0x>" % id(self)
+ # XXX: not sure if this would be helpful, or confusing...
+ ### let repr output string required to recreate the CryptContext
+ ##data = self.to_string(compact=True)
+ ##if not PY3:
+ ## data = data.encode("utf-8")
+ ##return "CryptContext.from_string(%r)" % (data,)
+
+ #===================================================================
+ # deprecated policy object
+ #===================================================================
def _get_policy(self):
- return self._policy
-
- def _set_policy(self, value):
- if not isinstance(value, CryptPolicy):
- raise TypeError("value must be a CryptPolicy instance")
- if value is not self._policy:
- self._policy = value
- self._compile()
-
- policy = property(_get_policy, _set_policy)
-
- #------------------------------------------------------------------
- # compile policy information into _CryptRecord instances
- #------------------------------------------------------------------
- def _compile(self):
- "update context object internals based on new policy instance"
- policy = self._policy
+ # The CryptPolicy class has been deprecated, so to support any
+ # legacy accesses, we create a stub policy object so .policy attr
+ # will continue to work.
+ #
+ # the code waits until app accesses a specific policy object attribute
+ # before issuing deprecation warning, so developer gets method-specific
+ # suggestion for how to upgrade.
+
+ # NOTE: making a copy of the context so the policy acts like a snapshot,
+ # to retain the pre-1.6 behavior.
+ return CryptPolicy(_internal_context=self.copy(), _stub_policy=True)
+
+ def _set_policy(self, policy):
+ warn("The CryptPolicy class and the ``context.policy`` attribute have "
+ "been deprecated as of Passlib 1.6, and will be removed in "
+ "Passlib 1.8; please use the ``context.load()`` and "
+ "``context.update()`` methods instead.",
+ DeprecationWarning, stacklevel=2)
+ if isinstance(policy, CryptPolicy):
+ self.load(policy._context)
+ else:
+ raise TypeError("expected CryptPolicy instance")
+
+ policy = property(_get_policy, _set_policy,
+ doc="[deprecated] returns CryptPolicy instance "
+ "tied to this CryptContext")
+
+ #===================================================================
+ # loading / updating configuration
+ #===================================================================
+ @staticmethod
+ def _parse_ini_stream(stream, section, filename):
+ "helper read INI from stream, extract passlib section as dict"
+ # NOTE: this expects a unicode stream under py3,
+ # and a utf-8 bytes stream under py2,
+ # allowing the resulting dict to always use native strings.
+ p = SafeConfigParser()
+ if PY_MIN_32:
+ # python 3.2 deprecated readfp in favor of read_file
+ p.read_file(stream, filename)
+ else:
+ p.readfp(stream, filename)
+ return dict(p.items(section))
+
+ def load_path(self, path, update=False, section="passlib", encoding="utf-8"):
+ """load new configuration into CryptContext from a local file.
+
+ This function is a wrapper for :meth:`load`, which
+ loads a configuration string from the local file *path*,
+ instead of an in-memory source. It's behavior and options
+ are otherwise identical to :meth:`!load` when provided with
+ an INI-formatted string.
+
+ .. versionadded:: 1.6
+ """
+ def helper(stream):
+ kwds = self._parse_ini_stream(stream, section, path)
+ return self.load(kwds, update=update)
+ if PY3:
+ # decode to unicode, which load() expected under py3
+ with open(path, "rt", encoding=encoding) as stream:
+ return helper(stream)
+ elif encoding in ["utf-8", "ascii"]:
+ # keep as utf-8 bytes, which load() expects under py2
+ with open(path, "rb") as stream:
+ return helper(stream)
+ else:
+ # transcode to utf-8 bytes
+ with open(path, "rb") as fh:
+ tmp = fh.read().decode(encoding).encode("utf-8")
+ return helper(BytesIO(tmp))
+
+ def load(self, source, update=False, section="passlib", encoding="utf-8"):
+ """load new configuration into CryptContext, replacing existing config.
+
+ :arg source:
+ source of new configuration.
+
+ *source* can be one of a few of different types:
+
+ :class:`!dict` or another mapping
+
+ the key/value pairs will be interpreted the same
+ keywords for the :class:`CryptContext` class constructor.
+
+ :class:`!unicode` or :class:`!bytes` string
+
+ this will be interpreted as an INI-formatted file,
+ and appropriate key/value pairs will be loaded from
+ the specified *section*.
+
+ :type update: bool
+ :param update:
+ By default, :meth:`load` will replace the existing configuration
+ entirely. If ``update=True``, it will preserve any existing
+ configuration options that are not overridden by the new source,
+ much like the :meth:`update` method.
+
+ :type section: str
+ :param section:
+ When parsing an INI-formatted string, :meth:`load` will look for
+ a section named ``"passlib"``. This option allows an alternate
+ section name to be used. Ignored when loading from a dictionary.
+
+ :type encoding: str
+ :param encoding:
+ Encoding to use when decode bytes from string.
+ Defaults to ``"utf-8"``. Ignoring when loading from a dictionary.
+
+ :raises TypeError:
+ * If the source cannot be identified.
+ * If an unknown / malformed keyword is encountered.
+
+ :raises ValueError:
+ If an invalid keyword value is encountered.
+
+ If an error occurs during a :meth:`!load` call, the :class`!CryptContext`
+ instance will be restored to the configuration it was in before
+ the :meth:`!load` call was made, it should never be left in an
+ inconsistent state due to a load failure.
+
+ .. versionadded:: 1.6
+ """
+ #-----------------------------------------------------------
+ # autodetect source type, convert to dict
+ #-----------------------------------------------------------
+ parse_keys = True
+ if isinstance(source, base_string_types):
+ if PY3:
+ source = to_unicode(source, encoding, errname="source")
+ else:
+ source = to_bytes(source, "utf-8", source_encoding=encoding,
+ errname="source")
+ source = self._parse_ini_stream(NativeStringIO(source), section,
+ "<string>")
+ elif isinstance(source, CryptContext):
+ # do this a little more efficiently since we can extract
+ # the keys as tuples directly from the other instance.
+ source = dict(source._iter_config(resolve=True))
+ parse_keys = False
+ elif not hasattr(source, "items"):
+ # assume it's not a mapping.
+ raise ExpectedTypeError(source, "string or dict", "source")
+
+ # XXX: add support for other iterable types, e.g. sequence of pairs?
+
+ #-----------------------------------------------------------
+ # parse dict keys into (category, scheme, option) format,
+ # and merge with existing configuration if needed.
+ #-----------------------------------------------------------
+ if parse_keys:
+ parse = self._parse_config_key
+ source = dict((parse(key), value) for key, value in iteritems(source))
+ if update and self._handlers is not None:
+ if not source:
+ return
+ tmp = source
+ source = dict(self._iter_config(resolve=True))
+ source.update(tmp)
+
+ #-----------------------------------------------------------
+ # clear internal config, replace with content of source.
+ #-----------------------------------------------------------
+ # NOTE: if this fails, 'self' will be an unpredicatable state,
+ # since config parsing can fail at a number of places.
+ # the follow code fixes this by backing up the state, and restoring
+ # it if any errors occur. this is somewhat... hacked...
+ # but it works for now, and performance is not an issue in the
+ # error case. but because of that, care should be taken
+ # that _load() never modifies existing attrs, and instead replaces
+ # them entirely.
+ state = self.__dict__.copy()
+ try:
+ self._load(source)
+ except:
+ self.__dict__.clear()
+ self.__dict__.update(state)
+ raise
+
+ def _load(self, source):
+ """load source keys into internal configuration.
+
+ note that if this throws error, object's config will be left
+ in inconsistent state, load() takes care of backing up / restoring
+ original config.
+ """
+ #-----------------------------------------------------------
+ # build & validate list of handlers
+ #-----------------------------------------------------------
+ handlers = []
+ schemes = []
+ data = source.get((None,None,"schemes"))
+ if isinstance(data, str):
+ data = _splitcomma(data)
+ for elem in data or ():
+ # resolve elem -> handler & scheme
+ if hasattr(elem, "name"):
+ handler = elem
+ scheme = handler.name
+ _validate_handler_name(scheme)
+ elif isinstance(elem, str):
+ handler = get_crypt_handler(elem)
+ scheme = handler.name
+ else:
+ raise TypeError("scheme must be name or CryptHandler, "
+ "not %r" % type(elem))
+
+ #check scheme name already in use
+ if scheme in schemes:
+ raise KeyError("multiple handlers with same name: %r" %
+ (scheme,))
+
+ #add to handler list
+ handlers.append(handler)
+ schemes.append(scheme)
+
+ self._handlers = handlers = tuple(handlers)
+ self._schemes = schemes = tuple(schemes)
+
+ #-----------------------------------------------------------
+ # initialize internal storage, write all scheme-specific options
+ # to _scheme_options, validate & store all global CryptContext
+ # options in the appropriate private attrs.
+ #-----------------------------------------------------------
+ scheme_options = self._scheme_options = {}
+ self._default_schemes = {}
+ self._deprecated_schemes = {}
+ self._mvtmap = {}
+ categories = set()
+ add_cat = categories.add
+ for (cat, scheme, key), value in iteritems(source):
+ add_cat(cat)
+ # store scheme-specific options for later,
+ # and let _CryptRecord() handle validation in next section.
+ if scheme:
+ # check for invalid options
+ if key == "rounds":
+ # for now, translating this to 'default_rounds' to be helpful.
+ # need to pick one of the two as official,
+ # and deprecate the other one.
+ key = "default_rounds"
+ elif key in _forbidden_scheme_options:
+ raise KeyError("%r option not allowed in CryptContext "
+ "configuration" % (key,))
+ # coerce strings for certain fields (e.g. min_rounds -> int)
+ if isinstance(value, str):
+ func = _coerce_scheme_options.get(key)
+ if func:
+ value = func(value)
+ # store value in scheme_options
+ if scheme in scheme_options:
+ config = scheme_options[scheme]
+ if cat in config:
+ config[cat][key] = value
+ else:
+ config[cat] = {key: value}
+ else:
+ scheme_options[scheme] = {cat: {key: value}}
+ # otherwise it's a CryptContext option of some type.
+ # perform validation here, and store internally.
+ elif key == "default":
+ if hasattr(value, "name"):
+ value = value.name
+ elif not isinstance(value, str):
+ raise ExpectedTypeError(value, "str", "default")
+ if schemes and value not in schemes:
+ raise KeyError("default scheme not found in policy")
+ self._default_schemes[cat] = value
+ elif key == "deprecated":
+ if isinstance(value, str):
+ value = _splitcomma(value)
+ elif not isinstance(value, (list,tuple)):
+ raise ExpectedTypeError(value, "str or seq", "deprecated")
+ if schemes:
+ for scheme in value:
+ if not isinstance(scheme, str):
+ raise ExpectedTypeError(value, "str", "deprecated element")
+ if scheme not in schemes:
+ raise KeyError("deprecated scheme not found "
+ "in policy: %r" % (scheme,))
+ # TODO: make sure there's at least one non-deprecated scheme.
+ # TODO: make sure default scheme hasn't been deprecated.
+ self._deprecated_schemes[cat] = value
+ elif key == "min_verify_time":
+ warn("'min_verify_time' is deprecated as of Passlib 1.6, will be "
+ "ignored in 1.7, and removed in 1.8.", DeprecationWarning)
+ value = float(value)
+ if value < 0:
+ raise ValueError("'min_verify_time' must be >= 0")
+ self._mvtmap[cat] = value
+ elif key == "schemes":
+ if cat:
+ raise KeyError("'schemes' context option is not allowed "
+ "per category")
+ #else: cat=None already handled above
+ else:
+ raise KeyError("unknown CryptContext keyword: %r" % (key,))
+ categories.discard(None)
+ self._categories = categories = tuple(sorted(categories))
+
+ #-----------------------------------------------------------
+ # compile table of _CryptRecord instances, one for every
+ # (scheme,category) combination.
+ #-----------------------------------------------------------
+ # NOTE: could do all of this on-demand in _get_record(),
+ # but _CryptRecord() handles final validation of settings,
+ # and we want to alert the user to errors now instead of later.
records = self._records = {}
self._record_lists = {}
- handlers = policy._handlers
- if not handlers:
+ get_options = self._get_record_options
+ for handler in handlers:
+ scheme = handler.name
+ kwds, _ = get_options(scheme, None)
+ records[scheme, None] = _CryptRecord(handler, **kwds)
+ for cat in categories:
+ kwds, has_cat_options = get_options(scheme, cat)
+ if has_cat_options:
+ records[scheme, cat] = _CryptRecord(handler, **kwds)
+ # NOTE: if handler has no category-specific opts, _get_record()
+ # will automatically use the default category's record.
+ # NOTE: default records for specific category stored under the
+ # key (None,category); these are populated on-demand by _get_record().
+
+ @staticmethod
+ def _parse_config_key(ckey):
+ """helper used to parse ``cat__scheme__option`` keys into a tuple"""
+ # split string into 1-3 parts
+ assert isinstance(ckey, str)
+ parts = ckey.split("." if "." in ckey else "__")
+ count = len(parts)
+ if count == 1:
+ cat, scheme, key = None, None, parts[0]
+ elif count == 2:
+ cat = None
+ scheme, key = parts
+ elif count == 3:
+ cat, scheme, key = parts
+ else:
+ raise TypeError("keys must have less than 3 separators: %r" %
+ (ckey,))
+ # validate & normalize the parts
+ if cat == "default":
+ cat = None
+ elif not cat and cat is not None:
+ raise TypeError("empty category: %r" % ckey)
+ if scheme == "context":
+ scheme = None
+ elif not scheme and scheme is not None:
+ raise TypeError("empty scheme: %r" % ckey)
+ if not key:
+ raise TypeError("empty option: %r" % ckey)
+ return cat, scheme, key
+
+ def update(self, *args, **kwds):
+ """helper for quickly changing configuration.
+
+ this acts much like the dictionary :meth:`!update` method;
+ it accepts any keyword accepted by the :class:`CryptContext`
+ constructor, and updates the context's configuration,
+ replacing the original value(s).
+
+ .. versionadded:: 1.6
+
+ .. seealso:: :meth:`copy`
+ """
+ if args:
+ if len(args) > 1:
+ raise TypeError("expected at most one positional argument")
+ if kwds:
+ raise TypeError("positional arg and keywords mutually exclusive")
+ self.load(args[0], update=True)
+ elif kwds:
+ self.load(kwds, update=True)
+
+ # XXX: make this public?
+ def _simplify(self):
+ "helper to remove redundant/unused options"
+ # don't do anything if no schemes are defined
+ if not self._schemes:
return
- get_option = policy._get_option
- get_handler_options = policy._get_handler_options
- schemes = policy._schemes
- default_scheme = get_option(None, None, "default") or schemes[0]
- for cat in policy._categories:
- # build record for all schemes, re-using record from default
- # category if there aren't any category-specific options.
- for handler in handlers:
- scheme = handler.name
- kwds, has_cat_options = get_handler_options(scheme, cat)
- if cat and not has_cat_options:
- records[scheme, cat] = records[scheme, None]
+
+ def strip_items(target, filter):
+ keys = [key for key,value in iteritems(target)
+ if filter(key,value)]
+ for key in keys:
+ del target[key]
+
+ # remove redundant default.
+ defaults = self._default_schemes
+ if defaults.get(None) == self._schemes[0]:
+ del defaults[None]
+
+ # remove options for unused schemes.
+ scheme_options = self._scheme_options
+ schemes = self._schemes + ("all",)
+ strip_items(scheme_options, lambda k,v: k not in schemes)
+
+ # remove rendundant cat defaults.
+ cur = self.default_scheme()
+ strip_items(defaults, lambda k,v: k and v==cur)
+
+ # remove redundant category deprecations.
+ deprecated = self._deprecated_schemes
+ cur = self._deprecated_schemes.get(None)
+ strip_items(deprecated, lambda k,v: k and v==cur)
+
+ # remove redundant category options.
+ for scheme, config in iteritems(scheme_options):
+ if None in config:
+ cur = config[None]
+ strip_items(config, lambda k,v: k and v==cur)
+
+ # XXX: anything else?
+
+ #===================================================================
+ # reading configuration
+ #===================================================================
+ def _get_record_options(self, scheme, category):
+ """return composite dict of options for given scheme + category.
+
+ this is currently a private method, though some variant
+ of it's output may eventually be made public.
+
+ given a scheme & category, it returns two things:
+ a set of all the keyword options to pass to the _CryptRecord constructor,
+ and a bool flag indicating whether any of these options
+ were specific to the named category. if this flag is false,
+ the options are identical to the options for the default category.
+
+ the options dict includes all the scheme-specific settings,
+ as well as optional *deprecated* and *min_verify_time* keywords.
+ """
+ scheme_options = self._scheme_options
+ has_cat_options = False
+
+ # start with options common to all schemes
+ common_kwds = scheme_options.get("all")
+ if common_kwds is None:
+ kwds = {}
+ else:
+ # start with global options
+ tmp = common_kwds.get(None)
+ kwds = tmp.copy() if tmp is not None else {}
+
+ # add category options
+ if category:
+ tmp = common_kwds.get(category)
+ if tmp is not None:
+ kwds.update(tmp)
+ has_cat_options = True
+
+ # add scheme-specific options
+ scheme_kwds = scheme_options.get(scheme)
+ if scheme_kwds:
+ # add global options
+ tmp = scheme_kwds.get(None)
+ if tmp is not None:
+ kwds.update(tmp)
+
+ # add category options
+ if category:
+ tmp = scheme_kwds.get(category)
+ if tmp is not None:
+ kwds.update(tmp)
+ has_cat_options = True
+
+ # add deprecated flag
+ dep_map = self._deprecated_schemes
+ if dep_map:
+ deplist = dep_map.get(None)
+ dep = (deplist is not None and scheme in deplist)
+ if category:
+ deplist = dep_map.get(category)
+ if deplist is not None:
+ value = (scheme in deplist)
+ if value != dep:
+ dep = value
+ has_cat_options = True
+ if dep:
+ kwds['deprecated'] = True
+
+ # add min_verify_time setting
+ mvt_map = self._mvtmap
+ if mvt_map:
+ mvt = mvt_map.get(None)
+ if category:
+ value = mvt_map.get(category)
+ if value is not None and value != mvt:
+ mvt = value
+ has_cat_options = True
+ if mvt:
+ kwds['min_verify_time'] = mvt
+
+ return kwds, has_cat_options
+
+ def schemes(self, resolve=False):
+ """return schemes loaded into this CryptContext instance.
+
+ :returns:
+ returns list of schemes as a tuple.
+ if ``resolve=True``, returns the actual handler objects instead
+ of the names (but in the same order).
+
+ .. versionadded:: 1.6
+ This was previously available as ``CryptContext().policy.schemes()``
+ """
+ return self._handlers if resolve else self._schemes
+
+ # XXX: need to decide if exposing this would be useful to applications
+ # in any way that isn't already served by to_dict()
+ ##def deprecated_schemes(self, category=None, resolve=False):
+ ## """return tuple of deprecated schemes"""
+ ## depmap = self._deprecated_schemes
+ ## if category and category in depmap:
+ ## deplist = depmap[category]
+ ## elif None in depmap:
+ ## deplist = depmap[None]
+ ## else:
+ ## return self.schemes(resolve)
+ ## if resolve:
+ ## return tuple(handler for handler in self._handlers
+ ## if handler.name not in deplist)
+ ## else:
+ ## return tuple(scheme for scheme in self._schemes
+ ## if scheme not in deplist)
+
+ def _is_deprecated_scheme(self, scheme, category=None):
+ "helper used by unittests to check if scheme is deprecated"
+ return self._get_record(scheme, category).deprecated
+# kwds, _ = self._get_record_options(scheme, category)
+# return bool(kwds.get("deprecated"))
+
+ def default_scheme(self, category=None, resolve=False):
+ """return name of scheme that :meth:`encrypt` will use by default.
+
+ .. versionadded:: 1.6
+ """
+ if resolve:
+ scheme = self.default_scheme(category)
+ for handler in self._handlers:
+ if handler.name == scheme:
+ return handler
+ raise AssertionError("failed to find matching handler")
+ defaults = self._default_schemes
+ if defaults:
+ try:
+ return defaults[category]
+ except KeyError:
+ pass
+ if category:
+ try:
+ return defaults[None]
+ except KeyError:
+ pass
+ try:
+ return self._schemes[0]
+ except IndexError:
+ raise KeyError("no crypt algorithms loaded in this "
+ "CryptContext instance")
+
+ # XXX: need to decide if exposing this would be useful in any way
+ ##def categories(self):
+ ## """return user-categories with algorithm-specific options in this CryptContext.
+ ##
+ ## this will always return a tuple.
+ ## if no categories besides the default category have been configured,
+ ## the tuple will be empty.
+ ## """
+ ## return self._categories
+
+ # XXX: need to decide if exposing this would be useful to applications
+ # in any meaningful way that isn't already served by to_dict()
+ ##def options(self, scheme, category=None):
+ ## kwds, percat = self._config.get_options(scheme, category)
+ ## kwds.pop("min_verify_time", None)
+ ## return kwds
+
+ def handler(self, scheme="default", category=None):
+ """helper to resolve name of scheme -> handler object.
+
+ the scheme may optionally be set to ``"default"``,
+ in which case the handler attached to the default scheme will be
+ returned. if ``category`` is specified, the default for that
+ category will be returned.
+
+ this will raise a :exc:`KeyError` if no scheme of that name has been
+ loaded into this CryptContext.
+
+ .. versionadded:: 1.6
+ This was previously available as ``CryptContext().policy.get_handler()``
+ """
+ if scheme == "default":
+ return self.default_scheme(category, True)
+ for handler in self._handlers:
+ if handler.name == scheme:
+ return handler
+ if self._handlers:
+ raise KeyError("crypt algorithm not found in this "
+ "CryptContext instance: %r" % (scheme,))
+ else:
+ raise KeyError("no crypt algorithms loaded in this "
+ "CryptContext instance")
+
+ def _has_unregistered(self):
+ "check if any handlers in this context aren't in the global registry"
+ return not all(_is_handler_registered(handler)
+ for handler in self._handlers)
+
+ #===================================================================
+ # exporting config
+ #===================================================================
+ def _iter_config(self, resolve=False):
+ """regenerate original config.
+
+ this is an iterator which yields ``(cat,scheme,option),value`` items,
+ in the order they generally appear inside an INI file.
+ if interpreted as a dictionary, it should match the original
+ keywords passed to the CryptContext (aside from any canonization).
+
+ it's mainly used as the internal backend for most of the public
+ serialization methods.
+ """
+ # grab various bits of data
+ defaults = self._default_schemes
+ deprecated = self._deprecated_schemes
+ mvt = self._mvtmap
+ scheme_options = self._scheme_options
+ schemes = sorted(scheme_options)
+
+ # write loaded schemes (may differ from 'schemes' local var)
+ value = self._schemes
+ if value:
+ if resolve:
+ value = self._handlers
+ yield (None, None, "schemes"), list(value)
+
+ # then run through config for each user category
+ for cat in (None,) + self._categories:
+
+ # write default scheme (if set)
+ value = defaults.get(cat)
+ if value is not None:
+ yield (cat, None, "default"), value
+
+ # write deprecated-schemes list (if set)
+ value = deprecated.get(cat)
+ if value is not None:
+ yield (cat, None, "deprecated"), list(value)
+
+ # write mvt (if set)
+ value = mvt.get(cat)
+ if value is not None:
+ yield (cat, None, "min_verify_time"), value
+
+ # write per-scheme options for all schemes.
+ for scheme in schemes:
+ try:
+ kwds = scheme_options[scheme][cat]
+ except KeyError:
+ pass
else:
- records[scheme, cat] = _CryptRecord(handler, cat, **kwds)
- # clone default scheme's record to None so we can resolve default
- if cat:
- scheme = get_option(None, cat, "default") or default_scheme
+ for key in sorted(kwds):
+ yield (cat, scheme, key), kwds[key]
+
+ @staticmethod
+ def _render_config_key(key, compact=False):
+ "convert 3-part config key to single string"
+ cat, scheme, option = key
+ if cat:
+ fmt = "%s.%s.%s" if compact else "%s__%s__%s"
+ return fmt % (cat, scheme or "context", option)
+ elif scheme:
+ fmt = "%s.%s" if compact else "%s__%s"
+ return fmt % (scheme, option)
+ else:
+ return option
+
+ @staticmethod
+ def _render_ini_value(key, value):
+ "render value to string suitable for INI file"
+ # convert lists to comma separated lists
+ # (mainly 'schemes' & 'deprecated')
+ if isinstance(value, (list,tuple)):
+ value = ", ".join(value)
+
+ # convert numbers to strings
+ elif isinstance(value, num_types):
+ if isinstance(value, float) and key[2] == "vary_rounds":
+ value = ("%.2f" % value).rstrip("0") if value else "0"
else:
- scheme = default_scheme
- records[None, cat] = records[scheme, cat]
+ value = str(value)
+
+ assert isinstance(value, str), \
+ "expected string for key: %r %r" % (key, value)
+
+ #escape any percent signs.
+ return value.replace("%", "%%")
+
+ def to_dict(self, resolve=False):
+ """return as config dictionary;
+
+ if ``resolve=True``, the ``schemes`` key will contain
+ a list of handler objects rather than just their names.
+
+ the output of this should be acceptable as input
+ to the CryptContext constructor.
+
+ usage example::
+
+ >>> # you can dump the configuration of any crypt context...
+ >>> from passlib.apps import ldap_nocrypt_context
+ >>> ldap_nocrypt_context.to_dict()
+ {'schemes': ['ldap_salted_sha1',
+ 'ldap_salted_md5',
+ 'ldap_sha1',
+ 'ldap_md5',
+ 'ldap_plaintext']}
+
+ .. versionadded:: 1.6
+ This was previously available as ``CryptContext().policy.to_dict()``
+ """
+ # XXX: should resolve default to conditional behavior
+ # based on presence of unregistered handlers?
+ render_key = self._render_config_key
+ return dict((render_key(key), value)
+ for key, value in self._iter_config(resolve))
+
+ def _write_to_parser(self, parser, section, compact=False):
+ "helper to write to ConfigParser instance"
+ render_key = self._render_config_key
+ render_value = self._render_ini_value
+ parser.add_section(section)
+ for k,v in self._iter_config():
+ v = render_value(k, v)
+ k = render_key(k, compact)
+ parser.set(section, k, v)
+
+ def to_string(self, section="passlib", compact=False):
+ """serialize to INI format and return as unicode string.
+
+ :param section:
+ name of INI section to output, defaults to ``"passlib"``.
+
+ :param compact:
+ if ``True``, this will attempt to return as short a string
+ as possible, rather than a readable one.
+
+ :returns:
+ CryptContext configuration, serialized to a INI unicode string.
+
+ usage example::
+
+ >>> # you can dump the configuration of any crypt context...
+ >>> from passlib.apps import ldap_nocrypt_context
+ >>> print ldap_nocrypt_context.to_string()
+ [passlib]
+ schemes = ldap_salted_sha1, ldap_salted_md5, ldap_sha1, ldap_md5, ldap_plaintext
+
+ passing the output of this method into :meth:`load`,
+ :meth:`from_string` or :meth:`from_file` should recreate
+ the exact state of the :class:`CryptContext` instance.
+
+ .. versionadded:: 1.6
+ This was previously available as ``CryptContext().policy.to_string()``
+ """
+ parser = SafeConfigParser()
+ self._write_to_parser(parser, section, compact)
+ buf = NativeStringIO()
+ parser.write(buf)
+ out = buf.getvalue()
+ if compact:
+ out = out.replace(", ", ",").rstrip() + "\n"
+ out = re.sub(r"(?m)^([\w.]+)\s+=\s*", r"\1=", out)
+ else:
+ names = [ handler.name for handler in self._handlers
+ if not _is_handler_registered(handler) ]
+ if names:
+ names = ", ".join(repr(name) for name in names)
+ out += "# NOTE: the %s handler(s) are not registered with Passlib,\n" % names
+ out += "# so this string may not correctly reproduce the current configuration.\n\n"
+ if not PY3:
+ out = out.decode("utf-8")
+ return out
- def _get_record(self, scheme, category=None, required=True):
- "private helper used by CryptContext"
+ # XXX: is this useful enough to enable?
+ ##def write_to_path(self, path, section="passlib", update=False):
+ ## "write to INI file"
+ ## parser = ConfigParser()
+ ## if update and os.path.exists(path):
+ ## if not parser.read([path]):
+ ## raise EnvironmentError("failed to read existing file")
+ ## parser.remove_section(section)
+ ## self._write_to_parser(parser, section)
+ ## fh = file(path, "w")
+ ## parser.write(fh)
+ ## fh.close()
+
+ #===================================================================
+ # _CryptRecord cache
+ #===================================================================
+
+ # NOTE: the CryptContext object takes the current configuration,
+ # and creates a _CryptRecord containing the settings for each
+ # (scheme,category) combination. This is used by encrypt() etc
+ # to do a quick lookup of the appropriate record,
+ # and hand off the real work to the record's methods,
+ # which are optimized for the specific set of options.
+
+ def _get_record(self, scheme, category=None):
+ "return record for specific scheme & category (cached)"
+ # quick lookup in cache
try:
return self._records[scheme, category]
except KeyError:
pass
+
+ # if scheme=None, use category's default scheme,
+ # and cache for next time.
+ if not scheme:
+ default = self.default_scheme(category)
+ assert default
+ record = self._records[None, category] = self._get_record(default,
+ category)
+ return record
+
+ # if category has no record for scheme, use default category's record,
+ # and cache for next time.
if category:
- # category not referenced in policy file.
- # so populate cache from default category.
- cache = self._records
try:
- record = cache[scheme, None]
+ cache = self._records
+ record = cache[scheme, category] = cache[scheme, None]
+ return record
except KeyError:
pass
- else:
- cache[scheme, category] = record
- return record
- if not required:
- return None
- elif scheme:
+
+ # scheme not found in configuration.
+ if scheme:
raise KeyError("crypt algorithm not found in policy: %r" %
(scheme,))
else:
- assert not self._policy._handlers
+ assert not self._schemes, "somehow lost default scheme!"
raise KeyError("no crypt algorithms supported")
def _get_record_list(self, category=None):
- "return list of records for category"
+ "return list of records for category (cached)"
try:
return self._record_lists[category]
except KeyError:
- # XXX: could optimize for categories not in policy.
- get = self._get_record
value = self._record_lists[category] = [
- get(scheme, category)
- for scheme in self._policy._schemes
- ]
+ self._get_record(scheme, category)
+ for scheme in self._schemes
+ ]
return value
def _identify_record(self, hash, category, required=True):
"""internal helper to identify appropriate _CryptRecord for hash"""
+ # FIXME: if multiple hashes could match (e.g. lmhash vs nthash)
+ # this will only return first match. might want to do something
+ # about this in future, but for now only hashes with unique identifiers
+ # will work properly in a CryptContext.
if not isinstance(hash, base_string_types):
raise ExpectedStringError(hash, "hash")
records = self._get_record_list(category)
@@ -1327,14 +2188,14 @@ class CryptContext(object):
raise ValueError("hash could not be identified")
#===================================================================
- #password hash api proxy methods
+ # password hash api
#===================================================================
# NOTE: all the following methods do is look up the appropriate
# _CryptRecord for a given (scheme,category) combination,
- # and then let the record object take care of the rest,
- # since it will have optimized itself for the particular
- # settings used within the policy by that (scheme,category).
+ # and then let the record object take care of the rest.
+ # Each record object stores the options used
+ # by the specific (scheme,category) combination it manages.
# XXX: would a better name be needs_update/is_deprecated?
def hash_needs_update(self, hash, scheme=None, category=None):
@@ -1346,7 +2207,7 @@ class CryptContext(object):
number of rounds, and other properties against the current policy;
and returns True if the hash is using a deprecated scheme,
or is otherwise outside of the bounds specified by the policy.
- if so, the password should be re-encrypted using ``ctx.encrypt(passwd)``.
+ if so, the password should be re-encrypted using :meth:`encrypt`.
:arg hash: existing hash string
:param scheme: optionally identify specific scheme to check against.
@@ -1382,7 +2243,7 @@ class CryptContext(object):
(using the default if none other is specified).
See the :ref:`password-hash-api` for details.
"""
- #XXX: could insert normalization to preferred unicode encoding here
+ # XXX: could insert normalization to preferred unicode encoding here
return self._get_record(scheme, category).genhash(secret, config,
**context)
@@ -1429,8 +2290,24 @@ class CryptContext(object):
:returns:
The secret as encoded by the specified algorithm and options.
+
+ usage example::
+
+ >>> # given an existing CryptContext...
+ >>> from passlib.apps import custom_app_context
+
+ >>> # calling encrypt will generate a new salt, and hash
+ >>> # the password using the context's default scheme
+ >>> custom_app_context.encrypt("fooey")
+ '$5$rounds=39987$WWelpiPfgTdTrpSr$tosg/YcCKCzePQnB6eseVh8YdSKRkCJ6uQ/QpXoEUm.'
+
+ >>> # optionally you can specify a particular scheme and options
+ >>> # (though if these options are to be hardcoded, you should
+ >>> # just create your own CryptContext instance).
+ >>> custom_app_context.encrypt("fooey", "sha512_crypt", rounds=5000)
+ '$6$D3OcBBac5qGdvIbT$mQvIwPS8bWx2DnSrZrFK3e1Rie1vv8hlixCoBfDSJ..Bg1E0PJVzKzAkdt/cBm9CdADrCxv6tOPjqqn8AxpF01'
"""
- #XXX: could insert normalization to preferred unicode encoding here
+ # XXX: could insert normalization to preferred unicode encoding here
return self._get_record(scheme, category).encrypt(secret, **kwds)
def verify(self, secret, hash, scheme=None, category=None, **context):
@@ -1464,14 +2341,14 @@ class CryptContext(object):
record = self._get_record(scheme, category)
else:
record = self._identify_record(hash, category)
- # XXX: strip context kwds if scheme doesn't use them?
+ # XXX: have record strip context kwds if scheme doesn't use them?
# XXX: could insert normalization to preferred unicode encoding here
return record.verify(secret, hash, **context)
def verify_and_update(self, secret, hash, scheme=None, category=None, **kwds):
"""verify secret and check if hash needs upgrading, in a single call.
- This is a convience method for a common situation in most applications:
+ This is a convenience method for a common situation in most applications:
When a user logs in, they must :meth:`verify` if the password matches;
if successful, check if the hash algorithm
has been deprecated (:meth:`hash_needs_update`); and if so,
@@ -1511,11 +2388,12 @@ class CryptContext(object):
record = self._get_record(scheme, category)
else:
record = self._identify_record(hash, category)
- # XXX: strip context kwds if scheme doesn't use them?
- # XXX: could insert normalization to preferred unicode encoding here
+ # XXX: have record strip context kwds if scheme doesn't use them?
+ # XXX: could insert normalization to preferred unicode encoding here.
if not record.verify(secret, hash, **kwds):
return False, None
elif record.hash_needs_update(hash):
+ # NOTE: we re-encrypt with default scheme, not current one.
return True, self.encrypt(secret, None, category, **kwds)
else:
return True, None
@@ -1536,20 +2414,28 @@ class LazyCryptContext(CryptContext):
the first positional argument can be a list of schemes, or omitted,
just like CryptContext.
- :param create_policy:
+ :param onload:
if a callable is passed in via this keyword,
it will be invoked at lazy-load time
with the following signature:
- ``create_policy(**kwds) -> CryptPolicy``;
+ ``onload(**kwds) -> kwds``;
where ``kwds`` is all the additional kwds passed to LazyCryptContext.
- It should return a CryptPolicy instance, which will then be used
- by the CryptContext.
+ It should perform any additional deferred initialization,
+ and return the final dict of options to be passed to CryptContext.
+
+ .. versionadded:: 1.6
+
+ :param create_policy:
+
+ .. deprecated:: 1.6
+ This option will be removed in Passlib 1.8.
+ Applications should use *onload* instead.
:param kwds:
- All additional keywords are passed to CryptPolicy;
- or to the create_policy function if provided.
+ All additional keywords are passed to CryptContext;
+ or to the *onload* function (if provided).
This is mainly used internally by modules such as :mod:`passlib.apps`,
which define a large number of contexts, but only a few of them will be needed
@@ -1565,7 +2451,7 @@ class LazyCryptContext(CryptContext):
# previously it just called _lazy_init() when ``.policy`` was
# first accessed. now that is done whenever any of the public
# attributes are accessed, and the class itself is changed
- # to a regular CryptContext, to remove the overhead one it's unneeded.
+ # to a regular CryptContext, to remove the overhead once it's unneeded.
def __init__(self, schemes=None, **kwds):
if schemes is not None:
@@ -1575,16 +2461,26 @@ class LazyCryptContext(CryptContext):
def _lazy_init(self):
kwds = self._lazy_kwds
if 'create_policy' in kwds:
+ warn("The CryptPolicy class, and LazyCryptContext's "
+ "``create_policy`` keyword have been deprecated as of "
+ "Passlib 1.6, and will be removed in Passlib 1.8; "
+ "please use the ``onload`` keyword instead.",
+ DeprecationWarning)
create_policy = kwds.pop("create_policy")
- policy = create_policy(**kwds)
- kwds = dict(policy=CryptPolicy.from_source(policy))
- super(LazyCryptContext, self).__init__(**kwds)
+ result = create_policy(**kwds)
+ policy = CryptPolicy.from_source(result, _warn=False)
+ kwds = policy._context.to_dict()
+ elif 'onload' in kwds:
+ onload = kwds.pop("onload")
+ kwds = onload(**kwds)
del self._lazy_kwds
+ super(LazyCryptContext, self).__init__(**kwds)
self.__class__ = CryptContext
def __getattribute__(self, attr):
- if not attr.startswith("_"):
- self._lazy_init()
+ if (not attr.startswith("_") or attr.startswith("__")) and \
+ self._lazy_kwds is not None:
+ self._lazy_init()
return object.__getattribute__(self, attr)
#=========================================================