mirror of
https://github.com/GAM-team/GAM.git
synced 2025-07-08 13:43:35 +00:00
add passlib for sha-512 salted hash generation
This commit is contained in:
323
passlib/ext/django/models.py
Normal file
323
passlib/ext/django/models.py
Normal file
@ -0,0 +1,323 @@
|
||||
"""passlib.ext.django.models -- monkeypatch django hashing framework"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
from django import VERSION
|
||||
from django.conf import settings
|
||||
# pkg
|
||||
from passlib.context import CryptContext
|
||||
from passlib.exc import ExpectedTypeError
|
||||
from passlib.ext.django.utils import _PatchManager, hasher_to_passlib_name, \
|
||||
get_passlib_hasher, get_preset_config
|
||||
from passlib.utils.compat import callable, unicode, bytes
|
||||
# local
|
||||
__all__ = ["password_context"]
|
||||
|
||||
#=============================================================================
|
||||
# global attrs
|
||||
#=============================================================================
|
||||
|
||||
# the context object which this patches contrib.auth to use for password hashing.
|
||||
# configuration controlled by ``settings.PASSLIB_CONFIG``.
|
||||
password_context = CryptContext()
|
||||
|
||||
# function mapping User objects -> passlib user category.
|
||||
# may be overridden via ``settings.PASSLIB_GET_CATEGORY``.
|
||||
def _get_category(user):
|
||||
"""default get_category() implementation"""
|
||||
if user.is_superuser:
|
||||
return "superuser"
|
||||
elif user.is_staff:
|
||||
return "staff"
|
||||
else:
|
||||
return None
|
||||
|
||||
# object used to track state of patches applied to django.
|
||||
_manager = _PatchManager(log=logging.getLogger(__name__ + "._manager"))
|
||||
|
||||
# patch status
|
||||
_patched = False
|
||||
|
||||
#=============================================================================
|
||||
# applying & removing the patches
|
||||
#=============================================================================
|
||||
def _apply_patch():
|
||||
"""monkeypatch django's password handling to use ``passlib_context``,
|
||||
assumes the caller will configure the object.
|
||||
"""
|
||||
#
|
||||
# setup constants
|
||||
#
|
||||
log.debug("preparing to monkeypatch 'django.contrib.auth' ...")
|
||||
global _patched
|
||||
assert not _patched, "monkeypatching already applied"
|
||||
HASHERS_PATH = "django.contrib.auth.hashers"
|
||||
MODELS_PATH = "django.contrib.auth.models"
|
||||
USER_PATH = MODELS_PATH + ":User"
|
||||
FORMS_PATH = "django.contrib.auth.forms"
|
||||
|
||||
#
|
||||
# import UNUSUABLE_PASSWORD and is_password_usuable() helpers
|
||||
# (providing stubs for older django versions)
|
||||
#
|
||||
if VERSION < (1,4):
|
||||
has_hashers = False
|
||||
if VERSION < (1,0):
|
||||
UNUSABLE_PASSWORD = "!"
|
||||
else:
|
||||
from django.contrib.auth.models import UNUSABLE_PASSWORD
|
||||
|
||||
def is_password_usable(encoded):
|
||||
return encoded is not None and encoded != UNUSABLE_PASSWORD
|
||||
|
||||
def is_valid_secret(secret):
|
||||
return secret is not None
|
||||
|
||||
elif VERSION < (1,6):
|
||||
has_hashers = True
|
||||
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \
|
||||
is_password_usable
|
||||
|
||||
# NOTE: 1.4 - 1.5 - empty passwords no longer valid.
|
||||
def is_valid_secret(secret):
|
||||
return bool(secret)
|
||||
|
||||
else:
|
||||
has_hashers = True
|
||||
from django.contrib.auth.hashers import is_password_usable
|
||||
|
||||
# 1.6 - empty passwords valid again
|
||||
def is_valid_secret(secret):
|
||||
return secret is not None
|
||||
|
||||
if VERSION < (1,6):
|
||||
def make_unusable_password():
|
||||
return UNUSABLE_PASSWORD
|
||||
else:
|
||||
from django.contrib.auth.hashers import make_password as _make_password
|
||||
def make_unusable_password():
|
||||
return _make_password(None)
|
||||
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes
|
||||
has_unsalted_sha1 = (VERSION >= (1,4,6))
|
||||
|
||||
#
|
||||
# backport ``User.set_unusable_password()`` for Django 0.9
|
||||
# (simplifies rest of the code)
|
||||
#
|
||||
if not hasattr(_manager.getorig(USER_PATH), "set_unusable_password"):
|
||||
assert VERSION < (1,0)
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def set_unusable_password(user):
|
||||
user.password = make_unusable_password()
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def has_usable_password(user):
|
||||
return is_password_usable(user.password)
|
||||
|
||||
#
|
||||
# patch ``User.set_password() & ``User.check_password()`` to use
|
||||
# context & get_category (would just leave these as wrappers for hashers
|
||||
# module under django 1.4, but then we couldn't pass User object into
|
||||
# get_category very easily)
|
||||
#
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def set_password(user, password):
|
||||
"passlib replacement for User.set_password()"
|
||||
if is_valid_secret(password):
|
||||
# NOTE: pulls _get_category from module globals
|
||||
cat = _get_category(user)
|
||||
user.password = password_context.encrypt(password, category=cat)
|
||||
else:
|
||||
user.set_unusable_password()
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def check_password(user, password):
|
||||
"passlib replacement for User.check_password()"
|
||||
hash = user.password
|
||||
if not is_valid_secret(password) or not is_password_usable(hash):
|
||||
return False
|
||||
if not hash and VERSION < (1,4):
|
||||
return False
|
||||
# NOTE: pulls _get_category from module globals
|
||||
cat = _get_category(user)
|
||||
ok, new_hash = password_context.verify_and_update(password, hash,
|
||||
category=cat)
|
||||
if ok and new_hash is not None:
|
||||
# migrate to new hash if needed.
|
||||
user.password = new_hash
|
||||
user.save()
|
||||
return ok
|
||||
|
||||
#
|
||||
# override check_password() with our own implementation
|
||||
#
|
||||
@_manager.monkeypatch(HASHERS_PATH, enable=has_hashers)
|
||||
@_manager.monkeypatch(MODELS_PATH)
|
||||
def check_password(password, encoded, setter=None, preferred="default"):
|
||||
"passlib replacement for check_password()"
|
||||
# XXX: this currently ignores "preferred" keyword, since it's purpose
|
||||
# was for hash migration, and that's handled by the context.
|
||||
if not is_valid_secret(password) or not is_password_usable(encoded):
|
||||
return False
|
||||
ok = password_context.verify(password, encoded)
|
||||
if ok and setter and password_context.needs_update(encoded):
|
||||
setter(password)
|
||||
return ok
|
||||
|
||||
#
|
||||
# patch the other functions defined in the ``hashers`` module, as well
|
||||
# as any other known locations where they're imported within ``contrib.auth``
|
||||
#
|
||||
if has_hashers:
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(MODELS_PATH)
|
||||
def make_password(password, salt=None, hasher="default"):
|
||||
"passlib replacement for make_password()"
|
||||
if not is_valid_secret(password):
|
||||
return make_unusable_password()
|
||||
if hasher == "default":
|
||||
scheme = None
|
||||
else:
|
||||
scheme = hasher_to_passlib_name(hasher)
|
||||
kwds = dict(scheme=scheme)
|
||||
handler = password_context.handler(scheme)
|
||||
# NOTE: django make specify an empty string for the salt,
|
||||
# even if scheme doesn't accept a salt. we omit keyword
|
||||
# in that case.
|
||||
if salt is not None and (salt or 'salt' in handler.setting_kwds):
|
||||
kwds['salt'] = salt
|
||||
return password_context.encrypt(password, **kwds)
|
||||
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(FORMS_PATH)
|
||||
def get_hasher(algorithm="default"):
|
||||
"passlib replacement for get_hasher()"
|
||||
if algorithm == "default":
|
||||
scheme = None
|
||||
else:
|
||||
scheme = hasher_to_passlib_name(algorithm)
|
||||
# NOTE: resolving scheme -> handler instead of
|
||||
# passing scheme into get_passlib_hasher(),
|
||||
# in case context contains custom handler
|
||||
# shadowing name of a builtin handler.
|
||||
handler = password_context.handler(scheme)
|
||||
return get_passlib_hasher(handler, algorithm=algorithm)
|
||||
|
||||
# identify_hasher() was added in django 1.5,
|
||||
# patching it anyways for 1.4, so passlib's version is always available.
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(FORMS_PATH)
|
||||
def identify_hasher(encoded):
|
||||
"passlib helper to identify hasher from encoded password"
|
||||
handler = password_context.identify(encoded, resolve=True,
|
||||
required=True)
|
||||
algorithm = None
|
||||
if (has_unsalted_sha1 and handler.name == "django_salted_sha1" and
|
||||
encoded.startswith("sha1$$")):
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
|
||||
# but passlib just reuses the "sha1$salt$digest" handler.
|
||||
# we want to resolve to correct django hasher.
|
||||
algorithm = "unsalted_sha1"
|
||||
return get_passlib_hasher(handler, algorithm=algorithm)
|
||||
|
||||
_patched = True
|
||||
log.debug("... finished monkeypatching django")
|
||||
|
||||
def _remove_patch():
|
||||
"""undo the django monkeypatching done by this module.
|
||||
offered as a last resort if it's ever needed.
|
||||
|
||||
.. warning::
|
||||
This may cause problems if any other Django modules have imported
|
||||
their own copies of the patched functions, though the patched
|
||||
code has been designed to throw an error as soon as possible in
|
||||
this case.
|
||||
"""
|
||||
global _patched
|
||||
if _patched:
|
||||
log.debug("removing django monkeypatching...")
|
||||
_manager.unpatch_all(unpatch_conflicts=True)
|
||||
password_context.load({})
|
||||
_patched = False
|
||||
log.debug("...finished removing django monkeypatching")
|
||||
return True
|
||||
if _manager: # pragma: no cover -- sanity check
|
||||
log.warning("reverting partial monkeypatching of django...")
|
||||
_manager.unpatch_all()
|
||||
password_context.load({})
|
||||
log.debug("...finished removing django monkeypatching")
|
||||
return True
|
||||
log.debug("django not monkeypatched")
|
||||
return False
|
||||
|
||||
#=============================================================================
|
||||
# main code
|
||||
#=============================================================================
|
||||
def _load():
|
||||
global _get_category
|
||||
|
||||
# TODO: would like to add support for inheriting config from a preset
|
||||
# (or from existing hasher state) and letting PASSLIB_CONFIG
|
||||
# be an update, not a replacement.
|
||||
|
||||
# TODO: wrap and import any custom hashers as passlib handlers,
|
||||
# so they could be used in the passlib config.
|
||||
|
||||
# load config from settings
|
||||
_UNSET = object()
|
||||
config = getattr(settings, "PASSLIB_CONFIG", _UNSET)
|
||||
if config is _UNSET:
|
||||
# XXX: should probably deprecate this alias
|
||||
config = getattr(settings, "PASSLIB_CONTEXT", _UNSET)
|
||||
if config is _UNSET:
|
||||
config = "passlib-default"
|
||||
if config is None:
|
||||
warn("setting PASSLIB_CONFIG=None is deprecated, "
|
||||
"and support will be removed in Passlib 1.8, "
|
||||
"use PASSLIB_CONFIG='disabled' instead.",
|
||||
DeprecationWarning)
|
||||
config = "disabled"
|
||||
elif not isinstance(config, (unicode, bytes, dict)):
|
||||
raise ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG")
|
||||
|
||||
# load custom category func (if any)
|
||||
get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None)
|
||||
if get_category and not callable(get_category):
|
||||
raise ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY")
|
||||
|
||||
# check if we've been disabled
|
||||
if config == "disabled":
|
||||
if _patched: # pragma: no cover -- sanity check
|
||||
log.error("didn't expect monkeypatching would be applied!")
|
||||
_remove_patch()
|
||||
return
|
||||
|
||||
# resolve any preset aliases
|
||||
if isinstance(config, str) and '\n' not in config:
|
||||
config = get_preset_config(config)
|
||||
|
||||
# setup context
|
||||
_apply_patch()
|
||||
password_context.load(config)
|
||||
if get_category:
|
||||
# NOTE: _get_category is module global which is read by
|
||||
# monkeypatched functions constructed by _apply_patch()
|
||||
_get_category = get_category
|
||||
log.debug("passlib.ext.django loaded")
|
||||
|
||||
# wrap load function so we can undo any patching if something goes wrong
|
||||
try:
|
||||
_load()
|
||||
except:
|
||||
_remove_patch()
|
||||
raise
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
Reference in New Issue
Block a user