summaryrefslogtreecommitdiff
path: root/passlib/ext/django
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-06-30 22:11:37 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-06-30 22:11:37 -0400
commitfa2099ff2b1e0adb6b0d3b262d51d3d3f4cba364 (patch)
tree33aab72f77e149ec9fb1711ed09d616c08f3fd0a /passlib/ext/django
parent27c42673c2af9edf844907d4209cc53492f3c30a (diff)
downloadpasslib-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__.py10
-rw-r--r--passlib/ext/django/models.py125
-rw-r--r--passlib/ext/django/utils.py125
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
+#===================================================================