summaryrefslogtreecommitdiff
path: root/passlib/tests/test_ext_django.py
diff options
context:
space:
mode:
Diffstat (limited to 'passlib/tests/test_ext_django.py')
-rw-r--r--passlib/tests/test_ext_django.py538
1 files changed, 538 insertions, 0 deletions
diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py
new file mode 100644
index 0000000..8a32664
--- /dev/null
+++ b/passlib/tests/test_ext_django.py
@@ -0,0 +1,538 @@
+"""test passlib.ext.django"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import logging; log = logging.getLogger(__name__)
+import sys
+import warnings
+#site
+#pkg
+from passlib.context import CryptContext, CryptPolicy
+from passlib.apps import django_context
+from passlib.ext.django import utils
+from passlib.hash import sha256_crypt
+from passlib.tests.utils import TestCase, unittest, ut_version, catch_warnings
+import passlib.tests.test_drivers as td
+from passlib.utils import Undef
+from passlib.registry import get_crypt_handler
+#module
+
+#=========================================================
+# import & configure django settings,
+#=========================================================
+
+try:
+ from django.conf import settings, LazySettings
+ has_django = True
+except ImportError:
+ settings = None
+ has_django = False
+
+if has_django:
+ if not isinstance(settings, LazySettings):
+ #this could mean django has been configured somehow,
+ #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 our unittests
+ settings.configure()
+
+def update_settings(**kwds):
+ for k,v in kwds.iteritems():
+ if v is Undef:
+ if hasattr(settings, k):
+ delattr(settings, k)
+ else:
+ setattr(settings, k, v)
+
+#=========================================================
+# and prepare helper to skip all relevant tests
+# if django isn't installed.
+#=========================================================
+def skipUnlessDjango(cls):
+ "helper to skip class if django not present"
+ if has_django:
+ return cls
+ if ut_version < 2:
+ return None
+ return unittest.skip("Django not installed")(cls)
+
+#=========================================================
+# mock user object
+#=========================================================
+if has_django:
+ import django.contrib.auth.models as dam
+
+ class FakeUser(dam.User):
+ "stub user object for testing"
+ #this mainly just overrides .save() to test commit behavior.
+
+ saved_password = None
+
+ def save(self):
+ self.saved_password = self.password
+
+#=========================================================
+# helper contexts
+#=========================================================
+
+# simple context which looks NOTHING like django,
+# so we can tell if patching worked.
+simple_context = CryptContext(
+ schemes = [ "md5_crypt", "des_crypt" ],
+ default = "md5_crypt",
+ deprecated = [ "des_crypt" ],
+)
+
+# some sample hashes
+sample1 = 'password'
+sample1_md5 = '$1$kAd49ifN$biuRAv1Tv0zGHyCv0uIqW.'
+sample1_des = 'PPPTDkiCeu/jM'
+sample1_sha1 = 'sha1$b215d$9ee0a66f84ef1ad99096355e788135f7e949bd41'
+
+# context for testing category funcs
+category_context = CryptContext(
+ schemes = [ "sha256_crypt" ],
+ sha256_crypt__rounds = 1000,
+ staff__sha256_crypt__rounds = 2000,
+ superuser__sha256_crypt__rounds = 3000,
+)
+
+def get_cc_rounds(**kwds):
+ "helper for testing category funcs"
+ user = FakeUser(**kwds)
+ user.set_password("placeholder")
+ return sha256_crypt.from_string(user.password).rounds
+
+#=========================================================
+# test utils
+#=========================================================
+class PatchTest(TestCase):
+ "test passlib.ext.django.utils:set_django_password_context"
+
+ case_prefix = "passlib.ext.django utils"
+
+ def assert_unpatched(self):
+ "helper to ensure django hasn't been patched"
+ state = utils._django_patch_state
+
+ #make sure we aren't currently patched
+ self.assertIs(state, None)
+
+ #make sure nothing else patches django
+ for func in [
+ dam.check_password,
+ dam.User.check_password,
+ dam.User.set_password,
+ ]:
+ self.assertEquals(func.__module__, "django.contrib.auth.models")
+ self.assertFalse(hasattr(dam.User, "password_context"))
+
+ def assert_patched(self, context=Undef):
+ "helper to ensure django HAS been patched"
+ state = utils._django_patch_state
+
+ #make sure we're patched
+ self.assertIsNot(state, None)
+
+ #make sure our methods are exposed
+ for func in [
+ dam.check_password,
+ dam.User.check_password,
+ dam.User.set_password,
+ ]:
+ self.assertEquals(func.__module__, "passlib.ext.django.utils")
+
+ #make sure methods match
+ self.assertIs(dam.check_password, state['models_check_password'])
+ self.assertIs(dam.User.check_password.im_func, state['user_check_password'])
+ self.assertIs(dam.User.set_password.im_func, state['user_set_password'])
+
+ #make sure context matches
+ obj = dam.User.password_context
+ self.assertIs(obj, state['context'])
+ if context is not Undef:
+ self.assertIs(obj, context)
+
+ #make sure old methods were stored
+ for key in [
+ "orig_models_check_password",
+ "orig_user_check_password",
+ "orig_user_set_password",
+ ]:
+ value = state[key]
+ self.assertEquals(value.__module__, "django.contrib.auth.models")
+
+ def setUp(self):
+ #reset to baseline, and verify
+ utils.set_django_password_context(None)
+ self.assert_unpatched()
+
+ def tearDown(self):
+ #reset to baseline, and verify
+ utils.set_django_password_context(None)
+ self.assert_unpatched()
+
+ def test_00_patch_control(self):
+ "test set_django_password_context patch/unpatch"
+
+ #check context=None has no effect
+ utils.set_django_password_context(None)
+ self.assert_unpatched()
+
+ #patch to use stock django context
+ utils.set_django_password_context(django_context)
+ self.assert_patched(context=django_context)
+
+ #try to remove patch
+ utils.set_django_password_context(None)
+ self.assert_unpatched()
+
+ def test_01_patch_control_detection(self):
+ "test set_django_password_context detection of foreign monkeypatches"
+ def dummy():
+ pass
+
+ with catch_warnings(record=True) as wlog:
+ warnings.simplefilter("always")
+
+ #patch to use stock django context
+ utils.set_django_password_context(django_context)
+ self.assert_patched(context=django_context)
+ self.assertEquals(len(wlog), 0)
+
+ #mess with User.set_password, make sure it's detected
+ dam.User.set_password = dummy
+ utils.set_django_password_context(django_context)
+ self.assert_patched(context=django_context)
+ self.assertEquals(len(wlog), 1)
+ self.assertWarningMatches(wlog.pop(),
+ message_re="^another library has patched.*User\.set_password$")
+
+ #mess with user.check_password, make sure it's detected
+ dam.User.check_password = dummy
+ utils.set_django_password_context(django_context)
+ self.assert_patched(context=django_context)
+ self.assertEquals(len(wlog), 1)
+ self.assertWarningMatches(wlog.pop(),
+ message_re="^another library has patched.*User\.check_password$")
+
+ #mess with user.check_password, make sure it's detected
+ dam.check_password = dummy
+ utils.set_django_password_context(django_context)
+ self.assert_patched(context=django_context)
+ self.assertEquals(len(wlog), 1)
+ self.assertWarningMatches(wlog.pop(),
+ message_re="^another library has patched.*models:check_password$")
+
+ def test_01_patch_bad_types(self):
+ "test set_django_password_context bad inputs"
+ set = utils.set_django_password_context
+ self.assertRaises(TypeError, set, CryptPolicy())
+ self.assertRaises(TypeError, set, "")
+
+ def test_02_models_check_password(self):
+ "test monkeypatched models.check_password()"
+
+ # patch to use simple context
+ utils.set_django_password_context(simple_context)
+ self.assert_patched(context=simple_context)
+
+ # check correct hashes pass
+ self.assertTrue(dam.check_password(sample1, sample1_des))
+ self.assertTrue(dam.check_password(sample1, sample1_md5))
+
+ # check bad password fail w/ false
+ self.assertFalse(dam.check_password('x', sample1_des))
+ self.assertFalse(dam.check_password('x', sample1_md5))
+
+ # and other hashes fail w/ error
+ self.assertRaises(ValueError, dam.check_password, sample1, sample1_sha1)
+ self.assertRaises(ValueError, dam.check_password, sample1, None)
+
+ def test_03_check_password(self):
+ "test monkeypatched User.check_password()"
+ # NOTE: using FakeUser so we can test .save()
+ user = FakeUser()
+
+ # patch to use simple context
+ utils.set_django_password_context(simple_context)
+ self.assert_patched(context=simple_context)
+
+ # test that blank hash is never accepted
+ self.assertIs(user.password, '')
+ self.assertIs(user.saved_password, None)
+ self.assertFalse(user.check_password('x'))
+
+ # check correct secrets pass, and wrong ones fail
+ user.password = sample1_md5
+ self.assertTrue(user.check_password(sample1))
+ self.assertFalse(user.check_password('x'))
+ self.assertFalse(user.check_password(None))
+
+ # none of that should have triggered update of password
+ self.assertIs(user.password, sample1_md5)
+ self.assertIs(user.saved_password, None)
+
+ #check unusable password
+ user.set_unusable_password()
+ self.assertFalse(user.has_usable_password())
+ self.assertFalse(user.check_password(None))
+ self.assertFalse(user.check_password(''))
+ self.assertFalse(user.check_password(sample1))
+
+ def test_04_check_password_migration(self):
+ "test User.check_password() hash migration"
+ # NOTE: using FakeUser so we can test .save()
+ user = FakeUser()
+
+ # patch to use simple context
+ utils.set_django_password_context(simple_context)
+ self.assert_patched(context=simple_context)
+
+ # set things up with a password that needs migration
+ user.password = sample1_des
+ self.assertIs(user.password, sample1_des)
+ self.assertIs(user.saved_password, None)
+
+ # run check with bad password...
+ # shouldn't have migrated
+ self.assertFalse(user.check_password('x'))
+ self.assertFalse(user.check_password(None))
+
+ self.assertIs(user.password, sample1_des)
+ self.assertIs(user.saved_password, None)
+
+ # run check with correct password...
+ # should have migrated to md5 and called save()
+ self.assertTrue(user.check_password(sample1))
+
+ self.assertTrue(user.password.startswith("$1$"))
+ self.assertIs(user.saved_password, user.password)
+
+ # check resave doesn't happen
+ user.saved_password = None
+ self.assertTrue(user.check_password(sample1))
+ self.assertIs(user.saved_password, None)
+
+ def test_05_set_password(self):
+ "test monkeypatched User.set_password()"
+ user = FakeUser()
+
+ # patch to use simple context
+ utils.set_django_password_context(simple_context)
+ self.assert_patched(context=simple_context)
+
+ # sanity check
+ self.assertIs(user.password, '')
+ self.assertIs(user.saved_password, None)
+ self.assertTrue(user.has_usable_password())
+
+ # set password
+ user.set_password(sample1)
+ self.assertTrue(user.check_password(sample1))
+ self.assertEquals(simple_context.identify(user.password), "md5_crypt")
+ self.assertIs(user.saved_password, None)
+
+ #check unusable password
+ user.set_password(None)
+ self.assertFalse(user.has_usable_password())
+ self.assertIs(user.saved_password, None)
+
+ def test_06_get_category(self):
+ "test default get_category function"
+ func = utils.get_category
+ self.assertIs(func(FakeUser()), None)
+ self.assertEquals(func(FakeUser(is_staff=True)), "staff")
+ self.assertEquals(func(FakeUser(is_superuser=True)), "superuser")
+ self.assertEquals(func(FakeUser(is_staff=True,
+ is_superuser=True)), "superuser")
+
+ def test_07_get_category(self):
+ "test set_django_password_context's get_category parameter"
+ # test patch uses default get_category
+ utils.set_django_password_context(category_context)
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(is_staff=True), 2000)
+ self.assertEquals(get_cc_rounds(is_superuser=True), 3000)
+
+ # test patch uses explicit get_category
+ def get_category(user):
+ return user.first_name or None
+ utils.set_django_password_context(category_context, get_category)
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(first_name='other'), 1000)
+ self.assertEquals(get_cc_rounds(first_name='staff'), 2000)
+ self.assertEquals(get_cc_rounds(first_name='superuser'), 3000)
+
+ # test patch can disable get_category
+ utils.set_django_password_context(category_context, None)
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(first_name='other'), 1000)
+ self.assertEquals(get_cc_rounds(first_name='staff', is_staff=True), 1000)
+ self.assertEquals(get_cc_rounds(first_name='superuser', is_superuser=True), 1000)
+
+PatchTest = skipUnlessDjango(PatchTest)
+
+#=========================================================
+# test django plugin
+#=========================================================
+
+django_hash_tests = [
+ td.HexMd5Test,
+ td.DjangoDesCryptTest,
+ td.DjangoSaltedMd5Test,
+ td.DjangoSaltedSha1Test,
+ ]
+
+default_hash_tests = django_hash_tests + [ td.Builtin_SHA512CryptTest ]
+
+class PluginTest(TestCase):
+ "test django plugin via settings"
+
+ case_prefix = "passlib.ext.django plugin"
+
+ def setUp(self):
+ #remove django patch
+ utils.set_django_password_context(None)
+
+ #ensure django settings are empty
+ update_settings(
+ PASSLIB_CONTEXT=Undef,
+ PASSLIB_GET_CATEGORY=Undef,
+ )
+
+ #unload module so it's re-run
+ sys.modules.pop("passlib.ext.django.models", None)
+
+ def tearDown(self):
+ #remove django patch
+ utils.set_django_password_context(None)
+
+ def check_hashes(self, tests, new_hash=None, deprecated=None):
+ u = FakeUser()
+ deprecated = None
+
+ # check new hash construction
+ if new_hash:
+ u.set_password("placeholder")
+ handler = get_crypt_handler(new_hash)
+ self.assertTrue(handler.identify(u.password))
+
+ # run against hashes from tests...
+ for test in tests:
+ for secret, hash in test.all_correct_hashes:
+
+ # check against valid password
+ u.password = hash
+ self.assertTrue(u.check_password(secret))
+ if new_hash and deprecated and test.handler.name in deprecated:
+ self.assertFalse(handler.identify(hash))
+ self.assertTrue(handler.identify(u.password))
+
+ # check against invalid password
+ u.password = hash
+ self.assertFalse(u.check_password('x'+secret))
+ if new_hash and deprecated and test.handler.name in deprecated:
+ self.assertFalse(handler.identify(hash))
+ self.assertEquals(u.password, hash)
+
+ # check disabled handling
+ u.set_password(None)
+ handler = get_crypt_handler("django_disabled")
+ self.assertTrue(handler.identify(u.password))
+ self.assertFalse(u.check_password('placeholder'))
+
+ def test_00_actual_django(self):
+ "test actual Django behavior has not changed"
+ #NOTE: if this test fails,
+ # probably means newer version of Django,
+ # and passlib's policies should be updated.
+ self.check_hashes(django_hash_tests,
+ "django_salted_sha1",
+ ["hex_md5"])
+
+ def test_01_explicit_unset(self, value=None):
+ "test PASSLIB_CONTEXT = None"
+ update_settings(
+ PASSLIB_CONTEXT=value,
+ )
+ import passlib.ext.django.models
+ self.check_hashes(django_hash_tests,
+ "django_salted_sha1",
+ ["hex_md5"])
+
+ def test_02_stock_ctx(self):
+ "test PASSLIB_CONTEXT = utils.STOCK_CTX"
+ self.test_01_explicit_unset(value=utils.STOCK_CTX)
+
+ def test_03_implicit_default_ctx(self):
+ "test PASSLIB_CONTEXT unset"
+ import passlib.ext.django.models
+ self.check_hashes(default_hash_tests,
+ "sha512_crypt",
+ ["hex_md5", "django_salted_sha1",
+ "django_salted_md5",
+ "django_des_crypt",
+ ])
+
+ def test_04_explicit_default_ctx(self):
+ "test PASSLIB_CONTEXT = utils.DEFAULT_CTX"
+ update_settings(
+ PASSLIB_CONTEXT=utils.DEFAULT_CTX,
+ )
+ self.test_03_implicit_default_ctx()
+
+ def test_05_default_ctx_alias(self):
+ "test PASSLIB_CONTEXT = 'passlib-default'"
+ update_settings(
+ PASSLIB_CONTEXT="passlib-default",
+ )
+ self.test_03_implicit_default_ctx()
+
+ def test_06_categories(self):
+ "test PASSLIB_GET_CATEGORY unset"
+ update_settings(
+ PASSLIB_CONTEXT=category_context.policy,
+ )
+ import passlib.ext.django.models
+
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(is_staff=True), 2000)
+ self.assertEquals(get_cc_rounds(is_superuser=True), 3000)
+
+ def test_07_categories_explicit(self):
+ "test PASSLIB_GET_CATEGORY = function"
+ def get_category(user):
+ return user.first_name or None
+ update_settings(
+ PASSLIB_CONTEXT = category_context.policy,
+ PASSLIB_GET_CATEGORY = get_category,
+ )
+ import passlib.ext.django.models
+
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(first_name='other'), 1000)
+ self.assertEquals(get_cc_rounds(first_name='staff'), 2000)
+ self.assertEquals(get_cc_rounds(first_name='superuser'), 3000)
+
+ def test_08_categories_disabled(self):
+ "test PASSLIB_GET_CATEGORY = None"
+ update_settings(
+ PASSLIB_CONTEXT = category_context.policy,
+ PASSLIB_GET_CATEGORY = None,
+ )
+ import passlib.ext.django.models
+
+ self.assertEquals(get_cc_rounds(), 1000)
+ self.assertEquals(get_cc_rounds(first_name='other'), 1000)
+ self.assertEquals(get_cc_rounds(first_name='staff', is_staff=True), 1000)
+ self.assertEquals(get_cc_rounds(first_name='superuser', is_superuser=True), 1000)
+
+PluginTest = skipUnlessDjango(PluginTest)
+
+#=========================================================
+#eof
+#=========================================================