diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-03-21 19:54:55 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-03-21 19:54:55 -0400 |
| commit | f6c3fb3c0878cc74a4467828418ffb3ccd0082b2 (patch) | |
| tree | 855db1dae542695bd3a57c9a43860467a3ebfe77 /passlib | |
| parent | a8bd4647cabe847ee728944428962e947c6df7bd (diff) | |
| download | passlib-f6c3fb3c0878cc74a4467828418ffb3ccd0082b2.tar.gz | |
cleaned up registry code, added more UTs.
Diffstat (limited to 'passlib')
| -rw-r--r-- | passlib/base.py | 194 | ||||
| -rw-r--r-- | passlib/hash.py | 9 | ||||
| -rw-r--r-- | passlib/tests/_test_bad_register.py | 14 | ||||
| -rw-r--r-- | passlib/tests/test_base.py | 161 | ||||
| -rw-r--r-- | passlib/tests/test_utils.py | 13 | ||||
| -rw-r--r-- | passlib/unix.py | 2 | ||||
| -rw-r--r-- | passlib/utils/__init__.py | 21 |
7 files changed, 310 insertions, 104 deletions
diff --git a/passlib/base.py b/passlib/base.py index 0284181..465bb21 100644 --- a/passlib/base.py +++ b/passlib/base.py @@ -27,16 +27,15 @@ from warnings import warn #site from pkg_resources import resource_string #libs -##import passlib.drivers.as _hmod from passlib.utils import Undef, is_crypt_handler, splitcomma, rng #pkg #local __all__ = [ - #global registry - 'register_crypt_handler', - 'register_crypt_location', - 'get_crypt_handler', - 'list_crypt_handlers' + #registry interface + "register_crypt_handler_path", + "register_crypt_handler", + "get_crypt_handler", + "list_crypt_handlers", #contexts 'CryptPolicy', @@ -44,9 +43,9 @@ __all__ = [ ] #========================================================= -#proxy object +#registry proxy object #========================================================= -class PasslibHashProxy(object): +class PasslibRegistryProxy(object): """proxy module passlib.hash this module is in fact an object which lazy-loads @@ -58,35 +57,45 @@ class PasslibHashProxy(object): def __getattr__(self, attr): if attr.startswith("_"): - raise AttributeError, "unknown attribute: %r" % (attr,) + raise AttributeError, "missing attribute: %r" % (attr,) handler = get_crypt_handler(attr, None) - if handler is None: + if handler: + return handler + else: raise AttributeError, "unknown password hash: %r" % (attr,) - setattr(self, attr, handler) - return handler + + def __setattr__(self, attr, value): + register_crypt_handler(value, name=attr) def __repr__(self): return "<proxy module 'passlib.hash'>" def __dir__(self): - "report list of all actual attrs, PLUS all known algorithms that haven't been loaded" + #add in handlers that will be lazy-loaded, + #otherwise this is std dir implementation attrs = set(dir(self.__class__)) attrs.update(self.__dict__) - attrs.update(_driver_locations) + attrs.update(_handler_locations) return sorted(attrs) -#NOTE: this is inserted into sys.modules by passlib/__init__.py -_hashmod = PasslibHashProxy() + #========================================================= + #eoc + #========================================================= -#========================================================= -#global registry -#========================================================= +#singleton instance +_proxy = PasslibRegistryProxy() -#: dict mapping hash names -> loaded driver objects (uses passlib.hash.__dict__ so the two will always be in sync) -_drivers = _hashmod.__dict__ +#========================================================== +#internal registry state +#========================================================== -#: dict mapping hash names -> (module name, None | class name) that should be location of driver -_driver_locations = { +#: dict mapping name -> handler for all loaded handlers. uses proxy's dict so they stay in sync. +_handlers = _proxy.__dict__ + +#: dict mapping name -> (module path, attribute) for lazy-loading of handlers +_handler_locations = { + #NOTE: this is a hardcoded list of the handlers built into passlib, + #applications should call register_crypt_handler_location() to add their own "apr_md5_crypt": ("passlib.drivers.md5_crypt", "apr_md5_crypt"), "bcrypt": ("passlib.drivers.bcrypt", "bcrypt"), "bigcrypt": ("passlib.drivers.des_crypt", "bigcrypt"), @@ -118,54 +127,103 @@ _driver_locations = { "unix_fallback": ("passlib.drivers.misc", "unix_fallback"), } -def register_crypt_location(name, path): - "register location to lazy-load driver when requested" - global _driver_locations +#: master regexp for detecting valid handler names +_name_re = re.compile("^[a-z][_a-z0-9]{2,}$") + +#========================================================== +#registry frontend functions +#========================================================== +def register_crypt_handler_path(name, path): + """register location to lazy-load handler when requested. + + custom hashes may be registered via :func:`register_crypt_handler`, + or they may be registered by this function, + which will delay actually importing and loading the handler + until a call to :func:`get_crypt_handler` is made for the specified name. + + :arg name: name of handler + :arg path: module import path + + the specified module path should contain a password hash handler + called :samp:`{name}`, or the path may contain a semicolon, + specifying the module and module attribute to use. + """ + global _handler_locations if ':' in path: modname, modattr = path.split(":") else: - modname = path - modattr = None - _driver_locations[name] = (modname, modattr) + modname, modattr = path, name + _handler_locations[name] = (modname, modattr) + +def register_crypt_handler(handler, force=False, name=None): + """register password hash handler. -def register_crypt_handler(obj, force=False): - "register CryptHandler handler" - global _drivers + this method registers a handler with the internal passlib registry, + so that it will be returned by :func:`get_crypt_handler` when requested. - #validate obj - if not is_crypt_handler(obj): - raise TypeError, "object does not appear to be a CryptHandler: %r" % (obj,) - assert obj, "CryptHandlers must be boolean True: %r" % (obj,) + :arg handler: the password hash handler to register + :param force: force override of existing handler (defaults to False) + + :raises KeyError: + if a (different) handler was already registered with + the same name, and ``force=True`` was not specified. + """ + global _handlers, _name_re + + #validate handler + if not is_crypt_handler(handler): + raise TypeError, "object does not appear to be a crypt handler: %r" % (handler,) + assert handler, "crypt handlers must be boolean True: %r" % (handler,) + + #if name specified, make sure it matched + #(this is mainly used as a check to help __setattr__) + if name: + if name != handler.name: + raise ValueError, "handlers must be stored only under their own name" + else: + name = handler.name #validate name - name = obj.name if not name: raise ValueError, "name is null: %r" % (name,) if name.lower() != name: raise ValueError, "name must be lower-case: %r" % (name,) - if re.search("[^_a-z0-9]",name): - raise ValueError, "invalid characters in name (only underscore, a-z, 0-9 allowed): %r" % (name,) + if not _name_re.match(name): + raise ValueError, "invalid characters in name (must be 3+ characters, begin with a-z, and contain only underscore, a-z, 0-9): %r" % (name,) #check for existing handler - other = _drivers.get(name) + other = _handlers.get(name) if other: - if other is obj: + if other is handler: return #already registered if force: log.warning("overriding previous handler registered to name %r: %r", name, other) else: - raise ValueError, "handler already registered for name %r: %r" % (name, other) + raise KeyError, "a handler has already registered for the name %r: %r (use force=True to override)" % (name, other) - #put handler into hash module - _drivers[name] = obj - log.info("registered crypt handler %r: %r", name, obj) + #register handler in dict + _handlers[name] = handler + log.info("registered crypt handler %r: %r", name, handler) def get_crypt_handler(name, default=Undef): - "resolve crypt algorithm name" - global _drivers, _driver_locations + """return handler for specified password hash scheme. + + this method looks up a handler for the specified scheme. + if the handler is not already loaded, + it checks if the location of one is known (:func:`register_crypt_handler`) + and loads it first. + + :arg name: name of handler to return + :param default: if specified, returns default value if no handler found. + + :raises KeyError: if no handler matching that name is found, and no default specified + + :returns: handler attached to name, or default if specified + """ + global _handlers, _handler_locations #check if handler loaded - handler = _drivers.get(name) + handler = _handlers.get(name, None) if handler: return handler @@ -176,56 +234,48 @@ def get_crypt_handler(name, default=Undef): name = alt #check if handler loaded - handler = _drivers.get(name) + handler = _handlers.get(name) if handler: return handler #check if lazy load mapping has been specified for this driver - route = _driver_locations.get(name) + route = _handler_locations.get(name) if route: modname, modattr = route #try to load the module - any import errors indicate runtime config, - # either missing packages, or bad path provided to register_crypt_location() + # either missing packages, or bad path provided to register_crypt_handler_path() mod = __import__(modname, None, None, ['dummy'], 0) #first check if importing module triggered register_crypt_handler(), - #though this is discouraged due to it's magical implicitness - handler = _drivers.get(name) + #(though this is discouraged due to it's magical implicitness) + handler = _handlers.get(name) if handler: #XXX: issue deprecation warning here? assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler) return handler - #if attribute specified, assume *that's* the handler, otherwise assume the module itself. - if modattr: - handler = getattr(mod, modattr) - else: - handler = mod - - #XXX: can this ever happen under legitimate circumstances? - if handler.name != name: - raise RuntimeError, "handler name does not match expected name: %r vs %r" % (handler.name, name) - - #run through register_crypt_handler, to validate it - register_crypt_handler(handler) - + #then get real handler & register it + handler = getattr(mod, modattr) + register_crypt_handler(handler, name=name) return handler - #TODO: check egg entry points under name "passlib.hash" - #fail! if default is Undef: raise KeyError, "no crypt handler found for algorithm: %r" % (name,) else: return default -def list_crypt_handlers(): - "return sorted list of all known crypt algorithm names" - return filter(lambda x: not x.startswith("_"), dir(_hashmod)) +def list_crypt_handlers(loaded_only=False): + "return sorted list of all known crypt handler names" + global _handlers, _handler_locations + names = set(_handlers) + if not loaded_only: + names.update(_handler_locations) + return sorted(names) #========================================================= -#policy +#crypt policy #========================================================= def _parse_policy_key(key): "helper to normalize & parse policy keys; returns ``(category, name, option)``" diff --git a/passlib/hash.py b/passlib/hash.py index d0d3520..a246996 100644 --- a/passlib/hash.py +++ b/passlib/hash.py @@ -3,7 +3,8 @@ NOTE: this module does not actually contain any hashes. this file is a stub which is replaced by a proxy object, - which lazy-loads hashes as requested by calling get_crypt_handler(). + which lazy-loads hashes as requested. + the actually implementations of hashes (at least, those built into passlib) are stored in the passlib.drivers subpackage. """ @@ -18,10 +19,10 @@ NOTE: #import proxy object, and replace this module with it. #this should cause any import commands to return that object, #not this module -from passlib.base import _hashmod as hash +from passlib.base import _proxy import sys -sys.modules['passlib.hash'] = hash -del sys, hash +sys.modules['passlib.hash'] = _proxy +del sys, _proxy #========================================================= #eoc diff --git a/passlib/tests/_test_bad_register.py b/passlib/tests/_test_bad_register.py new file mode 100644 index 0000000..8554ee5 --- /dev/null +++ b/passlib/tests/_test_bad_register.py @@ -0,0 +1,14 @@ +"helper for method in test_base.py" + +from passlib.base import register_crypt_handler +from passlib.utils.drivers import BaseHash + +class dummy_bad(BaseHash): + name = "dummy_bad" + setting_kwds = () + +class alt_dummy_bad(BaseHash): + name = "dummy_bad" + setting_kwds = () + +register_crypt_handler(alt_dummy_bad) diff --git a/passlib/tests/test_base.py b/passlib/tests/test_base.py index b3a1071..60bb9ba 100644 --- a/passlib/tests/test_base.py +++ b/passlib/tests/test_base.py @@ -9,10 +9,13 @@ from logging import getLogger import os import time import warnings +import sys #site #pkg -from passlib import base, hash -from passlib.base import CryptContext, CryptPolicy +from passlib import hash, base +from passlib.base import CryptContext, CryptPolicy, \ + register_crypt_handler, register_crypt_handler_path, \ + get_crypt_handler, list_crypt_handlers from passlib.utils.drivers import BaseHash from passlib.tests.utils import TestCase, mktemp, catch_warnings from passlib.drivers.md5_crypt import md5_crypt as AnotherHash @@ -21,25 +24,89 @@ from passlib.tests.test_utils_drivers import UnsaltedHash, SaltedHash log = getLogger(__name__) #========================================================= -#proxy +#test registry #========================================================= -class MiscTest(TestCase): +class dummy_0(BaseHash): + name = "dummy_0" + setting_kwds = () + +class alt_dummy_0(BaseHash): + name = "dummy_0" + setting_kwds = () + +dummy_x = 1 + +def unload_handler_name(name): + if hasattr(hash, name): + delattr(hash, name) + + #NOTE: this messes w/ internals of registry, shouldn't be used publically. + paths = base._handler_locations + if name in paths: + del paths[name] + +class RegistryTest(TestCase): + + case_prefix = "passlib registry" def tearDown(self): - if hasattr(hash, "dummy_1"): - del hash.dummy_1 + for name in ("dummy_0", "dummy_1", "dummy_x", "dummy_bad"): + unload_handler_name(name) def test_hash_proxy(self): + "test passlib.hash proxy object" dir(hash) repr(hash) self.assertRaises(AttributeError, getattr, hash, 'fooey') + def test_register_crypt_handler_path(self): + "test register_crypt_handler_path()" + + #NOTE: this messes w/ internals of registry, shouldn't be used publically. + paths = base._handler_locations + + #check namespace is clear + self.assertTrue('dummy_0' not in paths) + self.assertFalse(hasattr(hash, 'dummy_0')) + + #try lazy load + register_crypt_handler_path('dummy_0', 'passlib.tests.test_base') + self.assertTrue('dummy_0' in list_crypt_handlers()) + self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True)) + self.assertIs(hash.dummy_0, dummy_0) + self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True)) + unload_handler_name('dummy_0') + + #try lazy load w/ alt + register_crypt_handler_path('dummy_0', 'passlib.tests.test_base:alt_dummy_0') + self.assertIs(hash.dummy_0, alt_dummy_0) + unload_handler_name('dummy_0') + + #check lazy load w/ wrong type fails + register_crypt_handler_path('dummy_x', 'passlib.tests.test_base') + self.assertRaises(TypeError, get_crypt_handler, 'dummy_x') + + #check lazy load w/ wrong name fails + register_crypt_handler_path('alt_dummy_0', 'passlib.tests.test_base') + self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") + + #TODO: check lazy load which calls register_crypt_handler (warning should be issued) + sys.modules.pop("passlib.tests._test_bad_register", None) + register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register") + with catch_warnings(): + warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning) + h = get_crypt_handler("dummy_bad") + from passlib.tests import _test_bad_register as tbr + self.assertIs(h, tbr.alt_dummy_bad) + def test_register_crypt_handler(self): - self.assertRaises(TypeError, base.register_crypt_handler, {}) + "test register_crypt_handler()" - self.assertRaises(ValueError, base.register_crypt_handler, BaseHash) - self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="AB_CD"))) - self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="ab-cd"))) + self.assertRaises(TypeError, register_crypt_handler, {}) + + self.assertRaises(ValueError, register_crypt_handler, BaseHash) + self.assertRaises(ValueError, register_crypt_handler, type('x', (BaseHash,), dict(name="AB_CD"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (BaseHash,), dict(name="ab-cd"))) class dummy_1(BaseHash): name = "dummy_1" @@ -47,34 +114,34 @@ class MiscTest(TestCase): class dummy_1b(BaseHash): name = "dummy_1" - self.assertTrue('dummy_1' not in base.list_crypt_handlers()) + self.assertTrue('dummy_1' not in list_crypt_handlers()) - base.register_crypt_handler(dummy_1) - base.register_crypt_handler(dummy_1) - self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + register_crypt_handler(dummy_1) + register_crypt_handler(dummy_1) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) - self.assertRaises(ValueError, base.register_crypt_handler, dummy_1b) - self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + self.assertRaises(KeyError, register_crypt_handler, dummy_1b) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) - base.register_crypt_handler(dummy_1b, force=True) - self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1b) + register_crypt_handler(dummy_1b, force=True) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1b) - self.assertTrue('dummy_1' in base.list_crypt_handlers()) + self.assertTrue('dummy_1' in list_crypt_handlers()) def test_get_crypt_handler(self): + "test get_crypt_handler()" class dummy_1(BaseHash): name = "dummy_1" - self.assertRaises(KeyError, base.get_crypt_handler, "dummy_1") - self.assertIs(base.get_crypt_handler("dummy_1", None), None) + self.assertRaises(KeyError, get_crypt_handler, "dummy_1") - base.register_crypt_handler(dummy_1) - self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + register_crypt_handler(dummy_1) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with catch_warnings(): warnings.filterwarnings("ignore", "handler names be lower-case, and use underscores instead of hyphens:.*", UserWarning) - self.assertIs(base.get_crypt_handler("DUMMY-1"), dummy_1) + self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) #========================================================= # @@ -183,6 +250,26 @@ sha512_crypt.min_rounds = 45000 sha512_crypt__min_rounds=45000, ) + #----------------------------------------------------- + #sample 4 - category specific + #----------------------------------------------------- + sample_config_4s = """ +[passlib] +schemes = sha512_crypt +all.vary_rounds = 10% +default.sha512_crypt.max_rounds = 20000 +admin.all.vary_rounds = 5% +admin.sha512_crypt.max_rounds = 40000 +""" + + sample_config_4pd = dict( + schemes = [ "sha512_crypt" ], + all__vary_rounds = "10%", + sha512_crypt__max_rounds = 20000, + admin__all__vary_rounds = "5%", + admin__sha512_crypt__max_rounds = 40000, + ) + #========================================================= #constructors #========================================================= @@ -232,6 +319,9 @@ sha512_crypt.min_rounds = 45000 policy = CryptPolicy.from_string(self.sample_config_1s) self.assertEquals(policy.to_dict(), self.sample_config_1pd) + policy = CryptPolicy.from_string(self.sample_config_4s) + self.assertEquals(policy.to_dict(), self.sample_config_4pd) + def test_03_from_source(self): "test CryptPolicy.from_source() constructor" @@ -370,10 +460,26 @@ sha512_crypt.min_rounds = 45000 max_rounds = 50000, )) + p4 = CryptPolicy.from_string(self.sample_config_4s) + self.assertEquals(p4.get_options("sha512_crypt"), dict( + vary_rounds="10%", + max_rounds=20000, + )) + + self.assertEquals(p4.get_options("sha512_crypt", "user"), dict( + vary_rounds="10%", + max_rounds=20000, + )) + + self.assertEquals(p4.get_options("sha512_crypt", "admin"), dict( + vary_rounds="5%", + max_rounds=40000, + )) + def test_14_handler_is_deprecated(self): "test handler_is_deprecated() method" pa = CryptPolicy(**self.sample_config_1pd) - pb = pa.replace(deprecated=["des_crypt", "bsdi_crypt"]) + pb = pa.replace(deprecated=["des_crypt", "bsdi_crypt"], admin__context__deprecated=["des_crypt"]) self.assert_(not pa.handler_is_deprecated("des_crypt")) self.assert_(not pa.handler_is_deprecated(hash.bsdi_crypt)) @@ -383,6 +489,11 @@ sha512_crypt.min_rounds = 45000 self.assert_(pb.handler_is_deprecated(hash.bsdi_crypt)) self.assert_(not pb.handler_is_deprecated("sha512_crypt")) + #check categories as well + self.assertTrue(pb.handler_is_deprecated("des_crypt", "user")) + self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin")) + self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "admin")) + #TODO: test this. ##def test_gen_min_verify_time(self): ## "test get_min_verify_time() method" diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py index 382500e..ad9dfaf 100644 --- a/passlib/tests/test_utils.py +++ b/passlib/tests/test_utils.py @@ -11,14 +11,25 @@ import random #module from passlib import utils from passlib.base import CryptContext -from passlib.utils import h64, des +from passlib.utils import h64, des, Undef from passlib.utils.md4 import md4 from passlib.tests.utils import TestCase, Params as ak, enable_option + #========================================================= #byte funcs #========================================================= class UtilsTest(TestCase): + def test_undef(self): + "test Undef singleton" + self.assertEqual(repr(Undef), "<Undef>") + self.assertFalse(Undef==None,) + self.assertFalse(Undef==Undef,) + self.assertFalse(Undef==True,) + self.assertTrue(Undef!=None,) + self.assertTrue(Undef!=Undef,) + self.assertTrue(Undef!=True,) + def test_list_to_bytes(self): self.assertFunctionResults(utils.list_to_bytes, [ #standard big endian diff --git a/passlib/unix.py b/passlib/unix.py index 30bf955..6942033 100644 --- a/passlib/unix.py +++ b/passlib/unix.py @@ -4,7 +4,7 @@ #imports #========================================================= #pkg -from passlib.base import CryptContext, register_crypt_handler +from passlib.base import CryptContext from passlib.utils.drivers import CryptHandler #local __all__ = [ diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index 619d492..00f27da 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -101,7 +101,26 @@ class classproperty(object): ## update_wrapper(wrapper, func) ## return classmethod(wrapper) -Undef = object() #singleton used as default kwd value in some functions +#NOTE: Undef is only used in *one* place now, could just remove it + +class UndefType(object): + _undef = None + + def __new__(cls): + if cls._undef is None: + cls._undef = object.__new__(cls) + return cls._undef + + def __repr__(self): + return '<Undef>' + + def __eq__(self, other): + return False + + def __ne__(self, other): + return True + +Undef = UndefType() #singleton used as default kwd value in some functions #========================================================== #protocol helpers |
