Simplified caching of password hashers.
load_hashers cached its result regardless of its password_hashers argument which required fragile cache invalidation. Remove that argument in favor of @override_settings and triggering cache invalidation with a signal.
This commit is contained in:
parent
2331650835
commit
dca33ac15d
|
@ -13,22 +13,13 @@ from django.utils.encoding import force_bytes, force_str, force_text
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.crypto import (
|
from django.utils.crypto import (
|
||||||
pbkdf2, constant_time_compare, get_random_string)
|
pbkdf2, constant_time_compare, get_random_string)
|
||||||
|
from django.utils import lru_cache
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import ugettext_noop as _
|
from django.utils.translation import ugettext_noop as _
|
||||||
|
|
||||||
|
|
||||||
UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
|
UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
|
||||||
UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
|
UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
|
||||||
HASHERS = None # lazily loaded from PASSWORD_HASHERS
|
|
||||||
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(setting_changed)
|
|
||||||
def reset_hashers(**kwargs):
|
|
||||||
if kwargs['setting'] == 'PASSWORD_HASHERS':
|
|
||||||
global HASHERS, PREFERRED_HASHER
|
|
||||||
HASHERS = None
|
|
||||||
PREFERRED_HASHER = None
|
|
||||||
|
|
||||||
|
|
||||||
def is_password_usable(encoded):
|
def is_password_usable(encoded):
|
||||||
|
@ -85,20 +76,29 @@ def make_password(password, salt=None, hasher='default'):
|
||||||
return hasher.encode(password, salt)
|
return hasher.encode(password, salt)
|
||||||
|
|
||||||
|
|
||||||
def load_hashers(password_hashers=None):
|
@lru_cache.lru_cache()
|
||||||
global HASHERS
|
def get_hashers():
|
||||||
global PREFERRED_HASHER
|
|
||||||
hashers = []
|
hashers = []
|
||||||
if not password_hashers:
|
for hasher_path in settings.PASSWORD_HASHERS:
|
||||||
password_hashers = settings.PASSWORD_HASHERS
|
hasher_cls = import_string(hasher_path)
|
||||||
for backend in password_hashers:
|
hasher = hasher_cls()
|
||||||
hasher = import_string(backend)()
|
|
||||||
if not getattr(hasher, 'algorithm'):
|
if not getattr(hasher, 'algorithm'):
|
||||||
raise ImproperlyConfigured("hasher doesn't specify an "
|
raise ImproperlyConfigured("hasher doesn't specify an "
|
||||||
"algorithm name: %s" % backend)
|
"algorithm name: %s" % hasher_path)
|
||||||
hashers.append(hasher)
|
hashers.append(hasher)
|
||||||
HASHERS = dict((hasher.algorithm, hasher) for hasher in hashers)
|
return hashers
|
||||||
PREFERRED_HASHER = hashers[0]
|
|
||||||
|
|
||||||
|
@lru_cache.lru_cache()
|
||||||
|
def get_hashers_by_algorithm():
|
||||||
|
return {hasher.algorithm: hasher for hasher in get_hashers()}
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(setting_changed)
|
||||||
|
def reset_hashers(**kwargs):
|
||||||
|
if kwargs['setting'] == 'PASSWORD_HASHERS':
|
||||||
|
get_hashers.cache_clear()
|
||||||
|
get_hashers_by_algorithm.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
def get_hasher(algorithm='default'):
|
def get_hasher(algorithm='default'):
|
||||||
|
@ -113,17 +113,16 @@ def get_hasher(algorithm='default'):
|
||||||
return algorithm
|
return algorithm
|
||||||
|
|
||||||
elif algorithm == 'default':
|
elif algorithm == 'default':
|
||||||
if PREFERRED_HASHER is None:
|
return get_hashers()[0]
|
||||||
load_hashers()
|
|
||||||
return PREFERRED_HASHER
|
|
||||||
else:
|
else:
|
||||||
if HASHERS is None:
|
hashers = get_hashers_by_algorithm()
|
||||||
load_hashers()
|
try:
|
||||||
if algorithm not in HASHERS:
|
return hashers[algorithm]
|
||||||
|
except KeyError:
|
||||||
raise ValueError("Unknown password hashing algorithm '%s'. "
|
raise ValueError("Unknown password hashing algorithm '%s'. "
|
||||||
"Did you specify it in the PASSWORD_HASHERS "
|
"Did you specify it in the PASSWORD_HASHERS "
|
||||||
"setting?" % algorithm)
|
"setting?" % algorithm)
|
||||||
return HASHERS[algorithm]
|
|
||||||
|
|
||||||
|
|
||||||
def identify_hasher(encoded):
|
def identify_hasher(encoded):
|
||||||
|
|
|
@ -3,11 +3,12 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
||||||
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
|
from django.conf.global_settings import PASSWORD_HASHERS
|
||||||
from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
|
from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
|
||||||
check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher,
|
check_password, make_password, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher,
|
||||||
get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
|
get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,11 +27,9 @@ class PBKDF2SingleIterationHasher(PBKDF2PasswordHasher):
|
||||||
iterations = 1
|
iterations = 1
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
|
||||||
class TestUtilsHashPass(SimpleTestCase):
|
class TestUtilsHashPass(SimpleTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
load_hashers(password_hashers=default_hashers)
|
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
encoded = make_password('lètmein')
|
encoded = make_password('lètmein')
|
||||||
self.assertTrue(encoded.startswith('pbkdf2_sha256$'))
|
self.assertTrue(encoded.startswith('pbkdf2_sha256$'))
|
||||||
|
@ -253,8 +252,8 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertFalse(state['upgraded'])
|
self.assertFalse(state['upgraded'])
|
||||||
|
|
||||||
def test_pbkdf2_upgrade(self):
|
def test_pbkdf2_upgrade(self):
|
||||||
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
|
||||||
hasher = get_hasher('default')
|
hasher = get_hasher('default')
|
||||||
|
self.assertEqual('pbkdf2_sha256', hasher.algorithm)
|
||||||
self.assertNotEqual(hasher.iterations, 1)
|
self.assertNotEqual(hasher.iterations, 1)
|
||||||
|
|
||||||
old_iterations = hasher.iterations
|
old_iterations = hasher.iterations
|
||||||
|
@ -284,8 +283,8 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
hasher.iterations = old_iterations
|
hasher.iterations = old_iterations
|
||||||
|
|
||||||
def test_pbkdf2_upgrade_new_hasher(self):
|
def test_pbkdf2_upgrade_new_hasher(self):
|
||||||
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
|
||||||
hasher = get_hasher('default')
|
hasher = get_hasher('default')
|
||||||
|
self.assertEqual('pbkdf2_sha256', hasher.algorithm)
|
||||||
self.assertNotEqual(hasher.iterations, 1)
|
self.assertNotEqual(hasher.iterations, 1)
|
||||||
|
|
||||||
state = {'upgraded': False}
|
state = {'upgraded': False}
|
||||||
|
|
Loading…
Reference in New Issue