Commit 5f1cde9f authored by Eli Collins's avatar Eli Collins

Merge from default

--HG--
branch : ironpython-support-dev
glob:docs/_build
glob:*.pyc
glob:*.egg-info
glob:.coverage
......@@ -8,3 +7,4 @@ glob:dist
glob:*$py.class
glob:MANIFEST
glob:.tox
glob:app.yaml
......@@ -53,6 +53,9 @@ Release History
* The :doc:`ldap salted digests </lib/passlib.hash.ldap_std>`
now support salts from 4-16 bytes [issue 30].
* :class:`bsdi_crypt` now issues a warning if an even number of rounds
is requested by the application, due to a known weakness in DES.
* All hashes will now throw :exc:`~passlib.exc.PasswordSizeError`
if the provided password is larger than 4096 characters.
......@@ -66,32 +69,45 @@ Release History
.. currentmodule:: passlib.context
* :class:`~CryptContext` now supports a :ref:`passprep <passprep>` option,
which runs all passwords through SASLPrep (:rfc:`4013`)
which can be used to run all passwords through SASLPrep (:rfc:`4013`),
in order to normalize their unicode representation before hashing
[issue 24].
* Internals of :class:`CryptPolicy` have been
re-written drastically. Should now be stricter (and more informative)
about invalid values, and common :class:`CryptContext`
operations should all have much shorter code-paths.
* The :class:`!CryptContext` option
:ref:`min_verify_time <min-verify-time>` has been deprecated,
will be ignored in release 1.7, and will be removed in release 1.8.
* The internals of :class:`!CryptContext` have been rewritten
drastically. It's methods should now be stricter and more informative
about invalid values; and common :class:`!CryptContext` operations
should be faster, and have shorter internal code paths.
* Config parsing now done with :class:`SafeConfigParser`.
:meth:`CryptPolicy.from_path` and :meth:`CryptPolicy.from_string`
previously used :class:`!ConfigParser` interpolation.
Release 1.5 switched to :class:`SafeConfigParser`,
* The :attr:`!CryptContext.policy` attr, and the supporting
:class:`!CryptPolicy` class, have been deprecated in their entirety.
They will not be removed until Passlib 1.8, to give applications
which used these features time to migrate. Applications which did
not use either of these features explicitly should be unaffected by
this change.
The functionality of :class:`!CryptPolicy` has been merged
into the :class:`CryptContext` class, in order to simplify
the exposed interface. Information on migrating can be found
in the :class:`CryptPolicy` documentation, as well as in
the :exc:`DeprecationWarning` messages issued when a :class:`!CryptPolicy`
is invoked.
* :meth:`CryptContext.from_path` and :meth:`CryptContext.from_string`
(and the legacy :class:`CryptPolicy` object) now use stdlib's
:class:`!SafeConfigParser`.
Previous releases used the original :class:`!ConfigParser` interpolation.
Passlib 1.5 switched to :class:`SafeConfigParser`,
but kept support for the old format as a (deprecated) fallback.
This fallback has been removed in 1.6; any
legacy config files may need to escape raw ``%`` characters
legacy config files may need to double any raw ``%`` characters
in order to load successfully.
* The main CryptContext methods (e.g. :meth:`~CryptContext.encrypt`,
and :meth:`~CryptContext.verify`) will now consistently raise
a :exc:`TypeError` when called with ``hash=None`` or another
non-string type, to match the :doc:`password-hash-api`.
Under previous releases, they might return ``False``,
raise :exc:`ValueError`, or raise :exc:`TypeError`,
depending on the specific method and context settings.
Utils
.. currentmodule:: passlib.utils.handlers
......@@ -134,12 +150,13 @@ Release History
* deprecated some unused functions in :mod:`!passlib.utils`,
they will be removed in release 1.7.
* The :class:`!CryptContext` option
:ref:`min_verify_time <min-verify-time>` has been deprecated,
will be ignored in release 1.7, and will be removed in release 1.8.
Other
* The api for the :mod:`passlib.apache` module has been updated
to add more flexibility, and to fix some ambiguous method
and keyword names. The old names are still supported, but deprecated,
and will be removed in Passlib 1.8.
* Handle platform-specific error strings returned by :func:`!crypt.crypt`.
* Passlib is now source-compatible with Python 2.5+ and Python 3,
......
......@@ -7,15 +7,15 @@ parsing was being sped up. it could definitely be improved.
# init script env
#=============================================================================
import os, sys
root_dir = os.path.join(os.path.dirname(__file__), os.path.pardir)
sys.path.insert(0, root_dir)
root = os.path.join(os.path.dirname(__file__), os.path.pardir)
sys.path.insert(0, root)
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
from timeit import Timer
import os
import warnings
# site
# pkg
......@@ -26,18 +26,99 @@ except ImportError:
import passlib.utils.handlers as uh
from passlib.utils.compat import u, print_, unicode
# local
__all__ = [
]
#=============================================================================
# benchmarking support
#=============================================================================
class benchmark:
"class to hold various benchmarking helpers"
@classmethod
def constructor(cls, **defaults):
"""mark callable as something which should be benchmarked.
callable should return a function will be timed.
"""
def marker(func):
if func.__doc__:
name = func.__doc__.splitlines()[0]
else:
name = func.__name__
func._benchmark_task = ("ctor", name, defaults)
return func
return marker
@classmethod
def run(cls, source, **defaults):
"""run benchmark for all tasks in source, yielding result records"""
for obj in source.values():
for record in cls._run_object(obj, defaults):
yield record
@classmethod
def _run_object(cls, obj, defaults):
args = getattr(obj, "_benchmark_task", None)
if not args:
return
mode, name, options = args
kwds = defaults.copy()
kwds.update(options)
if mode == "ctor":
itr = obj()
if not hasattr(itr, "next"):
itr = [itr]
for func in itr:
# TODO: per function name & options
secs, precision = cls.measure(func, None, **kwds)
yield name, secs, precision
else:
raise ValueError("invalid mode: %r" % (mode,))
@staticmethod
def measure(func, setup=None, maxtime=1, bestof=3):
"""timeit() wrapper which tries to get as accurate a measurement as
possible w/in maxtime seconds.
:returns:
``(avg_seconds_per_call, log10_number_of_repetitions)``
"""
from timeit import Timer
from math import log
timer = Timer(func, setup=setup or '')
number = 1
while True:
delta = min(timer.repeat(bestof, number))
maxtime -= delta*bestof
if maxtime < 0:
return delta/number, int(log(number, 10))
number *= 10
@staticmethod
def pptime(secs, precision=3):
"""helper to pretty-print fractional seconds values"""
usec = int(secs * 1e6)
if usec < 1000:
return "%.*g usec" % (precision, usec)
msec = usec / 1000
if msec < 1000:
return "%.*g msec" % (precision, msec)
sec = msec / 1000
return "%.*g sec" % (precision, sec)
#=============================================================================
# utils
#=============================================================================
sample_config_1p = os.path.join(root, "passlib", "tests", "sample_config_1s.cfg")
class BlankHandler(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
from passlib.context import CryptContext
if hasattr(CryptContext, "from_path"):
CryptPolicy = None
else:
from passlib.context import CryptPolicy
setting_kwds = ("rounds", "salt", "salt_size")
class BlankHandler(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
name = "blank"
ident = u("$b$")
setting_kwds = ("rounds", "salt", "salt_size")
checksum_size = 1
min_salt_size = max_salt_size = 1
......@@ -62,99 +143,109 @@ class AnotherHandler(BlankHandler):
name = "another"
ident = u("$a$")
SECRET = u("toomanysecrets")
OTHER = u("setecastronomy")
#=============================================================================
# crypt context tests
# CryptContext benchmarks
#=============================================================================
def setup_policy():
import os
from passlib.context import CryptPolicy
test_path = os.path.join(root_dir, "passlib", "tests", "sample_config_1s.cfg")
def test_policy_creation():
with open(test_path, "rb") as fh:
policy1 = CryptPolicy.from_string(fh.read())
yield test_policy_creation
default = CryptPolicy.from_path(test_path)
def test_policy_composition():
policy2 = default.replace(
schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt",
"des_crypt", "unix_fallback" ],
deprecated = [ "des_crypt" ],
)
yield test_policy_composition
secret = u("secret")
other = u("other")
def setup_context():
from passlib.context import CryptContext
def test_context_init():
return CryptContext(
@benchmark.constructor()
def test_context_from_path():
"test speed of CryptContext.from_path()"
path = sample_config_1p
if CryptPolicy:
def helper():
CryptPolicy.from_path(path)
else:
def helper():
CryptContext.from_path(path)
return helper
@benchmark.constructor()
def test_context_update():
"test speed of CryptContext.update()"
kwds = dict(
schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt",
"des_crypt", "unix_fallback" ],
deprecated = [ "des_crypt" ],
sha512_crypt__min_rounds=4000,
)
if CryptPolicy:
policy=CryptPolicy.from_path(sample_config_1p)
def helper():
policy.replace(**kwds)
else:
ctx = CryptContext.from_path(sample_config_1p)
def helper():
ctx.copy(**kwds)
return helper
@benchmark.constructor()
def test_context_init():
"test speed of CryptContext() constructor"
kwds = dict(
schemes=[BlankHandler, AnotherHandler],
default="another",
blank__min_rounds=1500,
blank__max_rounds=2500,
another__vary_rounds=100,
)
yield test_context_init
)
def helper():
CryptContext(**kwds)
return helper
ctx = test_context_init()
def test_context_calls():
hash = ctx.encrypt(secret, rounds=2001)
ctx.verify(secret, hash)
ctx.verify_and_update(secret, hash)
ctx.verify_and_update(other, hash)
yield test_context_calls
@benchmark.constructor()
def test_context_calls():
"test speed of CryptContext password methods"
ctx = CryptContext(
schemes=[BlankHandler, AnotherHandler],
default="another",
blank__min_rounds=1500,
blank__max_rounds=2500,
another__vary_rounds=100,
)
def helper():
hash = ctx.encrypt(SECRET, rounds=2001)
ctx.verify(SECRET, hash)
ctx.verify_and_update(SECRET, hash)
ctx.verify_and_update(OTHER, hash)
return helper
def setup_handlers():
#=============================================================================
# handler benchmarks
#=============================================================================
@benchmark.constructor()
def test_md5_crypt_builtin():
"test test md5_crypt builtin backend"
from passlib.hash import md5_crypt
md5_crypt.set_backend("builtin")
def test_md5_crypt():
hash = md5_crypt.encrypt(secret)
md5_crypt.verify(secret, hash)
md5_crypt.verify(other, hash)
yield test_md5_crypt
def helper():
hash = md5_crypt.encrypt(SECRET)
md5_crypt.verify(SECRET, hash)
md5_crypt.verify(OTHER, hash)
yield helper
@benchmark.constructor()
def test_ldap_salted_md5():
"test ldap_salted_md5"
from passlib.hash import ldap_salted_md5 as handler
def helper():
hash = handler.encrypt(SECRET, salt='....')
handler.verify(SECRET, hash)
handler.verify(OTHER, hash)
yield helper
#=============================================================================
# main
#=============================================================================
def pptime(secs):
precision = 3
usec = int(secs * 1e6)
if usec < 1000:
return "%.*g usec" % (precision, usec)
msec = usec / 1000
if msec < 1000:
return "%.*g msec" % (precision, msec)
sec = msec / 1000
return "%.*g sec" % (precision, sec)
def main(*args):
names = args
source = globals()
for key in sorted(source):
if not key.startswith("setup_"):
continue
sname = key[6:]
setup = source[key]
for test in setup():
name = test.__name__
if name.startswith("test_"):
name = name[5:]
if names and name not in names:
continue
timer = Timer(test)
number = 1
while True:
t = timer.timeit(number)
if t > .2:
break
number *= 10
repeat = 3
best = min(timer.repeat(repeat, number)) / number
print_("%30s %s" % (name, pptime(best)))
if args:
orig = source
source = dict((k,orig[k]) for k in orig if k in args)
helper = benchmark.run(source, maxtime=2, bestof=3)
for name, secs, precision in helper:
print_("%-50s %9s (%d)" % (name, benchmark.pptime(secs), precision))
if __name__ == "__main__":
import sys
......
application: fake-app
version: 2
runtime: python
api_version: 1
handlers:
- url: /.*
script: dummy.py
......@@ -8,6 +8,13 @@
This module provides utilities for reading and writing Apache's
htpasswd and htdigest files; though the use of two helper classes.
.. versionchanged:: 1.6
The api for this module was updated to be more flexible,
and to have (hopefully) less confusing method names.
The old method and keyword names are supported but deprecated, and
will be removed in Passlib 1.8.
No more backwards-incompatible changes are currently planned.
.. index:: apache; htpasswd
Htpasswd Files
......@@ -17,34 +24,34 @@ A quick summary of it's usage::
>>> from passlib.apache import HtpasswdFile
>>> #when creating a new file, set to autoload=False, add entries, and save.
>>> ht = HtpasswdFile("test.htpasswd", autoload=False)
>>> ht.update("someuser", "really secret password")
>>> # when creating a new file, set to new=True, add entries, and save.
>>> ht = HtpasswdFile("test.htpasswd", new=True)
>>> ht.set_password("someuser", "really secret password")
>>> ht.save()
>>> #loading an existing file to update a password
>>> # loading an existing file to update a password
>>> ht = HtpasswdFile("test.htpasswd")
>>> ht.update("someuser", "new secret password")
>>> ht.set_password("someuser", "new secret password")
>>> ht.save()
>>> #examining file, verifying user's password
>>> # examining file, verifying user's password
>>> ht = HtpasswdFile("test.htpasswd")
>>> ht.users()
[ "someuser" ]
>>> ht.verify("someuser", "wrong password")
>>> ht.check_password("someuser", "wrong password")
False
>>> ht.verify("someuser", "new secret password")
>>> ht.check_password("someuser", "new secret password")
True
>>> #making in-memory changes and exporting to string
>>> # making in-memory changes and exporting to string
>>> ht = HtpasswdFile()
>>> ht.update("someuser", "mypass")
>>> ht.update("someuser", "anotherpass")
>>> ht.set_password("someuser", "mypass")
>>> ht.set_password("someuser", "anotherpass")
>>> print ht.to_string()
someuser:$apr1$T4f7D9ly$EobZDROnHblCNPCtrgh5i/
anotheruser:$apr1$vBdPWvh1$GrhfbyGvN/7HalW5cS9XB1
.. autoclass:: HtpasswdFile(path, default=None, autoload=True)
.. autoclass:: HtpasswdFile(path=None, new=False, autosave=False, ...)
.. index:: apache; htdigest
......@@ -53,7 +60,7 @@ Htdigest Files
The :class:`!HtdigestFile` class allows management of htdigest files
in a similar fashion to :class:`HtpasswdFile`.
.. autoclass:: HtdigestFile(path, autoload=True)
.. autoclass:: HtdigestFile(path, default_realm=None, new=False, autosave=False, ...)
.. rubric:: Footnotes
......
......@@ -8,8 +8,7 @@
.. currentmodule:: passlib.context
This details all the constructors and methods provided by :class:`!CryptContext`
and :class:`!CryptPolicy`.
This details all the constructors and methods provided by :class:`!CryptContext`.
.. seealso::
......@@ -19,12 +18,14 @@ and :class:`!CryptPolicy`.
The Context Object
==================
.. autoclass:: CryptContext(schemes=None, policy=<default policy>, \*\*kwds)
The Policy Object
=================
.. autoclass:: CryptPolicy(\*\*kwds)
.. autoclass:: CryptContext(schemes=None, \*\*kwds)
Other Helpers
=============
.. autoclass:: LazyCryptContext([schemes=None,] **kwds [, create_policy=None])
.. autoclass:: LazyCryptContext([schemes=None,] \*\*kwds [, onload=None])
.. rst-class:: html-toggle
(deprecated) The CryptPolicy Class
==================================
.. autoclass:: CryptPolicy
......@@ -9,9 +9,13 @@
.. currentmodule:: passlib.context
The :class:`CryptContext` accepts a number of keyword options.
These are divides into the "context options", which affect
the context instance directly, and the "hash options",
which affect the context treats a particular type of hash:
These can be provided to any of the CryptContext constructor methods,
as well as the :meth:`CryptContext.update` method, or any configuration
string or INI file passed to :meth:`CryptContext.load`.
The options are divided into two categories: "context options", which directly
affect the :class:`!CryptContext` object itself; and "hash options", which
affect the behavior of a particular password hashing scheme.
.. seealso::
......@@ -21,8 +25,7 @@ which affect the context treats a particular type of hash:
Context Options
===============
The following keyword options are accepted by both the :class:`CryptContext`
and :class:`CryptPolicy` constructors, and directly affect the behavior
The following keyword options directly affect the behavior
of the :class:`!CryptContext` instance itself:
``schemes``
......@@ -41,15 +44,14 @@ of the :class:`!CryptContext` instance itself:
``deprecated``
List of handler names which should be considered deprecated by the CryptContext.
This should be a subset of the names of the handlers listed in schemes.
This is optional, if not specified, no handlers will be considered deprecated.
This should be a subset of the names of the handlers listed in *schemes*.
This is optional, and if not specified, no handlers will be considered deprecated.
For use in INI files, this may also be specified as a single comma-separated string
For INI files, this may also be specified as a single comma-separated string
of handler names.
This is primarily used by :meth:`CryptContext.hash_needs_update` and
:meth:`CryptPolicy.handler_is_deprecated`. If the application does not use
these methods, this option can be ignored.
This is primarily used by :meth:`CryptContext.hash_needs_update`.
If the application does not use this method, this option can be ignored.
Example: ``deprecated=["des_crypt"]``.
......@@ -77,25 +79,22 @@ of the :class:`!CryptContext` instance itself:
For symmetry with the format of the hash option keywords (below),
all of the above context option keywords may also be specified
using the format :samp:`context__{option}` (note double underscores),
or :samp:`context.{option}` within INI files.
using the format :samp:`context__{option}` (note double underscores).
.. note::
To override context options for a particular :ref:`user category <user-categories>`,
use the format :samp:`{category}__context__{option}`,
or :samp:`{category}.context.{option}` within an INI file.
use the format :samp:`{category}__context__{option}`.
Hash Options
============
The following keyword options are accepted by both the :class:`CryptContext`
and :class:`CryptPolicy` constructors, and affect how a :class:`!CryptContext` instance
treats hashes belonging to a particular hash scheme, as identified by the hash's handler name.
The following keyword option affect how a :class:`!CryptContext` instance
treats hashes belonging to a particular hash scheme,
as identified by the scheme's name.
All hash option keywords should be specified using the format :samp:`{hash}__{option}`
(note double underscores); where :samp:`{hash}` is the name of the hash's handler,
and :samp:`{option}` is the name of the specific options being set.
Within INI files, this may be specified using the alternate format :samp:`{hash}.{option}`.
:samp:`{hash}__default_rounds`
......@@ -111,11 +110,9 @@ Within INI files, this may be specified using the alternate format :samp:`{hash}
to have a rounds value random chosen from the range :samp:`{default_rounds} +/- {vary_rounds}`.
This may be specified as an integer value, or as a string containing an integer
with a percent suffix (eg: ``"10%"``). if specified as a percent,
with a percent suffix (eg: ``"10%"``). If specified as a percent,
the amount varied will be calculated as a percentage of the :samp:`{default_rounds}` value.
The default Passlib policy sets this to ``"10%"``.
.. note::
If this is specified as a percentage, and the hash algorithm
......@@ -172,6 +169,11 @@ Within INI files, this may be specified using the alternate format :samp:`{hash}
It is recommended to set this for all hashes via ``all__passprep``,
instead of settings it per algorithm.
.. note::