You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					1081 lines
				
				40 KiB
			
		
		
			
		
	
	
					1081 lines
				
				40 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								"""test passlib.ext.django"""
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# imports
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# core
							 | 
						||
| 
								 | 
							
								from __future__ import absolute_import, division, print_function
							 | 
						||
| 
								 | 
							
								import logging; log = logging.getLogger(__name__)
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								import re
							 | 
						||
| 
								 | 
							
								# site
							 | 
						||
| 
								 | 
							
								# pkg
							 | 
						||
| 
								 | 
							
								from passlib import apps as _apps, exc, registry
							 | 
						||
| 
								 | 
							
								from passlib.apps import django10_context, django14_context, django16_context
							 | 
						||
| 
								 | 
							
								from passlib.context import CryptContext
							 | 
						||
| 
								 | 
							
								from passlib.ext.django.utils import (
							 | 
						||
| 
								 | 
							
								    DJANGO_VERSION, MIN_DJANGO_VERSION, DjangoTranslator, quirks,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								from passlib.utils.compat import iteritems, get_method_function, u
							 | 
						||
| 
								 | 
							
								from passlib.utils.decor import memoized_property
							 | 
						||
| 
								 | 
							
								# tests
							 | 
						||
| 
								 | 
							
								from passlib.tests.utils import TestCase, TEST_MODE, handler_derived_from
							 | 
						||
| 
								 | 
							
								from passlib.tests.test_handlers import get_handler_case
							 | 
						||
| 
								 | 
							
								# local
							 | 
						||
| 
								 | 
							
								__all__ = [
							 | 
						||
| 
								 | 
							
								    "DjangoBehaviorTest",
							 | 
						||
| 
								 | 
							
								    "ExtensionBehaviorTest",
							 | 
						||
| 
								 | 
							
								    "DjangoExtensionTest",
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    "_ExtensionSupport",
							 | 
						||
| 
								 | 
							
								    "_ExtensionTest",
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# configure django settings for testcases
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# whether we have supported django version
							 | 
						||
| 
								 | 
							
								has_min_django = DJANGO_VERSION >= MIN_DJANGO_VERSION
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# import and configure empty django settings
							 | 
						||
| 
								 | 
							
								# NOTE: we don't want to set up entirety of django, so not using django.setup() directly.
							 | 
						||
| 
								 | 
							
								#       instead, manually configuring the settings, and setting it up w/ no apps installed.
							 | 
						||
| 
								 | 
							
								#       in future, may need to alter this so we call django.setup() after setting
							 | 
						||
| 
								 | 
							
								#       DJANGO_SETTINGS_MODULE to a custom settings module w/ a dummy django app.
							 | 
						||
| 
								 | 
							
								if has_min_django:
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # initialize django settings manually
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    from django.conf import settings, LazySettings
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not isinstance(settings, LazySettings):
							 | 
						||
| 
								 | 
							
								        # this probably means django globals have been configured already,
							 | 
						||
| 
								 | 
							
								        # which we don't want, since test cases reset and manipulate settings.
							 | 
						||
| 
								 | 
							
								        raise RuntimeError("expected django.conf.settings to be LazySettings: %r" % (settings,))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # else configure a blank settings instance for the unittests
							 | 
						||
| 
								 | 
							
								    if not settings.configured:
							 | 
						||
| 
								 | 
							
								        settings.configure()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # init django apps w/ NO installed apps.
							 | 
						||
| 
								 | 
							
								    # NOTE: required for django >= 1.9
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    from django.apps import apps
							 | 
						||
| 
								 | 
							
								    apps.populate(["django.contrib.contenttypes", "django.contrib.auth"])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# log a warning if tested w/ newer version.
							 | 
						||
| 
								 | 
							
								# NOTE: this is mainly here as place to mark what version it was run against before release.
							 | 
						||
| 
								 | 
							
								if DJANGO_VERSION >= (3, 2):
							 | 
						||
| 
								 | 
							
								    log.info("this release hasn't been tested against Django %r", DJANGO_VERSION)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# support funcs
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# flag for update_settings() to remove specified key entirely
							 | 
						||
| 
								 | 
							
								UNSET = object()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def update_settings(**kwds):
							 | 
						||
| 
								 | 
							
								    """helper to update django settings from kwds"""
							 | 
						||
| 
								 | 
							
								    for k,v in iteritems(kwds):
							 | 
						||
| 
								 | 
							
								        if v is UNSET:
							 | 
						||
| 
								 | 
							
								            if hasattr(settings, k):
							 | 
						||
| 
								 | 
							
								                delattr(settings, k)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            setattr(settings, k, v)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if has_min_django:
							 | 
						||
| 
								 | 
							
								    from django.contrib.auth.models import User
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class FakeUser(User):
							 | 
						||
| 
								 | 
							
								        """mock user object for use in testing"""
							 | 
						||
| 
								 | 
							
								        # NOTE: this mainly just overrides .save() to test commit behavior.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: .Meta.app_label required for django >= 1.9
							 | 
						||
| 
								 | 
							
								        class Meta:
							 | 
						||
| 
								 | 
							
								            app_label = __name__
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        @memoized_property
							 | 
						||
| 
								 | 
							
								        def saved_passwords(self):
							 | 
						||
| 
								 | 
							
								            return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        def pop_saved_passwords(self):
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                return self.saved_passwords[:]
							 | 
						||
| 
								 | 
							
								            finally:
							 | 
						||
| 
								 | 
							
								                del self.saved_passwords[:]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        def save(self, update_fields=None):
							 | 
						||
| 
								 | 
							
								            # NOTE: ignoring update_fields for test purposes
							 | 
						||
| 
								 | 
							
								            self.saved_passwords.append(self.password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def create_mock_setter():
							 | 
						||
| 
								 | 
							
								    state = []
							 | 
						||
| 
								 | 
							
								    def setter(password):
							 | 
						||
| 
								 | 
							
								        state.append(password)
							 | 
						||
| 
								 | 
							
								    def popstate():
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            return state[:]
							 | 
						||
| 
								 | 
							
								        finally:
							 | 
						||
| 
								 | 
							
								            del state[:]
							 | 
						||
| 
								 | 
							
								    setter.popstate = popstate
							 | 
						||
| 
								 | 
							
								    return setter
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def check_django_hasher_has_backend(name):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    check whether django hasher is available;
							 | 
						||
| 
								 | 
							
								    or if it should be skipped because django lacks third-party library.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    assert name
							 | 
						||
| 
								 | 
							
								    from django.contrib.auth.hashers import make_password
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        make_password("", hasher=name)
							 | 
						||
| 
								 | 
							
								        return True
							 | 
						||
| 
								 | 
							
								    except ValueError as err:
							 | 
						||
| 
								 | 
							
								        if re.match("Couldn't load '.*?' algorithm .* No module named .*", str(err)):
							 | 
						||
| 
								 | 
							
								            return False
							 | 
						||
| 
								 | 
							
								        raise
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# work up stock django config
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _modify_django_config(kwds, sha_rounds=None):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    helper to build django CryptContext config matching expected setup for stock django deploy.
							 | 
						||
| 
								 | 
							
								    :param kwds:
							 | 
						||
| 
								 | 
							
								    :param sha_rounds:
							 | 
						||
| 
								 | 
							
								    :return:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    # make sure we have dict
							 | 
						||
| 
								 | 
							
								    if hasattr(kwds, "to_dict"):
							 | 
						||
| 
								 | 
							
								        # type: CryptContext
							 | 
						||
| 
								 | 
							
								        kwds = kwds.to_dict()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # update defaults
							 | 
						||
| 
								 | 
							
								    kwds.update(
							 | 
						||
| 
								 | 
							
								        # TODO: push this to passlib.apps django contexts
							 | 
						||
| 
								 | 
							
								        deprecated="auto",
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # fill in default rounds for current django version, so our sample hashes come back
							 | 
						||
| 
								 | 
							
								    # unchanged, instead of being upgraded in-place by check_password().
							 | 
						||
| 
								 | 
							
								    if sha_rounds is None and has_min_django:
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import PBKDF2PasswordHasher
							 | 
						||
| 
								 | 
							
								        sha_rounds = PBKDF2PasswordHasher.iterations
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # modify rounds
							 | 
						||
| 
								 | 
							
								    if sha_rounds:
							 | 
						||
| 
								 | 
							
								        kwds.update(
							 | 
						||
| 
								 | 
							
								            django_pbkdf2_sha1__default_rounds=sha_rounds,
							 | 
						||
| 
								 | 
							
								            django_pbkdf2_sha256__default_rounds=sha_rounds,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return kwds
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#----------------------------------------------------
							 | 
						||
| 
								 | 
							
								# build config dict that matches stock django
							 | 
						||
| 
								 | 
							
								#----------------------------------------------------
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# XXX: replace this with code that interrogates default django config directly?
							 | 
						||
| 
								 | 
							
								#      could then separate out "validation of djangoXX_context objects"
							 | 
						||
| 
								 | 
							
								#      and "validation that individual hashers match django".
							 | 
						||
| 
								 | 
							
								#      or maybe add a "get_django_context(django_version)" helper to passlib.apps?
							 | 
						||
| 
								 | 
							
								if DJANGO_VERSION >= (2, 1):
							 | 
						||
| 
								 | 
							
								    stock_config = _modify_django_config(_apps.django21_context)
							 | 
						||
| 
								 | 
							
								elif DJANGO_VERSION >= (1, 10):
							 | 
						||
| 
								 | 
							
								    stock_config = _modify_django_config(_apps.django110_context)
							 | 
						||
| 
								 | 
							
								else:
							 | 
						||
| 
								 | 
							
								    # assert DJANGO_VERSION >= (1, 8)
							 | 
						||
| 
								 | 
							
								    stock_config = _modify_django_config(_apps.django16_context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#----------------------------------------------------
							 | 
						||
| 
								 | 
							
								# override sample hashes used in test cases
							 | 
						||
| 
								 | 
							
								#----------------------------------------------------
							 | 
						||
| 
								 | 
							
								from passlib.hash import django_pbkdf2_sha256
							 | 
						||
| 
								 | 
							
								sample_hashes = dict(
							 | 
						||
| 
								 | 
							
								    django_pbkdf2_sha256=("not a password", django_pbkdf2_sha256
							 | 
						||
| 
								 | 
							
								                          .using(rounds=stock_config.get("django_pbkdf2_sha256__default_rounds"))
							 | 
						||
| 
								 | 
							
								                          .hash("not a password"))
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# test utils
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class _ExtensionSupport(object):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    test support funcs for loading/unloading extension.
							 | 
						||
| 
								 | 
							
								    this class is mixed in to various TestCase subclasses.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # support funcs
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def _iter_patch_candidates(cls):
							 | 
						||
| 
								 | 
							
								        """helper to scan for monkeypatches.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        returns tuple containing:
							 | 
						||
| 
								 | 
							
								        * object (module or class)
							 | 
						||
| 
								 | 
							
								        * attribute of object
							 | 
						||
| 
								 | 
							
								        * value of attribute
							 | 
						||
| 
								 | 
							
								        * whether it should or should not be patched
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # XXX: this and assert_unpatched() could probably be refactored to use
							 | 
						||
| 
								 | 
							
								        #      the PatchManager class to do the heavy lifting.
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth import models, hashers
							 | 
						||
| 
								 | 
							
								        user_attrs = ["check_password", "set_password"]
							 | 
						||
| 
								 | 
							
								        model_attrs = ["check_password", "make_password"]
							 | 
						||
| 
								 | 
							
								        hasher_attrs = ["check_password", "make_password", "get_hasher", "identify_hasher",
							 | 
						||
| 
								 | 
							
								                        "get_hashers"]
							 | 
						||
| 
								 | 
							
								        objs = [(models, model_attrs),
							 | 
						||
| 
								 | 
							
								                (models.User, user_attrs),
							 | 
						||
| 
								 | 
							
								                (hashers, hasher_attrs),
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								        for obj, patched in objs:
							 | 
						||
| 
								 | 
							
								            for attr in dir(obj):
							 | 
						||
| 
								 | 
							
								                if attr.startswith("_"):
							 | 
						||
| 
								 | 
							
								                    continue
							 | 
						||
| 
								 | 
							
								                value = obj.__dict__.get(attr, UNSET) # can't use getattr() due to GAE
							 | 
						||
| 
								 | 
							
								                if value is UNSET and attr not in patched:
							 | 
						||
| 
								 | 
							
								                    continue
							 | 
						||
| 
								 | 
							
								                value = get_method_function(value)
							 | 
						||
| 
								 | 
							
								                source = getattr(value, "__module__", None)
							 | 
						||
| 
								 | 
							
								                if source:
							 | 
						||
| 
								 | 
							
								                    yield obj, attr, source, (attr in patched)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # verify current patch state
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def assert_unpatched(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test that django is in unpatched state
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # make sure we aren't currently patched
							 | 
						||
| 
								 | 
							
								        mod = sys.modules.get("passlib.ext.django.models")
							 | 
						||
| 
								 | 
							
								        self.assertFalse(mod and mod.adapter.patched, "patch should not be enabled")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make sure no objects have been replaced, by checking __module__
							 | 
						||
| 
								 | 
							
								        for obj, attr, source, patched in self._iter_patch_candidates():
							 | 
						||
| 
								 | 
							
								            if patched:
							 | 
						||
| 
								 | 
							
								                self.assertTrue(source.startswith("django.contrib.auth."),
							 | 
						||
| 
								 | 
							
								                                "obj=%r attr=%r was not reverted: %r" %
							 | 
						||
| 
								 | 
							
								                                (obj, attr, source))
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                self.assertFalse(source.startswith("passlib."),
							 | 
						||
| 
								 | 
							
								                                "obj=%r attr=%r should not have been patched: %r" %
							 | 
						||
| 
								 | 
							
								                                (obj, attr, source))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def assert_patched(self, context=None):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        helper to ensure django HAS been patched, and is using specified config
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # make sure we're currently patched
							 | 
						||
| 
								 | 
							
								        mod = sys.modules.get("passlib.ext.django.models")
							 | 
						||
| 
								 | 
							
								        self.assertTrue(mod and mod.adapter.patched, "patch should have been enabled")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make sure only the expected objects have been patched
							 | 
						||
| 
								 | 
							
								        for obj, attr, source, patched in self._iter_patch_candidates():
							 | 
						||
| 
								 | 
							
								            if patched:
							 | 
						||
| 
								 | 
							
								                self.assertTrue(source == "passlib.ext.django.utils",
							 | 
						||
| 
								 | 
							
								                                "obj=%r attr=%r should have been patched: %r" %
							 | 
						||
| 
								 | 
							
								                                (obj, attr, source))
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                self.assertFalse(source.startswith("passlib."),
							 | 
						||
| 
								 | 
							
								                                "obj=%r attr=%r should not have been patched: %r" %
							 | 
						||
| 
								 | 
							
								                                (obj, attr, source))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check context matches
							 | 
						||
| 
								 | 
							
								        if context is not None:
							 | 
						||
| 
								 | 
							
								            context = CryptContext._norm_source(context)
							 | 
						||
| 
								 | 
							
								            self.assertEqual(mod.password_context.to_dict(resolve=True),
							 | 
						||
| 
								 | 
							
								                             context.to_dict(resolve=True))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # load / unload the extension (and verify it worked)
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    _config_keys = ["PASSLIB_CONFIG", "PASSLIB_CONTEXT", "PASSLIB_GET_CATEGORY"]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def load_extension(self, check=True, **kwds):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        helper to load extension with specified config & patch django
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        self.unload_extension()
							 | 
						||
| 
								 | 
							
								        if check:
							 | 
						||
| 
								 | 
							
								            config = kwds.get("PASSLIB_CONFIG") or kwds.get("PASSLIB_CONTEXT")
							 | 
						||
| 
								 | 
							
								        for key in self._config_keys:
							 | 
						||
| 
								 | 
							
								            kwds.setdefault(key, UNSET)
							 | 
						||
| 
								 | 
							
								        update_settings(**kwds)
							 | 
						||
| 
								 | 
							
								        import passlib.ext.django.models
							 | 
						||
| 
								 | 
							
								        if check:
							 | 
						||
| 
								 | 
							
								            self.assert_patched(context=config)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def unload_extension(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        helper to remove patches and unload extension
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # remove patches and unload module
							 | 
						||
| 
								 | 
							
								        mod = sys.modules.get("passlib.ext.django.models")
							 | 
						||
| 
								 | 
							
								        if mod:
							 | 
						||
| 
								 | 
							
								            mod.adapter.remove_patch()
							 | 
						||
| 
								 | 
							
								            del sys.modules["passlib.ext.django.models"]
							 | 
						||
| 
								 | 
							
								        # wipe config from django settings
							 | 
						||
| 
								 | 
							
								        update_settings(**dict((key, UNSET) for key in self._config_keys))
							 | 
						||
| 
								 | 
							
								        # check everything's gone
							 | 
						||
| 
								 | 
							
								        self.assert_unpatched()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # eoc
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# XXX: rename to ExtensionFixture?
							 | 
						||
| 
								 | 
							
								# NOTE: would roll this into _ExtensionSupport class;
							 | 
						||
| 
								 | 
							
								#       but we have to mix that one into django's TestCase classes as well;
							 | 
						||
| 
								 | 
							
								#       and our TestCase class (and this setUp() method) would foul things up.
							 | 
						||
| 
								 | 
							
								class _ExtensionTest(TestCase, _ExtensionSupport):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    TestCase mixin which makes sure extension is unloaded before test;
							 | 
						||
| 
								 | 
							
								    and make sure it's unloaded after test as well.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								    # setup
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def setUp(self):
							 | 
						||
| 
								 | 
							
								        super(_ExtensionTest, self).setUp()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.require_TEST_MODE("default")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if not DJANGO_VERSION:
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("Django not installed")
							 | 
						||
| 
								 | 
							
								        elif not has_min_django:
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("Django version too old")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # reset to baseline, and verify it worked
							 | 
						||
| 
								 | 
							
								        self.unload_extension()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # and do the same when the test exits
							 | 
						||
| 
								 | 
							
								        self.addCleanup(self.unload_extension)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								    # eoc
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# extension tests
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#: static passwords used by DjangoBehaviorTest methods
							 | 
						||
| 
								 | 
							
								PASS1 = "toomanysecrets"
							 | 
						||
| 
								 | 
							
								WRONG1 = "letmein"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DjangoBehaviorTest(_ExtensionTest):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    tests model to verify it matches django's behavior.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    running this class verifies the tests correctly assert what Django itself does.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    running the ExtensionBehaviorTest subclass below verifies "passlib.ext.django"
							 | 
						||
| 
								 | 
							
								    matches what the tests assert.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								    # class attrs
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    descriptionPrefix = "verify django behavior"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #: tracks whether tests should assume "passlib.ext.django" monkeypatch is applied.
							 | 
						||
| 
								 | 
							
								    #: (set to True by ExtensionBehaviorTest subclass)
							 | 
						||
| 
								 | 
							
								    patched = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #: dict containing CryptContext() config which should match current django deploy.
							 | 
						||
| 
								 | 
							
								    #: used by tests to verify expected behavior.
							 | 
						||
| 
								 | 
							
								    config = stock_config
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # NOTE: if this test fails, it means we're not accounting for
							 | 
						||
| 
								 | 
							
								    #       some part of django's hashing logic, or that this is
							 | 
						||
| 
								 | 
							
								    #       running against an untested version of django with a new
							 | 
						||
| 
								 | 
							
								    #       hashing policy.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								    # test helpers
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @memoized_property
							 | 
						||
| 
								 | 
							
								    def context(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        per-test CryptContext() created from .config.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        return CryptContext._norm_source(self.config)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def assert_unusable_password(self, user):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        check that user object is set to 'unusable password' constant
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        self.assertTrue(user.password.startswith("!"))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.has_usable_password())
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.pop_saved_passwords(), [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def assert_valid_password(self, user, hash=UNSET, saved=None):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        check that user object has a usable password hash.
							 | 
						||
| 
								 | 
							
								        :param hash: optionally check it has this exact hash
							 | 
						||
| 
								 | 
							
								        :param saved: check that mock commit history for user.password matches this list
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        if hash is UNSET:
							 | 
						||
| 
								 | 
							
								            self.assertNotEqual(user.password, "!")
							 | 
						||
| 
								 | 
							
								            self.assertNotEqual(user.password, None)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.assertEqual(user.password, hash)
							 | 
						||
| 
								 | 
							
								        self.assertTrue(user.has_usable_password(),
							 | 
						||
| 
								 | 
							
								                        "hash should be usable: %r" % (user.password,))
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.pop_saved_passwords(),
							 | 
						||
| 
								 | 
							
								                         [] if saved is None else [saved])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								    # test hashing interface
							 | 
						||
| 
								 | 
							
								    #-----------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								    # these functions are run against both the actual django code,
							 | 
						||
| 
								 | 
							
								    # to verify the assumptions of the unittests are correct;
							 | 
						||
| 
								 | 
							
								    # and run against the passlib extension, to verify it matches those assumptions.
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # these tests check the following django methods:
							 | 
						||
| 
								 | 
							
								    #   User.set_password()
							 | 
						||
| 
								 | 
							
								    #   User.check_password()
							 | 
						||
| 
								 | 
							
								    #   make_password() -- 1.4 only
							 | 
						||
| 
								 | 
							
								    #   check_password()
							 | 
						||
| 
								 | 
							
								    #   identify_hasher()
							 | 
						||
| 
								 | 
							
								    #   User.has_usable_password()
							 | 
						||
| 
								 | 
							
								    #   User.set_unusable_password()
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # XXX: this take a while to run. what could be trimmed?
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # TODO: add get_hasher() checks where appropriate in tests below.
							 | 
						||
| 
								 | 
							
								    #=============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_extension_config(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test extension config is loaded correctly
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        if not self.patched:
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("extension not loaded")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        ctx = self.context
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # contexts should match
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import check_password
							 | 
						||
| 
								 | 
							
								        from passlib.ext.django.models import password_context
							 | 
						||
| 
								 | 
							
								        self.assertEqual(password_context.to_dict(resolve=True), ctx.to_dict(resolve=True))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # should have patched both places
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.models import check_password as check_password2
							 | 
						||
| 
								 | 
							
								        self.assertEqual(check_password2, check_password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_default_algorithm(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test django's default algorithm
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        ctx = self.context
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import make_password
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.set_password() should use default alg
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.set_password(PASS1)
							 | 
						||
| 
								 | 
							
								        self.assertTrue(ctx.handler().verify(PASS1, user.password))
							 | 
						||
| 
								 | 
							
								        self.assert_valid_password(user)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make_password() should use default alg
							 | 
						||
| 
								 | 
							
								        hash = make_password(PASS1)
							 | 
						||
| 
								 | 
							
								        self.assertTrue(ctx.handler().verify(PASS1, hash))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_empty_password(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test how methods handle empty string as password
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        ctx = self.context
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.set_password() should use default alg
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.set_password('')
							 | 
						||
| 
								 | 
							
								        hash = user.password
							 | 
						||
| 
								 | 
							
								        self.assertTrue(ctx.handler().verify('', hash))
							 | 
						||
| 
								 | 
							
								        self.assert_valid_password(user, hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password() should return True
							 | 
						||
| 
								 | 
							
								        self.assertTrue(user.check_password(""))
							 | 
						||
| 
								 | 
							
								        self.assert_valid_password(user, hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # XXX: test make_password() ?
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # TODO: is_password_usable()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # identify_hasher() -- na
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password() should return True
							 | 
						||
| 
								 | 
							
								        self.assertTrue(check_password("", hash))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_unusable_flag(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test how methods handle 'unusable flag' in hash
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # sanity check via user.set_unusable_password()
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.set_unusable_password()
							 | 
						||
| 
								 | 
							
								        self.assert_unusable_password(user)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # ensure User.set_password() sets unusable flag
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.set_password(None)
							 | 
						||
| 
								 | 
							
								        self.assert_unusable_password(user)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password() should always fail
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(None))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password('None'))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(''))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(PASS1))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(WRONG1))
							 | 
						||
| 
								 | 
							
								        self.assert_unusable_password(user)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make_password() should also set flag
							 | 
						||
| 
								 | 
							
								        self.assertTrue(make_password(None).startswith("!"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password() should return False (didn't handle disabled under 1.3)
							 | 
						||
| 
								 | 
							
								        self.assertFalse(check_password(PASS1, '!'))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # identify_hasher() and is_password_usable() should reject it
							 | 
						||
| 
								 | 
							
								        self.assertFalse(is_password_usable(user.password))
							 | 
						||
| 
								 | 
							
								        self.assertRaises(ValueError, identify_hasher, user.password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_none_hash_value(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test how methods handle None as hash value
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        patched = self.patched
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.set_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password() - returns False
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.password = None
							 | 
						||
| 
								 | 
							
								        if quirks.none_causes_check_password_error and not patched:
							 | 
						||
| 
								 | 
							
								            # django 2.1+
							 | 
						||
| 
								 | 
							
								            self.assertRaises(TypeError, user.check_password, PASS1)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.assertFalse(user.check_password(PASS1))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.has_usable_password(),
							 | 
						||
| 
								 | 
							
								                         quirks.empty_is_usable_password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # TODO: is_password_usable()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password() - error
							 | 
						||
| 
								 | 
							
								        if quirks.none_causes_check_password_error and not patched:
							 | 
						||
| 
								 | 
							
								            self.assertRaises(TypeError, check_password, PASS1, None)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.assertFalse(check_password(PASS1, None))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # identify_hasher() - error
							 | 
						||
| 
								 | 
							
								        self.assertRaises(TypeError, identify_hasher, None)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_empty_hash_value(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test how methods handle empty string as hash value
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.set_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password()
							 | 
						||
| 
								 | 
							
								        # As of django 1.5, blank hash returns False (django issue 18453)
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.password = ""
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(PASS1))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # verify hash wasn't changed/upgraded during check_password() call
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.password, "")
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.pop_saved_passwords(), [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.has_usable_password()
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.has_usable_password(), quirks.empty_is_usable_password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # TODO: is_password_usable()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password()
							 | 
						||
| 
								 | 
							
								        self.assertFalse(check_password(PASS1, ""))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # identify_hasher() - throws error
							 | 
						||
| 
								 | 
							
								        self.assertRaises(ValueError, identify_hasher, "")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_invalid_hash_values(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        test how methods handle invalid hash values.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        for hash in [
							 | 
						||
| 
								 | 
							
								            "$789$foo",  # empty identifier
							 | 
						||
| 
								 | 
							
								        ]:
							 | 
						||
| 
								 | 
							
								            with self.subTest(hash=hash):
							 | 
						||
| 
								 | 
							
								                self._do_test_invalid_hash_value(hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _do_test_invalid_hash_value(self, hash):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.set_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.check_password()
							 | 
						||
| 
								 | 
							
								        # As of django 1.5, invalid hash returns False (side effect of django issue 18453)
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.password = hash
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(PASS1))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # verify hash wasn't changed/upgraded during check_password() call
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.password, hash)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.pop_saved_passwords(), [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # User.has_usable_password()
							 | 
						||
| 
								 | 
							
								        self.assertEqual(user.has_usable_password(), quirks.invalid_is_usable_password)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # TODO: is_password_usable()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # make_password() - n/a
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check_password()
							 | 
						||
| 
								 | 
							
								        self.assertFalse(check_password(PASS1, hash))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # identify_hasher() - throws error
							 | 
						||
| 
								 | 
							
								        self.assertRaises(ValueError, identify_hasher, hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_available_schemes(self):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        run a bunch of subtests for each hasher available in the default django setup
							 | 
						||
| 
								 | 
							
								        (as determined by reading self.context)
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        for scheme in self.context.schemes():
							 | 
						||
| 
								 | 
							
								            with self.subTest(scheme=scheme):
							 | 
						||
| 
								 | 
							
								                self._do_test_available_scheme(scheme)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _do_test_available_scheme(self, scheme):
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        helper to test how specific hasher behaves.
							 | 
						||
| 
								 | 
							
								        :param scheme: *passlib* name of hasher (e.g. "django_pbkdf2_sha256")
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        log = self.getLogger()
							 | 
						||
| 
								 | 
							
								        ctx = self.context
							 | 
						||
| 
								 | 
							
								        patched = self.patched
							 | 
						||
| 
								 | 
							
								        setter = create_mock_setter()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp()
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth.hashers import (
							 | 
						||
| 
								 | 
							
								            check_password,
							 | 
						||
| 
								 | 
							
								            make_password,
							 | 
						||
| 
								 | 
							
								            is_password_usable,
							 | 
						||
| 
								 | 
							
								            identify_hasher,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # setup constants & imports, pick a sample secret/hash combo
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        handler = ctx.handler(scheme)
							 | 
						||
| 
								 | 
							
								        log.debug("testing scheme: %r => %r", scheme, handler)
							 | 
						||
| 
								 | 
							
								        deprecated = ctx.handler(scheme).deprecated
							 | 
						||
| 
								 | 
							
								        assert not deprecated or scheme != ctx.default_scheme()
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            testcase = get_handler_case(scheme)
							 | 
						||
| 
								 | 
							
								        except exc.MissingBackendError:
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("backend not available")
							 | 
						||
| 
								 | 
							
								        assert handler_derived_from(handler, testcase.handler)
							 | 
						||
| 
								 | 
							
								        if handler.is_disabled:
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("skip disabled hasher")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # verify that django has a backend available
							 | 
						||
| 
								 | 
							
								        # (since our hasher may use different set of backends,
							 | 
						||
| 
								 | 
							
								        #  get_handler_case() above may work, but django will have nothing)
							 | 
						||
| 
								 | 
							
								        if not patched and not check_django_hasher_has_backend(handler.django_name):
							 | 
						||
| 
								 | 
							
								            assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \
							 | 
						||
| 
								 | 
							
								                "%r scheme should always have active backend" % scheme
							 | 
						||
| 
								 | 
							
								            log.warning("skipping scheme %r due to missing django dependency", scheme)
							 | 
						||
| 
								 | 
							
								            raise self.skipTest("skip due to missing dependency")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # find a sample (secret, hash) pair to test with
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            secret, hash = sample_hashes[scheme]
							 | 
						||
| 
								 | 
							
								        except KeyError:
							 | 
						||
| 
								 | 
							
								            get_sample_hash = testcase("setUp").get_sample_hash
							 | 
						||
| 
								 | 
							
								            while True:
							 | 
						||
| 
								 | 
							
								                secret, hash = get_sample_hash()
							 | 
						||
| 
								 | 
							
								                if secret:  # don't select blank passwords
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								        other = 'dontletmein'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # User.set_password() - not tested here
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # User.check_password()+migration against known hash
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        user = FakeUser()
							 | 
						||
| 
								 | 
							
								        user.password = hash
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check against invalid password
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(None))
							 | 
						||
| 
								 | 
							
								        ##self.assertFalse(user.check_password(''))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(user.check_password(other))
							 | 
						||
| 
								 | 
							
								        self.assert_valid_password(user, hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check against valid password
							 | 
						||
| 
								 | 
							
								        self.assertTrue(user.check_password(secret))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check if it upgraded the hash
							 | 
						||
| 
								 | 
							
								        # NOTE: needs_update kept separate in case we need to test rounds.
							 | 
						||
| 
								 | 
							
								        needs_update = deprecated
							 | 
						||
| 
								 | 
							
								        if needs_update:
							 | 
						||
| 
								 | 
							
								            self.assertNotEqual(user.password, hash)
							 | 
						||
| 
								 | 
							
								            self.assertFalse(handler.identify(user.password))
							 | 
						||
| 
								 | 
							
								            self.assertTrue(ctx.handler().verify(secret, user.password))
							 | 
						||
| 
								 | 
							
								            self.assert_valid_password(user, saved=user.password)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.assert_valid_password(user, hash)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # don't need to check rest for most deployments
							 | 
						||
| 
								 | 
							
								        if TEST_MODE(max="default"):
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # make_password() correctly selects algorithm
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        alg = DjangoTranslator().passlib_to_django_name(scheme)
							 | 
						||
| 
								 | 
							
								        hash2 = make_password(secret, hasher=alg)
							 | 
						||
| 
								 | 
							
								        self.assertTrue(handler.verify(secret, hash2))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # check_password()+setter against known hash
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # should call setter only if it needs_update
							 | 
						||
| 
								 | 
							
								        self.assertTrue(check_password(secret, hash, setter=setter))
							 | 
						||
| 
								 | 
							
								        self.assertEqual(setter.popstate(), [secret] if needs_update else [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # should not call setter
							 | 
						||
| 
								 | 
							
								        self.assertFalse(check_password(other, hash, setter=setter))
							 | 
						||
| 
								 | 
							
								        self.assertEqual(setter.popstate(), [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        ### check preferred kwd is ignored (feature we don't currently support fully)
							 | 
						||
| 
								 | 
							
								        ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey'))
							 | 
						||
| 
								 | 
							
								        ##self.assertEqual(setter.popstate(), [secret])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # TODO: get_hasher()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        # identify_hasher() recognizes known hash
							 | 
						||
| 
								 | 
							
								        #-------------------------------------------------------
							 | 
						||
| 
								 | 
							
								        self.assertTrue(is_password_usable(hash))
							 | 
						||
| 
								 | 
							
								        name = DjangoTranslator().django_to_passlib_name(identify_hasher(hash).algorithm)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(name, scheme)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # eoc
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===================================================================
							 | 
						||
| 
								 | 
							
								# extension fidelity tests
							 | 
						||
| 
								 | 
							
								#===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ExtensionBehaviorTest(DjangoBehaviorTest):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    test that "passlib.ext.django" conforms to behavioral assertions in DjangoBehaviorTest
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    descriptionPrefix = "verify extension behavior"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    config = dict(
							 | 
						||
| 
								 | 
							
								            schemes="sha256_crypt,md5_crypt,des_crypt",
							 | 
						||
| 
								 | 
							
								            deprecated="des_crypt",
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def setUp(self):
							 | 
						||
| 
								 | 
							
								        super(ExtensionBehaviorTest, self).setUp()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # always load extension before each test
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG=self.config)
							 | 
						||
| 
								 | 
							
								        self.patched = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===================================================================
							 | 
						||
| 
								 | 
							
								# extension internal tests
							 | 
						||
| 
								 | 
							
								#===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DjangoExtensionTest(_ExtensionTest):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    test the ``passlib.ext.django`` plugin
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # class attrs
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    descriptionPrefix = "passlib.ext.django plugin"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # monkeypatch testing
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_00_patch_control(self):
							 | 
						||
| 
								 | 
							
								        """test set_django_password_context patch/unpatch"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check config="disabled"
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="disabled", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_unpatched()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check legacy config=None
							 | 
						||
| 
								 | 
							
								        with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"):
							 | 
						||
| 
								 | 
							
								            self.load_extension(PASSLIB_CONFIG=None, check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_unpatched()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # try stock django 1.0 context
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="django-1.0", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(context=django10_context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # try to remove patch
							 | 
						||
| 
								 | 
							
								        self.unload_extension()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # patch to use stock django 1.4 context
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="django-1.4", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(context=django14_context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # try to remove patch again
							 | 
						||
| 
								 | 
							
								        self.unload_extension()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_01_overwrite_detection(self):
							 | 
						||
| 
								 | 
							
								        """test detection of foreign monkeypatching"""
							 | 
						||
| 
								 | 
							
								        # NOTE: this sets things up, and spot checks two methods,
							 | 
						||
| 
								 | 
							
								        #       this should be enough to verify patch manager is working.
							 | 
						||
| 
								 | 
							
								        # TODO: test unpatch behavior honors flag.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # configure plugin to use sample context
							 | 
						||
| 
								 | 
							
								        config = "[passlib]\nschemes=des_crypt\n"
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG=config)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # setup helpers
							 | 
						||
| 
								 | 
							
								        import django.contrib.auth.models as models
							 | 
						||
| 
								 | 
							
								        from passlib.ext.django.models import adapter
							 | 
						||
| 
								 | 
							
								        def dummy():
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # mess with User.set_password, make sure it's detected
							 | 
						||
| 
								 | 
							
								        orig = models.User.set_password
							 | 
						||
| 
								 | 
							
								        models.User.set_password = dummy
							 | 
						||
| 
								 | 
							
								        with self.assertWarningList("another library has patched.*User\.set_password"):
							 | 
						||
| 
								 | 
							
								            adapter._manager.check_all()
							 | 
						||
| 
								 | 
							
								        models.User.set_password = orig
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # mess with models.check_password, make sure it's detected
							 | 
						||
| 
								 | 
							
								        orig = models.check_password
							 | 
						||
| 
								 | 
							
								        models.check_password = dummy
							 | 
						||
| 
								 | 
							
								        with self.assertWarningList("another library has patched.*models:check_password"):
							 | 
						||
| 
								 | 
							
								            adapter._manager.check_all()
							 | 
						||
| 
								 | 
							
								        models.check_password = orig
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_02_handler_wrapper(self):
							 | 
						||
| 
								 | 
							
								        """test Hasher-compatible handler wrappers"""
							 | 
						||
| 
								 | 
							
								        from django.contrib.auth import hashers
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        passlib_to_django = DjangoTranslator().passlib_to_django
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # should return native django hasher if available
							 | 
						||
| 
								 | 
							
								        if DJANGO_VERSION > (1, 10):
							 | 
						||
| 
								 | 
							
								            self.assertRaises(ValueError, passlib_to_django, "hex_md5")
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            hasher = passlib_to_django("hex_md5")
							 | 
						||
| 
								 | 
							
								            self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # should return native django hasher
							 | 
						||
| 
								 | 
							
								        # NOTE: present but not enabled by default in django as of 2.1
							 | 
						||
| 
								 | 
							
								        #       (see _builtin_django_hashers)
							 | 
						||
| 
								 | 
							
								        hasher = passlib_to_django("django_bcrypt")
							 | 
						||
| 
								 | 
							
								        self.assertIsInstance(hasher, hashers.BCryptPasswordHasher)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # otherwise should return wrapper
							 | 
						||
| 
								 | 
							
								        from passlib.hash import sha256_crypt
							 | 
						||
| 
								 | 
							
								        hasher = passlib_to_django("sha256_crypt")
							 | 
						||
| 
								 | 
							
								        self.assertEqual(hasher.algorithm, "passlib_sha256_crypt")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # and wrapper should return correct hash
							 | 
						||
| 
								 | 
							
								        encoded = hasher.encode("stub")
							 | 
						||
| 
								 | 
							
								        self.assertTrue(sha256_crypt.verify("stub", encoded))
							 | 
						||
| 
								 | 
							
								        self.assertTrue(hasher.verify("stub", encoded))
							 | 
						||
| 
								 | 
							
								        self.assertFalse(hasher.verify("xxxx", encoded))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test wrapper accepts options
							 | 
						||
| 
								 | 
							
								        encoded = hasher.encode("stub", "abcd"*4, rounds=1234)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$"
							 | 
						||
| 
								 | 
							
								                                  "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6")
							 | 
						||
| 
								 | 
							
								        self.assertEqual(hasher.safe_summary(encoded),
							 | 
						||
| 
								 | 
							
								            {'algorithm': 'sha256_crypt',
							 | 
						||
| 
								 | 
							
								             'salt': u('abcdab**********'),
							 | 
						||
| 
								 | 
							
								             'rounds': 1234,
							 | 
						||
| 
								 | 
							
								             'hash': u('v2RWkZ*************************************'),
							 | 
						||
| 
								 | 
							
								             })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # made up name should throw error
							 | 
						||
| 
								 | 
							
								        # XXX: should this throw ValueError instead, to match django?
							 | 
						||
| 
								 | 
							
								        self.assertRaises(KeyError, passlib_to_django, "does_not_exist")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # PASSLIB_CONFIG settings
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    def test_11_config_disabled(self):
							 | 
						||
| 
								 | 
							
								        """test PASSLIB_CONFIG='disabled'"""
							 | 
						||
| 
								 | 
							
								        # test config=None (deprecated)
							 | 
						||
| 
								 | 
							
								        with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"):
							 | 
						||
| 
								 | 
							
								            self.load_extension(PASSLIB_CONFIG=None, check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_unpatched()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test disabled config
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="disabled", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_unpatched()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_12_config_presets(self):
							 | 
						||
| 
								 | 
							
								        """test PASSLIB_CONFIG='<preset>'"""
							 | 
						||
| 
								 | 
							
								        # test django presets
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONTEXT="django-default", check=False)
							 | 
						||
| 
								 | 
							
								        ctx = django16_context
							 | 
						||
| 
								 | 
							
								        self.assert_patched(ctx)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="django-1.0", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(django10_context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG="django-1.4", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(django14_context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_13_config_defaults(self):
							 | 
						||
| 
								 | 
							
								        """test PASSLIB_CONFIG default behavior"""
							 | 
						||
| 
								 | 
							
								        # check implicit default
							 | 
						||
| 
								 | 
							
								        from passlib.ext.django.utils import PASSLIB_DEFAULT
							 | 
						||
| 
								 | 
							
								        default = CryptContext.from_string(PASSLIB_DEFAULT)
							 | 
						||
| 
								 | 
							
								        self.load_extension()
							 | 
						||
| 
								 | 
							
								        self.assert_patched(PASSLIB_DEFAULT)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check default preset
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONTEXT="passlib-default", check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(PASSLIB_DEFAULT)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check explicit string
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONTEXT=PASSLIB_DEFAULT, check=False)
							 | 
						||
| 
								 | 
							
								        self.assert_patched(PASSLIB_DEFAULT)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def test_14_config_invalid(self):
							 | 
						||
| 
								 | 
							
								        """test PASSLIB_CONFIG type checks"""
							 | 
						||
| 
								 | 
							
								        update_settings(PASSLIB_CONTEXT=123, PASSLIB_CONFIG=UNSET)
							 | 
						||
| 
								 | 
							
								        self.assertRaises(TypeError, __import__, 'passlib.ext.django.models')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.unload_extension()
							 | 
						||
| 
								 | 
							
								        update_settings(PASSLIB_CONFIG="missing-preset", PASSLIB_CONTEXT=UNSET)
							 | 
						||
| 
								 | 
							
								        self.assertRaises(ValueError, __import__, 'passlib.ext.django.models')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # PASSLIB_GET_CATEGORY setting
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    def test_21_category_setting(self):
							 | 
						||
| 
								 | 
							
								        """test PASSLIB_GET_CATEGORY parameter"""
							 | 
						||
| 
								 | 
							
								        # define config where rounds can be used to detect category
							 | 
						||
| 
								 | 
							
								        config = dict(
							 | 
						||
| 
								 | 
							
								            schemes = ["sha256_crypt"],
							 | 
						||
| 
								 | 
							
								            sha256_crypt__default_rounds = 1000,
							 | 
						||
| 
								 | 
							
								            staff__sha256_crypt__default_rounds = 2000,
							 | 
						||
| 
								 | 
							
								            superuser__sha256_crypt__default_rounds = 3000,
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								        from passlib.hash import sha256_crypt
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        def run(**kwds):
							 | 
						||
| 
								 | 
							
								            """helper to take in user opts, return rounds used in password"""
							 | 
						||
| 
								 | 
							
								            user = FakeUser(**kwds)
							 | 
						||
| 
								 | 
							
								            user.set_password("stub")
							 | 
						||
| 
								 | 
							
								            return sha256_crypt.from_string(user.password).rounds
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test default get_category
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONFIG=config)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(is_staff=True), 2000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(is_superuser=True), 3000)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test patch uses explicit get_category function
							 | 
						||
| 
								 | 
							
								        def get_category(user):
							 | 
						||
| 
								 | 
							
								            return user.first_name or None
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONTEXT=config,
							 | 
						||
| 
								 | 
							
								                            PASSLIB_GET_CATEGORY=get_category)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='other'), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='staff'), 2000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='superuser'), 3000)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test patch can disable get_category entirely
							 | 
						||
| 
								 | 
							
								        def get_category(user):
							 | 
						||
| 
								 | 
							
								            return None
							 | 
						||
| 
								 | 
							
								        self.load_extension(PASSLIB_CONTEXT=config,
							 | 
						||
| 
								 | 
							
								                            PASSLIB_GET_CATEGORY=get_category)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='other'), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='staff', is_staff=True), 1000)
							 | 
						||
| 
								 | 
							
								        self.assertEqual(run(first_name='superuser', is_superuser=True), 1000)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # test bad value
							 | 
						||
| 
								 | 
							
								        self.assertRaises(TypeError, self.load_extension, PASSLIB_CONTEXT=config,
							 | 
						||
| 
								 | 
							
								                          PASSLIB_GET_CATEGORY='x')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								    # eoc
							 | 
						||
| 
								 | 
							
								    #===================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 | 
						||
| 
								 | 
							
								# eof
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 |