diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-06-30 22:11:37 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-06-30 22:11:37 -0400 |
| commit | fa2099ff2b1e0adb6b0d3b262d51d3d3f4cba364 (patch) | |
| tree | 33aab72f77e149ec9fb1711ed09d616c08f3fd0a /passlib/ext/django | |
| parent | 27c42673c2af9edf844907d4209cc53492f3c30a (diff) | |
| download | passlib-fa2099ff2b1e0adb6b0d3b262d51d3d3f4cba364.tar.gz | |
django work
* django hashes cleaned up, UTs added
* added passlib.apps.django_context for reading existing django hashes
* added experimental django plugin "passlib.ext.django" which monkeypatches django to use pbkdf2_sha256 (and many other features)
* not listing in changelog or documenting just yet, needs more testing
Diffstat (limited to 'passlib/ext/django')
| -rw-r--r-- | passlib/ext/django/__init__.py | 10 | ||||
| -rw-r--r-- | passlib/ext/django/models.py | 125 | ||||
| -rw-r--r-- | passlib/ext/django/utils.py | 125 |
3 files changed, 260 insertions, 0 deletions
diff --git a/passlib/ext/django/__init__.py b/passlib/ext/django/__init__.py new file mode 100644 index 0000000..1545e9c --- /dev/null +++ b/passlib/ext/django/__init__.py @@ -0,0 +1,10 @@ +"""passlib.ext.django - Django app to monkeypatch better password hashing into django + +.. warning:: + + This code is experimental and subject to change, + and not officially documented in Passlib just yet + (though it should work). + +see ``models`` submodule for details on how this app works. +""" diff --git a/passlib/ext/django/models.py b/passlib/ext/django/models.py new file mode 100644 index 0000000..02b54cc --- /dev/null +++ b/passlib/ext/django/models.py @@ -0,0 +1,125 @@ +"""passlib.ext.django.models + +.. warning:: + + This code is experimental and subject to change, + and not officially documented in Passlib just yet + (though it should work). + +When this is imported on Django load, +it automatically monkeypatches +:class:`django.contrib.auth.models.User` +to use a Passlib CryptContext instance in place of normal Django +password authentication. This provides hash migration, +ability to set stronger policies for superuser & staff passwords, +and stronger password hashing schemes. + +You can set the following options in django ``settings.py``: + +``PASSLIB_CONTEXT`` + This may be one of a number of values: + + * The string ``"passlib-default"``, which will cause Passlib + to replace Django's hash routines with a builtin policy + that supports all existing django hashes; but as users + log in, upgrades them all to :class:`~passlib.hash.pbkdf2_sha256`. + It also supports stronger hashing for the superuser account. + + This is the default behavior if ``PASSLIB_CONTEXT`` is not set. + + The exact policy can be found at + :data:`passlib.ext.django.models.passlib_default_ctx`. + + * ``None``, in which case this app will do nothing when django is loaded. + + * A :class:`~passlib.context.CryptContext` + instance which will be used in place of the normal Django password + hash routines. + + It is *strongly* recommended to use a context which will support + the existing Django hashes. + + * A multiline config string suitable for passing to + :meth:`passlib.context.CryptPolicy.from_string`. + This will be parsed and used much like a :class:`!CryptContext` instance. + +``PASSLIB_GET_CATEGORY`` + + By default, Passlib will invoke the specified context with a category + string that's dependant on the User instance. + superusers will be assigned to the ``superuser`` category, + staff to the ``staff`` category, and all other accounts + assigned to ``None``. + + This allows overriding that logic by specifying an alternate + function of the format ``get_category(user) -> category|None``. + + .. seealso:: + + See :ref:`user-categories` for more details about + the category system in Passlib. +""" +#=================================================================== +#imports +#=================================================================== +#site +from django.conf import settings +#pkg +from passlib.context import CryptContext, CryptPolicy +from passlib.utils import is_crypt_context, bytes +from passlib.ext.django.utils import get_category, set_django_password_context + +#=================================================================== +#constants +#=================================================================== + +#: default context used by app +passlib_default_ctx = """ +[passlib] +schemes = + pbkdf2_sha256, + django_salted_sha1, django_salted_md5, + django_des_crypt, hex_md5, + django_disabled + +default = pbkdf2_sha256 + +deprecated = + django_salted_sha1, django_salted_md5, + django_des_crypt, hex_md5 + +all__vary_rounds = 5%% + +pbkdf2_sha256__default_rounds = 4000 +staff__pbkdf2_sha256__default_rounds = 8000 +superuser__pbkdf2_sha256__default_rounds = 10000 +""" + +#=================================================================== +#main +#=================================================================== +def patch(): + #get config + ctx = getattr(settings, "PASSLIB_CONTEXT", "passlib-default") + catfunc = getattr(settings, "PASSLIB_GET_CATEGORY", get_category) + + #parse & validate input value + if not ctx: + return + if ctx == "passlib-default": + ctx = passlib_default_ctx + if isinstance(ctx, (unicode, bytes)): + ctx = CryptPolicy.from_string(ctx) + if isinstance(ctx, CryptPolicy): + ctx = CryptContext(policy=ctx) + if not is_crypt_context(ctx): + raise TypeError("django settings.PASSLIB_CONTEXT must be CryptContext instance or config string: %r" % (ctx,)) + + #monkeypatch django.contrib.auth.models:User + set_django_password_context(ctx, get_category=catfunc) + +patch() + +#=================================================================== +#eof +#=================================================================== diff --git a/passlib/ext/django/utils.py b/passlib/ext/django/utils.py new file mode 100644 index 0000000..9285f16 --- /dev/null +++ b/passlib/ext/django/utils.py @@ -0,0 +1,125 @@ +"""passlib.ext.django.utils - helper functions for patching Django hashing + +.. warning:: + + This code is experimental and subject to change, + and not officially documented in Passlib just yet + (though it should work). +""" +#=================================================================== +#imports +#=================================================================== +#site +from django.contrib.auth.models import User +#pkg +from passlib.utils import is_crypt_context, bytes +#local +__all__ = [ + "get_category", + "set_django_password_context", +] + +#=================================================================== +#monkeypatch framework +#=================================================================== + +# NOTE: this moneypatcher was written to be useful +# outside of this module, and re-invokable, +# which is why it tries so hard to maintain +# sanity about it's patch state. + +_django_patch_state = None + +def get_category(user): + "default get_category() implementation used by set_django_password_context" + if user.is_superuser: + return "superuser" + if user.is_staff: + return "staff" + return None + +def um(func): + "unwrap method (eg User.set_password -> orig func)" + return func.im_func + +def set_django_password_context(context=None, get_category=get_category): + """monkeypatches django.contrib.auth to use specified password context + + :arg context: + Passlib context to use for Django password hashing. + If ``None``, restores original django functions. + + In order to support existing hashes, + any context specified should include + all the hashes in :data:`django_context` + in addition to custom hashes. + + :param get_category: + Optional function to use when mapping Django user -> + CryptContext category. + + If a function, should have syntax ``catfunc(user) -> category|None``. + If ``None``, no function is used. + + By default, uses a function which returns ``"superuser"`` + for superusers, and ``"staff"`` for staff. + """ + global _django_patch_state + state = _django_patch_state + + # issue warning if something else monkeypatched User + # while our patch was applied. + if state is not None: + if um(User.set_password) is not state['set_password']: + warning("another library has patched django's User.set_password") + if um(User.check_password) is not state['check_password']: + warning("another library has patched django's User.check_password") + + #check if we should just restore original state + if context is None: + if state is not None: + User.pwd_context = None + User.set_password = state['orig_set_password'] + User.check_password = state['orig_check_password'] + _django_patch_state = None + return + + if not is_crypt_context(context): + raise TypeError("context must be CryptContext instance or None: %r" % + (type(context),)) + + #backup original state if this is first call + if state is None: + _django_patch_state = state = dict( + orig_check_password = um(User.check_password), + orig_set_password = um(User.set_password), + ) + + #prepare replacements + def set_password(user, raw_password): + "passlib replacement for User.set_password()" + if raw_password is None: + user.set_unusable_password() + else: + cat = get_category(user) if get_category else None + user.password = context.encrypt(raw_password, category=cat) + + def check_password(user, raw_password): + "passlib replacement for User.check_password()" + hash = user.password + cat = get_category(user) if get_category else None + ok, new_hash = context.verify_and_update(raw_password, hash, + category=cat) + if ok and new_hash: + user.password = new_hash + user.save() + return ok + + #set new state + User.pwd_context = context #just to make it easy to get to. + User.set_password = state['set_password'] = set_password + User.check_password = state['check_password'] = check_password + +#=================================================================== +#eof +#=================================================================== |
