| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080 |
- """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
- #=============================================================================
|