diff options
| author | Tim Graham <timograham@gmail.com> | 2015-02-10 09:17:08 -0500 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2015-02-11 10:19:22 -0500 |
| commit | 2d7aca3da0a46c09e9c70ebdb56ed340691a999f (patch) | |
| tree | ac7c041d73e922d8e83a1944d56a27138875fe8e /tests | |
| parent | 8192a164defa24d75672e6b10cec650489b8c748 (diff) | |
Moved contrib.auth tests out of contrib.
Diffstat (limited to 'tests')
42 files changed, 4477 insertions, 0 deletions
diff --git a/tests/auth_tests/__init__.py b/tests/auth_tests/__init__.py new file mode 100644 index 0000000000..2c308642e5 --- /dev/null +++ b/tests/auth_tests/__init__.py @@ -0,0 +1 @@ +# The password for the fixture data users is 'password' diff --git a/tests/auth_tests/backend_alias.py b/tests/auth_tests/backend_alias.py new file mode 100644 index 0000000000..ae14d1538c --- /dev/null +++ b/tests/auth_tests/backend_alias.py @@ -0,0 +1,4 @@ +# For testing that auth backends can be referenced using a convenience import +from .test_auth_backends import ImportedModelBackend + +__all__ = ['ImportedModelBackend'] diff --git a/tests/auth_tests/fixtures/authtestdata.json b/tests/auth_tests/fixtures/authtestdata.json new file mode 100644 index 0000000000..931328899b --- /dev/null +++ b/tests/auth_tests/fixtures/authtestdata.json @@ -0,0 +1,110 @@ +[ + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "2", + "model": "auth.user", + "fields": { + "username": "inactive", + "first_name": "Inactive", + "last_name": "User", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient2@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "3", + "model": "auth.user", + "fields": { + "username": "staff", + "first_name": "Staff", + "last_name": "Member", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "staffmember@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "4", + "model": "auth.user", + "fields": { + "username": "empty_password", + "first_name": "Empty", + "last_name": "Password", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "", + "email": "empty_password@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "5", + "model": "auth.user", + "fields": { + "username": "unmanageable_password", + "first_name": "Unmanageable", + "last_name": "Password", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "$", + "email": "unmanageable_password@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "6", + "model": "auth.user", + "fields": { + "username": "unknown_password", + "first_name": "Unknown", + "last_name": "Password", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "foo$bar", + "email": "unknown_password@example.com", + "date_joined": "2006-12-17 07:03:31" + } + } +] diff --git a/tests/auth_tests/fixtures/context-processors-users.xml b/tests/auth_tests/fixtures/context-processors-users.xml new file mode 100644 index 0000000000..aba8f4aace --- /dev/null +++ b/tests/auth_tests/fixtures/context-processors-users.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> +</django-objects> diff --git a/tests/auth_tests/fixtures/custom_user.json b/tests/auth_tests/fixtures/custom_user.json new file mode 100644 index 0000000000..770bea6541 --- /dev/null +++ b/tests/auth_tests/fixtures/custom_user.json @@ -0,0 +1,14 @@ +[ + { + "pk": "1", + "model": "auth.customuser", + "fields": { + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "last_login": "2006-12-17 07:03:31", + "email": "staffmember@example.com", + "is_active": true, + "is_admin": false, + "date_of_birth": "1976-11-08" + } + } +]
\ No newline at end of file diff --git a/tests/auth_tests/fixtures/natural.json b/tests/auth_tests/fixtures/natural.json new file mode 100644 index 0000000000..7811c7a548 --- /dev/null +++ b/tests/auth_tests/fixtures/natural.json @@ -0,0 +1,32 @@ +[ + { + "pk": 1, + "model": "auth.group", + "fields": { + "name": "my_group", + "permissions": [] + } + }, + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "my_username", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2012-01-13 00:14:00", + "groups": [ + [ + "my_group" + ] + ], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$LUyhxJjuLwXF$f6Zbpnx1L5dPze8m0itBaHMDyZ/n6JyhuavQy2RrBIM=", + "email": "email@example.com", + "date_joined": "2012-01-13 00:14:00" + } + } +] diff --git a/tests/auth_tests/fixtures/regular.json b/tests/auth_tests/fixtures/regular.json new file mode 100644 index 0000000000..b9f2680766 --- /dev/null +++ b/tests/auth_tests/fixtures/regular.json @@ -0,0 +1,30 @@ +[ + { + "pk": 1, + "model": "auth.group", + "fields": { + "name": "my_group", + "permissions": [] + } + }, + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "my_username", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2012-01-13 00:14:00", + "groups": [ + 1 + ], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$LUyhxJjuLwXF$f6Zbpnx1L5dPze8m0itBaHMDyZ/n6JyhuavQy2RrBIM=", + "email": "email@example.com", + "date_joined": "2012-01-13 00:14:00" + } + } +] diff --git a/tests/auth_tests/settings.py b/tests/auth_tests/settings.py new file mode 100644 index 0000000000..7da6144d4b --- /dev/null +++ b/tests/auth_tests/settings.py @@ -0,0 +1,20 @@ +import os + +from django.utils._os import upath + +AUTH_MIDDLEWARE_CLASSES = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +] + +AUTH_TEMPLATES = [{ + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(os.path.dirname(upath(__file__)), 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, +}] diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_access.html b/tests/auth_tests/templates/context_processors/auth_attrs_access.html new file mode 100644 index 0000000000..b5c65db28d --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_access.html @@ -0,0 +1 @@ +{{ user }} diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_messages.html b/tests/auth_tests/templates/context_processors/auth_attrs_messages.html new file mode 100644 index 0000000000..7b7e448ad2 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_messages.html @@ -0,0 +1 @@ +{% for m in messages %}{{ m }}{% endfor %} diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_no_access.html b/tests/auth_tests/templates/context_processors/auth_attrs_no_access.html new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_no_access.html @@ -0,0 +1 @@ + diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_perm_in_perms.html b/tests/auth_tests/templates/context_processors/auth_attrs_perm_in_perms.html new file mode 100644 index 0000000000..3a18cd7405 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_perm_in_perms.html @@ -0,0 +1,4 @@ +{% if 'auth' in perms %}Has auth permissions{% endif %} +{% if 'auth.add_permission' in perms %}Has auth.add_permission permissions{% endif %} +{% if 'nonexisting' in perms %}nonexisting perm found{% endif %} +{% if 'auth.nonexisting' in perms %}auth.nonexisting perm found{% endif %} diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_perms.html b/tests/auth_tests/templates/context_processors/auth_attrs_perms.html new file mode 100644 index 0000000000..6f441afc10 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_perms.html @@ -0,0 +1,4 @@ +{% if perms.auth %}Has auth permissions{% endif %} +{% if perms.auth.add_permission %}Has auth.add_permission permissions{% endif %} +{% if perms.nonexisting %}nonexisting perm found{% endif %} +{% if perms.auth.nonexisting in perms %}auth.nonexisting perm found{% endif %} diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_test_access.html b/tests/auth_tests/templates/context_processors/auth_attrs_test_access.html new file mode 100644 index 0000000000..a28ff937f8 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_test_access.html @@ -0,0 +1 @@ +{% if session_accessed %}Session accessed{% else %}Session not accessed{% endif %} diff --git a/tests/auth_tests/templates/context_processors/auth_attrs_user.html b/tests/auth_tests/templates/context_processors/auth_attrs_user.html new file mode 100644 index 0000000000..dc4c6b17c1 --- /dev/null +++ b/tests/auth_tests/templates/context_processors/auth_attrs_user.html @@ -0,0 +1,4 @@ +unicode: {{ user }} +id: {{ user.pk }} +username: {{ user.username }} +url: {% url 'userpage' user %} diff --git a/tests/auth_tests/templates/registration/html_password_reset_email.html b/tests/auth_tests/templates/registration/html_password_reset_email.html new file mode 100644 index 0000000000..1ebb550048 --- /dev/null +++ b/tests/auth_tests/templates/registration/html_password_reset_email.html @@ -0,0 +1 @@ +<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html> diff --git a/tests/auth_tests/templates/registration/logged_out.html b/tests/auth_tests/templates/registration/logged_out.html new file mode 100644 index 0000000000..0a06d9554a --- /dev/null +++ b/tests/auth_tests/templates/registration/logged_out.html @@ -0,0 +1 @@ +Logged out diff --git a/tests/auth_tests/templates/registration/login.html b/tests/auth_tests/templates/registration/login.html new file mode 100644 index 0000000000..5dd03cba72 --- /dev/null +++ b/tests/auth_tests/templates/registration/login.html @@ -0,0 +1 @@ +{{ form.as_ul }} diff --git a/tests/auth_tests/templates/registration/password_change_form.html b/tests/auth_tests/templates/registration/password_change_form.html new file mode 100644 index 0000000000..f9a859ae9f --- /dev/null +++ b/tests/auth_tests/templates/registration/password_change_form.html @@ -0,0 +1 @@ +{{ form }} diff --git a/tests/auth_tests/templates/registration/password_reset_complete.html b/tests/auth_tests/templates/registration/password_reset_complete.html new file mode 100644 index 0000000000..b6569b29d4 --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_complete.html @@ -0,0 +1 @@ +Password reset successfully diff --git a/tests/auth_tests/templates/registration/password_reset_confirm.html b/tests/auth_tests/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000000..42677d6766 --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_confirm.html @@ -0,0 +1,7 @@ +Hello, {{ form.user }}. + +{% if validlink %} +Please enter your new password: {{ form }} +{% else %} +The password reset link was invalid +{% endif %} diff --git a/tests/auth_tests/templates/registration/password_reset_done.html b/tests/auth_tests/templates/registration/password_reset_done.html new file mode 100644 index 0000000000..3a764cadf1 --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_done.html @@ -0,0 +1 @@ +Email sent diff --git a/tests/auth_tests/templates/registration/password_reset_email.html b/tests/auth_tests/templates/registration/password_reset_email.html new file mode 100644 index 0000000000..baac2fc2dd --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_email.html @@ -0,0 +1 @@ +{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/ diff --git a/tests/auth_tests/templates/registration/password_reset_form.html b/tests/auth_tests/templates/registration/password_reset_form.html new file mode 100644 index 0000000000..f9a859ae9f --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_form.html @@ -0,0 +1 @@ +{{ form }} diff --git a/tests/auth_tests/templates/registration/password_reset_subject.txt b/tests/auth_tests/templates/registration/password_reset_subject.txt new file mode 100644 index 0000000000..904b645c6c --- /dev/null +++ b/tests/auth_tests/templates/registration/password_reset_subject.txt @@ -0,0 +1 @@ +{% autoescape off %}Custom password reset on {{ site_name }}{% endautoescape %}
\ No newline at end of file diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py new file mode 100644 index 0000000000..5dde319197 --- /dev/null +++ b/tests/auth_tests/test_auth_backends.py @@ -0,0 +1,614 @@ +from __future__ import unicode_literals + +from datetime import date + +from django.contrib.auth import BACKEND_SESSION_KEY, authenticate, get_user +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.hashers import MD5PasswordHasher +from django.contrib.auth.models import AnonymousUser, Group, Permission, User +from django.contrib.auth.tests.custom_user import ( + CustomPermissionsUser, CustomUser, ExtensionUser, +) +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.http import HttpRequest +from django.test import TestCase, modify_settings, override_settings + + +class CountingMD5PasswordHasher(MD5PasswordHasher): + """Hasher that counts how many times it computes a hash.""" + + calls = 0 + + def encode(self, *args, **kwargs): + type(self).calls += 1 + return super(CountingMD5PasswordHasher, self).encode(*args, **kwargs) + + +class BaseModelBackendTest(object): + """ + A base class for tests that need to validate the ModelBackend + with different User models. Subclasses should define a class + level UserModel attribute, and a create_users() method to + construct two users for test purposes. + """ + backend = 'django.contrib.auth.backends.ModelBackend' + + def setUp(self): + self.patched_settings = modify_settings( + AUTHENTICATION_BACKENDS={'append': self.backend}, + ) + self.patched_settings.enable() + self.create_users() + + def tearDown(self): + self.patched_settings.disable() + # The custom_perms test messes with ContentTypes, which will + # be cached; flush the cache to ensure there are no side effects + # Refs #14975, #14925 + ContentType.objects.clear_cache() + + def test_has_perm(self): + user = self.UserModel._default_manager.get(pk=self.user.pk) + self.assertEqual(user.has_perm('auth.test'), False) + + user.is_staff = True + user.save() + self.assertEqual(user.has_perm('auth.test'), False) + + user.is_superuser = True + user.save() + self.assertEqual(user.has_perm('auth.test'), True) + + user.is_staff = True + user.is_superuser = True + user.is_active = False + user.save() + self.assertEqual(user.has_perm('auth.test'), False) + + def test_custom_perms(self): + user = self.UserModel._default_manager.get(pk=self.user.pk) + content_type = ContentType.objects.get_for_model(Group) + perm = Permission.objects.create(name='test', content_type=content_type, codename='test') + user.user_permissions.add(perm) + + # reloading user to purge the _perm_cache + user = self.UserModel._default_manager.get(pk=self.user.pk) + self.assertEqual(user.get_all_permissions() == {'auth.test'}, True) + self.assertEqual(user.get_group_permissions(), set()) + self.assertEqual(user.has_module_perms('Group'), False) + self.assertEqual(user.has_module_perms('auth'), True) + + perm = Permission.objects.create(name='test2', content_type=content_type, codename='test2') + user.user_permissions.add(perm) + perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3') + user.user_permissions.add(perm) + user = self.UserModel._default_manager.get(pk=self.user.pk) + self.assertEqual(user.get_all_permissions(), {'auth.test2', 'auth.test', 'auth.test3'}) + self.assertEqual(user.has_perm('test'), False) + self.assertEqual(user.has_perm('auth.test'), True) + self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), True) + + perm = Permission.objects.create(name='test_group', content_type=content_type, codename='test_group') + group = Group.objects.create(name='test_group') + group.permissions.add(perm) + user.groups.add(group) + user = self.UserModel._default_manager.get(pk=self.user.pk) + exp = {'auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'} + self.assertEqual(user.get_all_permissions(), exp) + self.assertEqual(user.get_group_permissions(), {'auth.test_group'}) + self.assertEqual(user.has_perms(['auth.test3', 'auth.test_group']), True) + + user = AnonymousUser() + self.assertEqual(user.has_perm('test'), False) + self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), False) + + def test_has_no_object_perm(self): + """Regressiontest for #12462""" + user = self.UserModel._default_manager.get(pk=self.user.pk) + content_type = ContentType.objects.get_for_model(Group) + perm = Permission.objects.create(name='test', content_type=content_type, codename='test') + user.user_permissions.add(perm) + + self.assertEqual(user.has_perm('auth.test', 'object'), False) + self.assertEqual(user.get_all_permissions('object'), set()) + self.assertEqual(user.has_perm('auth.test'), True) + self.assertEqual(user.get_all_permissions(), {'auth.test'}) + + def test_anonymous_has_no_permissions(self): + """ + #17903 -- Anonymous users shouldn't have permissions in + ModelBackend.get_(all|user|group)_permissions(). + """ + backend = ModelBackend() + + user = self.UserModel._default_manager.get(pk=self.user.pk) + content_type = ContentType.objects.get_for_model(Group) + user_perm = Permission.objects.create(name='test', content_type=content_type, codename='test_user') + group_perm = Permission.objects.create(name='test2', content_type=content_type, codename='test_group') + user.user_permissions.add(user_perm) + + group = Group.objects.create(name='test_group') + user.groups.add(group) + group.permissions.add(group_perm) + + self.assertEqual(backend.get_all_permissions(user), {'auth.test_user', 'auth.test_group'}) + self.assertEqual(backend.get_user_permissions(user), {'auth.test_user', 'auth.test_group'}) + self.assertEqual(backend.get_group_permissions(user), {'auth.test_group'}) + + user.is_anonymous = lambda: True + + self.assertEqual(backend.get_all_permissions(user), set()) + self.assertEqual(backend.get_user_permissions(user), set()) + self.assertEqual(backend.get_group_permissions(user), set()) + + def test_inactive_has_no_permissions(self): + """ + #17903 -- Inactive users shouldn't have permissions in + ModelBackend.get_(all|user|group)_permissions(). + """ + backend = ModelBackend() + + user = self.UserModel._default_manager.get(pk=self.user.pk) + content_type = ContentType.objects.get_for_model(Group) + user_perm = Permission.objects.create(name='test', content_type=content_type, codename='test_user') + group_perm = Permission.objects.create(name='test2', content_type=content_type, codename='test_group') + user.user_permissions.add(user_perm) + + group = Group.objects.create(name='test_group') + user.groups.add(group) + group.permissions.add(group_perm) + + self.assertEqual(backend.get_all_permissions(user), {'auth.test_user', 'auth.test_group'}) + self.assertEqual(backend.get_user_permissions(user), {'auth.test_user', 'auth.test_group'}) + self.assertEqual(backend.get_group_permissions(user), {'auth.test_group'}) + + user.is_active = False + user.save() + + self.assertEqual(backend.get_all_permissions(user), set()) + self.assertEqual(backend.get_user_permissions(user), set()) + self.assertEqual(backend.get_group_permissions(user), set()) + + def test_get_all_superuser_permissions(self): + """A superuser has all permissions. Refs #14795.""" + user = self.UserModel._default_manager.get(pk=self.superuser.pk) + self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) + + @override_settings(PASSWORD_HASHERS=['auth_tests.test_auth_backends.CountingMD5PasswordHasher']) + def test_authentication_timing(self): + """Hasher is run once regardless of whether the user exists. Refs #20760.""" + # Re-set the password, because this tests overrides PASSWORD_HASHERS + self.user.set_password('test') + self.user.save() + + CountingMD5PasswordHasher.calls = 0 + username = getattr(self.user, self.UserModel.USERNAME_FIELD) + authenticate(username=username, password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + + CountingMD5PasswordHasher.calls = 0 + authenticate(username='no_such_user', password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + + +class ModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the default User model. + """ + UserModel = User + + def create_users(self): + self.user = User.objects.create_user( + username='test', + email='test@example.com', + password='test', + ) + self.superuser = User.objects.create_superuser( + username='test2', + email='test2@example.com', + password='test', + ) + + +@override_settings(AUTH_USER_MODEL='auth.ExtensionUser') +class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the custom ExtensionUser model. + + This isn't a perfect test, because both the User and ExtensionUser are + synchronized to the database, which wouldn't ordinary happen in + production. As a result, it doesn't catch errors caused by the non- + existence of the User table. + + The specific problem is queries on .filter(groups__user) et al, which + makes an implicit assumption that the user model is called 'User'. In + production, the auth.User table won't exist, so the requested join + won't exist either; in testing, the auth.User *does* exist, and + so does the join. However, the join table won't contain any useful + data; for testing, we check that the data we expect actually does exist. + """ + + UserModel = ExtensionUser + + def create_users(self): + self.user = ExtensionUser._default_manager.create_user( + username='test', + email='test@example.com', + password='test', + date_of_birth=date(2006, 4, 25) + ) + self.superuser = ExtensionUser._default_manager.create_superuser( + username='test2', + email='test2@example.com', + password='test', + date_of_birth=date(1976, 11, 8) + ) + + +@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser') +class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the CustomPermissionsUser model. + + As with the ExtensionUser test, this isn't a perfect test, because both + the User and CustomPermissionsUser are synchronized to the database, + which wouldn't ordinary happen in production. + """ + + UserModel = CustomPermissionsUser + + def create_users(self): + self.user = CustomPermissionsUser._default_manager.create_user( + email='test@example.com', + password='test', + date_of_birth=date(2006, 4, 25) + ) + self.superuser = CustomPermissionsUser._default_manager.create_superuser( + email='test2@example.com', + password='test', + date_of_birth=date(1976, 11, 8) + ) + + +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserModelBackendAuthenticateTest(TestCase): + """ + Tests that the model backend can accept a credentials kwarg labeled with + custom user model's USERNAME_FIELD. + """ + + def test_authenticate(self): + test_user = CustomUser._default_manager.create_user( + email='test@example.com', + password='test', + date_of_birth=date(2006, 4, 25) + ) + authenticated_user = authenticate(email='test@example.com', password='test') + self.assertEqual(test_user, authenticated_user) + + +class TestObj(object): + pass + + +class SimpleRowlevelBackend(object): + def has_perm(self, user, perm, obj=None): + if not obj: + return # We only support row level perms + + if isinstance(obj, TestObj): + if user.username == 'test2': + return True + elif user.is_anonymous() and perm == 'anon': + return True + elif not user.is_active and perm == 'inactive': + return True + return False + + def has_module_perms(self, user, app_label): + if not user.is_anonymous() and not user.is_active: + return False + return app_label == "app1" + + def get_all_permissions(self, user, obj=None): + if not obj: + return [] # We only support row level perms + + if not isinstance(obj, TestObj): + return ['none'] + + if user.is_anonymous(): + return ['anon'] + if user.username == 'test2': + return ['simple', 'advanced'] + else: + return ['simple'] + + def get_group_permissions(self, user, obj=None): + if not obj: + return # We only support row level perms + + if not isinstance(obj, TestObj): + return ['none'] + + if 'test_group' in [group.name for group in user.groups.all()]: + return ['group_perm'] + else: + return ['none'] + + +@modify_settings(AUTHENTICATION_BACKENDS={ + 'append': 'auth_tests.test_auth_backends.SimpleRowlevelBackend', +}) +class RowlevelBackendTest(TestCase): + """ + Tests for auth backend that supports object level permissions + """ + + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + self.user2 = User.objects.create_user('test2', 'test2@example.com', 'test') + self.user3 = User.objects.create_user('test3', 'test3@example.com', 'test') + + def tearDown(self): + # The get_group_permissions test messes with ContentTypes, which will + # be cached; flush the cache to ensure there are no side effects + # Refs #14975, #14925 + ContentType.objects.clear_cache() + + def test_has_perm(self): + self.assertEqual(self.user1.has_perm('perm', TestObj()), False) + self.assertEqual(self.user2.has_perm('perm', TestObj()), True) + self.assertEqual(self.user2.has_perm('perm'), False) + self.assertEqual(self.user2.has_perms(['simple', 'advanced'], TestObj()), True) + self.assertEqual(self.user3.has_perm('perm', TestObj()), False) + self.assertEqual(self.user3.has_perm('anon', TestObj()), False) + self.assertEqual(self.user3.has_perms(['simple', 'advanced'], TestObj()), False) + + def test_get_all_permissions(self): + self.assertEqual(self.user1.get_all_permissions(TestObj()), {'simple'}) + self.assertEqual(self.user2.get_all_permissions(TestObj()), {'simple', 'advanced'}) + self.assertEqual(self.user2.get_all_permissions(), set()) + + def test_get_group_permissions(self): + group = Group.objects.create(name='test_group') + self.user3.groups.add(group) + self.assertEqual(self.user3.get_group_permissions(TestObj()), {'group_perm'}) + + +@override_settings( + AUTHENTICATION_BACKENDS=['auth_tests.test_auth_backends.SimpleRowlevelBackend'], +) +class AnonymousUserBackendTest(TestCase): + """ + Tests for AnonymousUser delegating to backend. + """ + + def setUp(self): + self.user1 = AnonymousUser() + + def test_has_perm(self): + self.assertEqual(self.user1.has_perm('perm', TestObj()), False) + self.assertEqual(self.user1.has_perm('anon', TestObj()), True) + + def test_has_perms(self): + self.assertEqual(self.user1.has_perms(['anon'], TestObj()), True) + self.assertEqual(self.user1.has_perms(['anon', 'perm'], TestObj()), False) + + def test_has_module_perms(self): + self.assertEqual(self.user1.has_module_perms("app1"), True) + self.assertEqual(self.user1.has_module_perms("app2"), False) + + def test_get_all_permissions(self): + self.assertEqual(self.user1.get_all_permissions(TestObj()), {'anon'}) + + +@override_settings(AUTHENTICATION_BACKENDS=[]) +class NoBackendsTest(TestCase): + """ + Tests that an appropriate error is raised if no auth backends are provided. + """ + def setUp(self): + self.user = User.objects.create_user('test', 'test@example.com', 'test') + + def test_raises_exception(self): + self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) + + +@override_settings(AUTHENTICATION_BACKENDS=['auth_tests.test_auth_backends.SimpleRowlevelBackend']) +class InActiveUserBackendTest(TestCase): + """ + Tests for an inactive user + """ + + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + self.user1.is_active = False + self.user1.save() + + def test_has_perm(self): + self.assertEqual(self.user1.has_perm('perm', TestObj()), False) + self.assertEqual(self.user1.has_perm('inactive', TestObj()), True) + + def test_has_module_perms(self): + self.assertEqual(self.user1.has_module_perms("app1"), False) + self.assertEqual(self.user1.has_module_perms("app2"), False) + + +class PermissionDeniedBackend(object): + """ + Always raises PermissionDenied in `authenticate`, `has_perm` and `has_module_perms`. + """ + supports_object_permissions = True + supports_anonymous_user = True + supports_inactive_user = True + + def authenticate(self, username=None, password=None): + raise PermissionDenied + + def has_perm(self, user_obj, perm, obj=None): + raise PermissionDenied + + def has_module_perms(self, user_obj, app_label): + raise PermissionDenied + + +class PermissionDeniedBackendTest(TestCase): + """ + Tests that other backends are not checked once a backend raises PermissionDenied + """ + backend = 'auth_tests.test_auth_backends.PermissionDeniedBackend' + + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + self.user1.save() + + @modify_settings(AUTHENTICATION_BACKENDS={'prepend': backend}) + def test_permission_denied(self): + "user is not authenticated after a backend raises permission denied #2550" + self.assertEqual(authenticate(username='test', password='test'), None) + + @modify_settings(AUTHENTICATION_BACKENDS={'append': backend}) + def test_authenticates(self): + self.assertEqual(authenticate(username='test', password='test'), self.user1) + + @modify_settings(AUTHENTICATION_BACKENDS={'prepend': backend}) + def test_has_perm_denied(self): + content_type = ContentType.objects.get_for_model(Group) + perm = Permission.objects.create(name='test', content_type=content_type, codename='test') + self.user1.user_permissions.add(perm) + + self.assertIs(self.user1.has_perm('auth.test'), False) + self.assertIs(self.user1.has_module_perms('auth'), False) + + @modify_settings(AUTHENTICATION_BACKENDS={'append': backend}) + def test_has_perm(self): + content_type = ContentType.objects.get_for_model(Group) + perm = Permission.objects.create(name='test', content_type=content_type, codename='test') + self.user1.user_permissions.add(perm) + + self.assertIs(self.user1.has_perm('auth.test'), True) + self.assertIs(self.user1.has_module_perms('auth'), True) + + +class NewModelBackend(ModelBackend): + pass + + +class ChangedBackendSettingsTest(TestCase): + """ + Tests for changes in the settings.AUTHENTICATION_BACKENDS + """ + backend = 'auth_tests.test_auth_backends.NewModelBackend' + + TEST_USERNAME = 'test_user' + TEST_PASSWORD = 'test_password' + TEST_EMAIL = 'test@example.com' + + def setUp(self): + User.objects.create_user(self.TEST_USERNAME, + self.TEST_EMAIL, + self.TEST_PASSWORD) + + @override_settings(AUTHENTICATION_BACKENDS=[backend]) + def test_changed_backend_settings(self): + """ + Tests that removing a backend configured in AUTHENTICATION_BACKENDS + make already logged-in users disconnect. + """ + + # Get a session for the test user + self.assertTrue(self.client.login( + username=self.TEST_USERNAME, + password=self.TEST_PASSWORD) + ) + + # Prepare a request object + request = HttpRequest() + request.session = self.client.session + + # Remove NewModelBackend + with self.settings(AUTHENTICATION_BACKENDS=[ + 'django.contrib.auth.backends.ModelBackend']): + # Get the user from the request + user = get_user(request) + + # Assert that the user retrieval is successful and the user is + # anonymous as the backend is not longer available. + self.assertIsNotNone(user) + self.assertTrue(user.is_anonymous()) + + +class TypeErrorBackend(object): + """ + Always raises TypeError. + """ + supports_object_permissions = True + supports_anonymous_user = True + supports_inactive_user = True + + def authenticate(self, username=None, password=None): + raise TypeError + + +class TypeErrorBackendTest(TestCase): + """ + Tests that a TypeError within a backend is propagated properly. + + Regression test for ticket #18171 + """ + backend = 'auth_tests.test_auth_backends.TypeErrorBackend' + + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + + @override_settings(AUTHENTICATION_BACKENDS=[backend]) + def test_type_error_raised(self): + self.assertRaises(TypeError, authenticate, username='test', password='test') + + +class ImproperlyConfiguredUserModelTest(TestCase): + """ + Tests that an exception from within get_user_model is propagated and doesn't + raise an UnboundLocalError. + + Regression test for ticket #21439 + """ + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + self.client.login( + username='test', + password='test' + ) + + @override_settings(AUTH_USER_MODEL='thismodel.doesntexist') + def test_does_not_shadow_exception(self): + # Prepare a request object + request = HttpRequest() + request.session = self.client.session + + self.assertRaises(ImproperlyConfigured, get_user, request) + + +class ImportedModelBackend(ModelBackend): + pass + + +class ImportedBackendTests(TestCase): + """ + #23925 - The backend path added to the session should be the same + as the one defined in AUTHENTICATION_BACKENDS setting. + """ + + backend = 'auth_tests.backend_alias.ImportedModelBackend' + + @override_settings(AUTHENTICATION_BACKENDS=[backend]) + def test_backend_path(self): + username = 'username' + password = 'password' + User.objects.create_user(username, 'email', password) + self.assertTrue(self.client.login(username=username, password=password)) + request = HttpRequest() + request.session = self.client.session + self.assertEqual(request.session[BACKEND_SESSION_KEY], self.backend) diff --git a/tests/auth_tests/test_basic.py b/tests/auth_tests/test_basic.py new file mode 100644 index 0000000000..6fe88f769f --- /dev/null +++ b/tests/auth_tests/test_basic.py @@ -0,0 +1,116 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.auth.tests.custom_user import CustomUser +from django.core.exceptions import ImproperlyConfigured +from django.dispatch import receiver +from django.test import TestCase, override_settings +from django.test.signals import setting_changed +from django.utils import translation + + +@receiver(setting_changed) +def user_model_swapped(**kwargs): + if kwargs['setting'] == 'AUTH_USER_MODEL': + from django.db.models.manager import ensure_default_manager + # Reset User manager + setattr(User, 'objects', User._default_manager) + ensure_default_manager(User) + apps.clear_cache() + + +class BasicTestCase(TestCase): + def test_user(self): + "Check that users can be created and can set their password" + u = User.objects.create_user('testuser', 'test@example.com', 'testpw') + self.assertTrue(u.has_usable_password()) + self.assertFalse(u.check_password('bad')) + self.assertTrue(u.check_password('testpw')) + + # Check we can manually set an unusable password + u.set_unusable_password() + u.save() + self.assertFalse(u.check_password('testpw')) + self.assertFalse(u.has_usable_password()) + u.set_password('testpw') + self.assertTrue(u.check_password('testpw')) + u.set_password(None) + self.assertFalse(u.has_usable_password()) + + # Check username getter + self.assertEqual(u.get_username(), 'testuser') + + # Check authentication/permissions + self.assertTrue(u.is_authenticated()) + self.assertFalse(u.is_staff) + self.assertTrue(u.is_active) + self.assertFalse(u.is_superuser) + + # Check API-based user creation with no password + u2 = User.objects.create_user('testuser2', 'test2@example.com') + self.assertFalse(u2.has_usable_password()) + + def test_user_no_email(self): + "Check that users can be created without an email" + u = User.objects.create_user('testuser1') + self.assertEqual(u.email, '') + + u2 = User.objects.create_user('testuser2', email='') + self.assertEqual(u2.email, '') + + u3 = User.objects.create_user('testuser3', email=None) + self.assertEqual(u3.email, '') + + def test_anonymous_user(self): + "Check the properties of the anonymous user" + a = AnonymousUser() + self.assertEqual(a.pk, None) + self.assertEqual(a.username, '') + self.assertEqual(a.get_username(), '') + self.assertFalse(a.is_authenticated()) + self.assertFalse(a.is_staff) + self.assertFalse(a.is_active) + self.assertFalse(a.is_superuser) + self.assertEqual(a.groups.all().count(), 0) + self.assertEqual(a.user_permissions.all().count(), 0) + + def test_superuser(self): + "Check the creation and properties of a superuser" + super = User.objects.create_superuser('super', 'super@example.com', 'super') + self.assertTrue(super.is_superuser) + self.assertTrue(super.is_active) + self.assertTrue(super.is_staff) + + def test_get_user_model(self): + "The current user model can be retrieved" + self.assertEqual(get_user_model(), User) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "The current user model can be swapped out for another" + self.assertEqual(get_user_model(), CustomUser) + with self.assertRaises(AttributeError): + User.objects.all() + + @override_settings(AUTH_USER_MODEL='badsetting') + def test_swappable_user_bad_setting(self): + "The alternate user setting must point to something in the format app.model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() + + @override_settings(AUTH_USER_MODEL='thismodel.doesntexist') + def test_swappable_user_nonexistent_model(self): + "The current user model must point to an installed model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() + + def test_user_verbose_names_translatable(self): + "Default User model verbose names are translatable (#19945)" + with translation.override('en'): + self.assertEqual(User._meta.verbose_name, 'user') + self.assertEqual(User._meta.verbose_name_plural, 'users') + with translation.override('es'): + self.assertEqual(User._meta.verbose_name, 'usuario') + self.assertEqual(User._meta.verbose_name_plural, 'usuarios') diff --git a/tests/auth_tests/test_context_processors.py b/tests/auth_tests/test_context_processors.py new file mode 100644 index 0000000000..26d73f2841 --- /dev/null +++ b/tests/auth_tests/test_context_processors.py @@ -0,0 +1,156 @@ +from django.contrib.auth import authenticate +from django.contrib.auth.context_processors import PermLookupDict, PermWrapper +from django.contrib.auth.models import Permission, User +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.test import TestCase, override_settings + +from .settings import AUTH_MIDDLEWARE_CLASSES, AUTH_TEMPLATES + + +class MockUser(object): + def has_module_perms(self, perm): + if perm == 'mockapp': + return True + return False + + def has_perm(self, perm): + if perm == 'mockapp.someperm': + return True + return False + + +class PermWrapperTests(TestCase): + """ + Test some details of the PermWrapper implementation. + """ + class EQLimiterObject(object): + """ + This object makes sure __eq__ will not be called endlessly. + """ + def __init__(self): + self.eq_calls = 0 + + def __eq__(self, other): + if self.eq_calls > 0: + return True + self.eq_calls += 1 + return False + + def test_permwrapper_in(self): + """ + Test that 'something' in PermWrapper works as expected. + """ + perms = PermWrapper(MockUser()) + # Works for modules and full permissions. + self.assertIn('mockapp', perms) + self.assertNotIn('nonexisting', perms) + self.assertIn('mockapp.someperm', perms) + self.assertNotIn('mockapp.nonexisting', perms) + + def test_permlookupdict_in(self): + """ + No endless loops if accessed with 'in' - refs #18979. + """ + pldict = PermLookupDict(MockUser(), 'mockapp') + with self.assertRaises(TypeError): + self.EQLimiterObject() in pldict + + +@override_settings( + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + ROOT_URLCONF='auth_tests.urls', + TEMPLATES=AUTH_TEMPLATES, + USE_TZ=False, # required for loading the fixture +) +class AuthContextProcessorTests(TestCase): + """ + Tests for the ``django.contrib.auth.context_processors.auth`` processor + """ + fixtures = ['context-processors-users.xml'] + + @override_settings(MIDDLEWARE_CLASSES=AUTH_MIDDLEWARE_CLASSES) + def test_session_not_accessed(self): + """ + Tests that the session is not accessed simply by including + the auth context processor + """ + response = self.client.get('/auth_processor_no_attr_access/') + self.assertContains(response, "Session not accessed") + + @override_settings(MIDDLEWARE_CLASSES=AUTH_MIDDLEWARE_CLASSES) + def test_session_is_accessed(self): + """ + Tests that the session is accessed if the auth context processor + is used and relevant attributes accessed. + """ + response = self.client.get('/auth_processor_attr_access/') + self.assertContains(response, "Session accessed") + + def test_perms_attrs(self): + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') + response = self.client.get('/auth_processor_perms/') + self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") + + def test_perm_in_perms_attrs(self): + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') + response = self.client.get('/auth_processor_perm_in_perms/') + self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") + + def test_message_attrs(self): + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_messages/') + self.assertContains(response, "Message 1") + + def test_user_attrs(self): + """ + Test that the lazy objects returned behave just like the wrapped objects. + """ + # These are 'functional' level tests for common use cases. Direct + # testing of the implementation (SimpleLazyObject) is in the 'utils' + # tests. + self.client.login(username='super', password='secret') + user = authenticate(username='super', password='secret') + response = self.client.get('/auth_processor_user/') + self.assertContains(response, "unicode: super") + self.assertContains(response, "id: 100") + self.assertContains(response, "username: super") + # bug #12037 is tested by the {% url %} in the template: + self.assertContains(response, "url: /userpage/super/") + + # See if this object can be used for queries where a Q() comparing + # a user can be used with another Q() (in an AND or OR fashion). + # This simulates what a template tag might do with the user from the + # context. Note that we don't need to execute a query, just build it. + # + # The failure case (bug #12049) on Python 2.4 with a LazyObject-wrapped + # User is a fatal TypeError: "function() takes at least 2 arguments + # (0 given)" deep inside deepcopy(). + # + # Python 2.5 and 2.6 succeeded, but logged internally caught exception + # spew: + # + # Exception RuntimeError: 'maximum recursion depth exceeded while + # calling a Python object' in <type 'exceptions.AttributeError'> + # ignored" + Q(user=response.context['user']) & Q(someflag=True) + + # Tests for user equality. This is hard because User defines + # equality in a non-duck-typing way + # See bug #12060 + self.assertEqual(response.context['user'], user) + self.assertEqual(user, response.context['user']) diff --git a/tests/auth_tests/test_decorators.py b/tests/auth_tests/test_decorators.py new file mode 100644 index 0000000000..ff1b1f52a2 --- /dev/null +++ b/tests/auth_tests/test_decorators.py @@ -0,0 +1,108 @@ +from django.conf import settings +from django.contrib.auth import models +from django.contrib.auth.decorators import login_required, permission_required +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.test import TestCase, override_settings +from django.test.client import RequestFactory + +from .test_views import AuthViewsTestCase + + +@override_settings(ROOT_URLCONF='auth_tests.urls') +class LoginRequiredTestCase(AuthViewsTestCase): + """ + Tests the login_required decorators + """ + + def testCallable(self): + """ + Check that login_required is assignable to callable objects. + """ + class CallableView(object): + def __call__(self, *args, **kwargs): + pass + login_required(CallableView()) + + def testView(self): + """ + Check that login_required is assignable to normal views. + """ + def normal_view(request): + pass + login_required(normal_view) + + def testLoginRequired(self, view_url='/login_required/', login_url=None): + """ + Check that login_required works on a simple view wrapped in a + login_required decorator. + """ + if login_url is None: + login_url = settings.LOGIN_URL + response = self.client.get(view_url) + self.assertEqual(response.status_code, 302) + self.assertIn(login_url, response.url) + self.login() + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + + def testLoginRequiredNextUrl(self): + """ + Check that login_required works on a simple view wrapped in a + login_required decorator with a login_url set. + """ + self.testLoginRequired(view_url='/login_required_login_url/', + login_url='/somewhere/') + + +class PermissionsRequiredDecoratorTest(TestCase): + """ + Tests for the permission_required decorator + """ + def setUp(self): + self.user = models.User.objects.create(username='joe', password='qwerty') + self.factory = RequestFactory() + # Add permissions auth.add_customuser and auth.change_customuser + perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser')) + self.user.user_permissions.add(*perms) + + def test_many_permissions_pass(self): + + @permission_required(['auth.add_customuser', 'auth.change_customuser']) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 200) + + def test_single_permission_pass(self): + + @permission_required('auth.add_customuser') + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 200) + + def test_permissioned_denied_redirect(self): + + @permission_required(['auth.add_customuser', 'auth.change_customuser', 'non-existent-permission']) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 302) + + def test_permissioned_denied_exception_raised(self): + + @permission_required([ + 'auth.add_customuser', 'auth.change_customuser', 'non-existent-permission' + ], raise_exception=True) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + self.assertRaises(PermissionDenied, a_view, request) diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py new file mode 100644 index 0000000000..9d6b48cb69 --- /dev/null +++ b/tests/auth_tests/test_forms.py @@ -0,0 +1,529 @@ +from __future__ import unicode_literals + +import re + +from django import forms +from django.contrib.auth.forms import ( + AuthenticationForm, PasswordChangeForm, PasswordResetForm, + ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, SetPasswordForm, + UserChangeForm, UserCreationForm, +) +from django.contrib.auth.models import User +from django.core import mail +from django.core.mail import EmailMultiAlternatives +from django.forms.fields import CharField, Field +from django.test import TestCase, override_settings +from django.utils import translation +from django.utils.encoding import force_text +from django.utils.text import capfirst +from django.utils.translation import ugettext as _ + +from .settings import AUTH_TEMPLATES + + +@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher']) +class UserCreationFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_user_already_exists(self): + data = { + 'username': 'testclient', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["username"].errors, + [force_text(User._meta.get_field('username').error_messages['unique'])]) + + def test_invalid_data(self): + data = { + 'username': 'jsmith!', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + validator = next(v for v in User._meta.get_field('username').validators if v.code == 'invalid') + self.assertEqual(form["username"].errors, [force_text(validator.message)]) + + def test_password_verification(self): + # The verification password is incorrect. + data = { + 'username': 'jsmith', + 'password1': 'test123', + 'password2': 'test', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["password2"].errors, + [force_text(form.error_messages['password_mismatch'])]) + + def test_both_passwords(self): + # One (or both) passwords weren't given + data = {'username': 'jsmith'} + form = UserCreationForm(data) + required_error = [force_text(Field.default_error_messages['required'])] + self.assertFalse(form.is_valid()) + self.assertEqual(form['password1'].errors, required_error) + self.assertEqual(form['password2'].errors, required_error) + + data['password2'] = 'test123' + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form['password1'].errors, required_error) + self.assertEqual(form['password2'].errors, []) + + def test_success(self): + # The success case. + data = { + 'username': 'jsmith@example.com', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertTrue(form.is_valid()) + u = form.save() + self.assertEqual(repr(u), '<User: jsmith@example.com>') + + +@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher']) +class AuthenticationFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_invalid_username(self): + # The user submits an invalid username. + + data = { + 'username': 'jsmith_does_not_exist', + 'password': 'test123', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [force_text(form.error_messages['invalid_login'] % { + 'username': User._meta.get_field('username').verbose_name + })]) + + def test_inactive_user(self): + # The user is inactive. + data = { + 'username': 'inactive', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [force_text(form.error_messages['inactive'])]) + + def test_inactive_user_i18n(self): + with self.settings(USE_I18N=True), translation.override('pt-br', deactivate=True): + # The user is inactive. + data = { + 'username': 'inactive', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [force_text(form.error_messages['inactive'])]) + + def test_custom_login_allowed_policy(self): + # The user is inactive, but our custom form policy allows them to log in. + data = { + 'username': 'inactive', + 'password': 'password', + } + + class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm): + def confirm_login_allowed(self, user): + pass + + form = AuthenticationFormWithInactiveUsersOkay(None, data) + self.assertTrue(form.is_valid()) + + # If we want to disallow some logins according to custom logic, + # we should raise a django.forms.ValidationError in the form. + class PickyAuthenticationForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if user.username == "inactive": + raise forms.ValidationError("This user is disallowed.") + raise forms.ValidationError("Sorry, nobody's allowed in.") + + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ['This user is disallowed.']) + + data = { + 'username': 'testclient', + 'password': 'password', + } + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."]) + + def test_success(self): + # The success case + data = { + 'username': 'testclient', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertTrue(form.is_valid()) + self.assertEqual(form.non_field_errors(), []) + + def test_username_field_label(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField(label="Name", max_length=75) + + form = CustomAuthenticationForm() + self.assertEqual(form['username'].label, "Name") + + def test_username_field_label_not_set(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField() + + form = CustomAuthenticationForm() + username_field = User._meta.get_field(User.USERNAME_FIELD) + self.assertEqual(form.fields['username'].label, capfirst(username_field.verbose_name)) + + def test_username_field_label_empty_string(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField(label='') + + form = CustomAuthenticationForm() + self.assertEqual(form.fields['username'].label, "") + + +@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher']) +class SetPasswordFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_password_verification(self): + # The two new passwords do not match. + user = User.objects.get(username='testclient') + data = { + 'new_password1': 'abc123', + 'new_password2': 'abc', + } + form = SetPasswordForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["new_password2"].errors, + [force_text(form.error_messages['password_mismatch'])]) + + def test_success(self): + user = User.objects.get(username='testclient') + data = { + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = SetPasswordForm(user, data) + self.assertTrue(form.is_valid()) + + +@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher']) +class PasswordChangeFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_incorrect_password(self): + user = User.objects.get(username='testclient') + data = { + 'old_password': 'test', + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = PasswordChangeForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["old_password"].errors, + [force_text(form.error_messages['password_incorrect'])]) + + def test_password_verification(self): + # The two new passwords do not match. + user = User.objects.get(username='testclient') + data = { + 'old_password': 'password', + 'new_password1': 'abc123', + 'new_password2': 'abc', + } + form = PasswordChangeForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["new_password2"].errors, + [force_text(form.error_messages['password_mismatch'])]) + + def test_success(self): + # The success case. + user = User.objects.get(username='testclient') + data = { + 'old_password': 'password', + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = PasswordChangeForm(user, data) + self.assertTrue(form.is_valid()) + + def test_field_order(self): + # Regression test - check the order of fields: + user = User.objects.get(username='testclient') + self.assertEqual(list(PasswordChangeForm(user, {}).fields), + ['old_password', 'new_password1', 'new_password2']) + + +@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher']) +class UserChangeFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_username_validity(self): + user = User.objects.get(username='testclient') + data = {'username': 'not valid'} + form = UserChangeForm(data, instance=user) + self.assertFalse(form.is_valid()) + validator = next(v for v in User._meta.get_field('username').validators if v.code == 'invalid') + self.assertEqual(form["username"].errors, [force_text(validator.message)]) + + def test_bug_14242(self): + # A regression test, introduce by adding an optimization for the + # UserChangeForm. + + class MyUserForm(UserChangeForm): + def __init__(self, *args, **kwargs): + super(MyUserForm, self).__init__(*args, **kwargs) + self.fields['groups'].help_text = 'These groups give users different permissions' + + class Meta(UserChangeForm.Meta): + fields = ('groups',) + + # Just check we can create it + MyUserForm({}) + + def test_unsuable_password(self): + user = User.objects.get(username='empty_password') + user.set_unusable_password() + user.save() + form = UserChangeForm(instance=user) + self.assertIn(_("No password set."), form.as_table()) + + def test_bug_17944_empty_password(self): + user = User.objects.get(username='empty_password') + form = UserChangeForm(instance=user) + self.assertIn(_("No password set."), form.as_table()) + + def test_bug_17944_unmanageable_password(self): + user = User.objects.get(username='unmanageable_password') + form = UserChangeForm(instance=user) + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) + + def test_bug_17944_unknown_password_algorithm(self): + user = User.objects.get(username='unknown_password') + form = UserChangeForm(instance=user) + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) + + def test_bug_19133(self): + "The change form does not return the password value" + # Use the form to construct the POST data + user = User.objects.get(username='testclient') + form_for_data = UserChangeForm(instance=user) + post_data = form_for_data.initial + + # The password field should be readonly, so anything + # posted here should be ignored; the form will be + # valid, and give back the 'initial' value for the + # password field. + post_data['password'] = 'new password' + form = UserChangeForm(instance=user, data=post_data) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['password'], 'sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161') + + def test_bug_19349_bound_password_field(self): + user = User.objects.get(username='testclient') + form = UserChangeForm(data={}, instance=user) + # When rendering the bound password field, + # ReadOnlyPasswordHashWidget needs the initial + # value to render correctly + self.assertEqual(form.initial['password'], form['password'].value()) + + +@override_settings( + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + TEMPLATES=AUTH_TEMPLATES, + USE_TZ=False, +) +class PasswordResetFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def create_dummy_user(self): + """ + Create a user and return a tuple (user_object, username, email). + """ + username = 'jsmith' + email = 'jsmith@example.com' + user = User.objects.create_user(username, email, 'test123') + return (user, username, email) + + def test_invalid_email(self): + data = {'email': 'not valid'} + form = PasswordResetForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form['email'].errors, [_('Enter a valid email address.')]) + + def test_nonexistent_email(self): + """ + Test nonexistent email address. This should not fail because it would + expose information about registered users. + """ + data = {'email': 'foo@bar.com'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + self.assertEqual(len(mail.outbox), 0) + + def test_cleaned_data(self): + (user, username, email) = self.create_dummy_user() + data = {'email': email} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + form.save(domain_override='example.com') + self.assertEqual(form.cleaned_data['email'], email) + self.assertEqual(len(mail.outbox), 1) + + def test_custom_email_subject(self): + data = {'email': 'testclient@example.com'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + # Since we're not providing a request object, we must provide a + # domain_override to prevent the save operation from failing in the + # potential case where contrib.sites is not installed. Refs #16412. + form.save(domain_override='example.com') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'Custom password reset on example.com') + + def test_custom_email_constructor(self): + data = {'email': 'testclient@example.com'} + + class CustomEmailPasswordResetForm(PasswordResetForm): + def send_mail(self, subject_template_name, email_template_name, + context, from_email, to_email, + html_email_template_name=None): + EmailMultiAlternatives( + "Forgot your password?", + "Sorry to hear you forgot your password.", + None, [to_email], + ['site_monitor@example.com'], + headers={'Reply-To': 'webmaster@example.com'}, + alternatives=[("Really sorry to hear you forgot your password.", + "text/html")]).send() + + form = CustomEmailPasswordResetForm(data) + self.assertTrue(form.is_valid()) + # Since we're not providing a request object, we must provide a + # domain_override to prevent the save operation from failing in the + # potential case where contrib.sites is not installed. Refs #16412. + form.save(domain_override='example.com') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'Forgot your password?') + self.assertEqual(mail.outbox[0].bcc, ['site_monitor@example.com']) + self.assertEqual(mail.outbox[0].content_subtype, "plain") + + def test_preserve_username_case(self): + """ + Preserve the case of the user name (before the @ in the email address) + when creating a user (#5605). + """ + user = User.objects.create_user('forms_test2', 'tesT@EXAMple.com', 'test') + self.assertEqual(user.email, 'tesT@example.com') + user = User.objects.create_user('forms_test3', 'tesT', 'test') + self.assertEqual(user.email, 'tesT') + + def test_inactive_user(self): + """ + Test that inactive user cannot receive password reset email. + """ + (user, username, email) = self.create_dummy_user() + user.is_active = False + user.save() + form = PasswordResetForm({'email': email}) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 0) + + def test_unusable_password(self): + user = User.objects.create_user('testuser', 'test@example.com', 'test') + data = {"email": "test@example.com"} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + user.set_unusable_password() + user.save() + form = PasswordResetForm(data) + # The form itself is valid, but no email is sent + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 0) + + def test_save_plaintext_email(self): + """ + Test the PasswordResetForm.save() method with no html_email_template_name + parameter passed in. + Test to ensure original behavior is unchanged after the parameter was added. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertFalse(message.is_multipart()) + self.assertEqual(message.get_content_type(), 'text/plain') + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(mail.outbox[0].alternatives), 0) + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload())) + + def test_save_html_email_template_name(self): + """ + Test the PasswordResetFOrm.save() method with html_email_template_name + parameter specified. + Test to ensure that a multipart email is sent with both text/plain + and text/html parts. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save(html_email_template_name='registration/html_password_reset_email.html') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox[0].alternatives), 1) + message = mail.outbox[0].message() + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload())) + self.assertTrue( + re.match(r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$', + message.get_payload(1).get_payload()) + ) + + +class ReadOnlyPasswordHashTest(TestCase): + + def test_bug_19349_render_with_none_value(self): + # Rendering the widget with value set to None + # mustn't raise an exception. + widget = ReadOnlyPasswordHashWidget() + html = widget.render(name='password', value=None, attrs={}) + self.assertIn(_("No password set."), html) + + def test_readonly_field_has_changed(self): + field = ReadOnlyPasswordHashField() + self.assertFalse(field.has_changed('aaa', 'bbb')) diff --git a/tests/auth_tests/test_handlers.py b/tests/auth_tests/test_handlers.py new file mode 100644 index 0000000000..e2ceab9c4e --- /dev/null +++ b/tests/auth_tests/test_handlers.py @@ -0,0 +1,77 @@ +from __future__ import unicode_literals + +from django.contrib.auth.handlers.modwsgi import ( + check_password, groups_for_user, +) +from django.contrib.auth.models import Group, User +from django.contrib.auth.tests.custom_user import CustomUser +from django.test import TransactionTestCase, override_settings + + +# This must be a TransactionTestCase because the WSGI auth handler performs +# its own transaction management. +class ModWsgiHandlerTestCase(TransactionTestCase): + """ + Tests for the mod_wsgi authentication handler + """ + + available_apps = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + ] + + def test_check_password(self): + """ + Verify that check_password returns the correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + """ + User.objects.create_user('test', 'test@example.com', 'test') + + # User not in database + self.assertIsNone(check_password({}, 'unknown', '')) + + # Valid user with correct password + self.assertTrue(check_password({}, 'test', 'test')) + + # correct password, but user is inactive + User.objects.filter(username='test').update(is_active=False) + self.assertFalse(check_password({}, 'test', 'test')) + + # Valid user with incorrect password + self.assertFalse(check_password({}, 'test', 'incorrect')) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_check_password_custom_user(self): + """ + Verify that check_password returns the correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + + with custom user installed + """ + + CustomUser._default_manager.create_user('test@example.com', '1990-01-01', 'test') + + # User not in database + self.assertIsNone(check_password({}, 'unknown', '')) + + # Valid user with correct password' + self.assertTrue(check_password({}, 'test@example.com', 'test')) + + # Valid user with incorrect password + self.assertFalse(check_password({}, 'test@example.com', 'incorrect')) + + def test_groups_for_user(self): + """ + Check that groups_for_user returns correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Group_Authorisation + """ + user1 = User.objects.create_user('test', 'test@example.com', 'test') + User.objects.create_user('test1', 'test1@example.com', 'test1') + group = Group.objects.create(name='test_group') + user1.groups.add(group) + + # User not in database + self.assertEqual(groups_for_user({}, 'unknown'), []) + + self.assertEqual(groups_for_user({}, 'test'), [b'test_group']) + self.assertEqual(groups_for_user({}, 'test1'), []) diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py new file mode 100644 index 0000000000..f264926aff --- /dev/null +++ b/tests/auth_tests/test_hashers.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from unittest import skipUnless + +from django.conf.global_settings import PASSWORD_HASHERS +from django.contrib.auth.hashers import ( + UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH, + BasePasswordHasher, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher, + check_password, get_hasher, identify_hasher, is_password_usable, + make_password, +) +from django.test import SimpleTestCase +from django.test.utils import override_settings +from django.utils import six + +try: + import crypt +except ImportError: + crypt = None + +try: + import bcrypt +except ImportError: + bcrypt = None + + +class PBKDF2SingleIterationHasher(PBKDF2PasswordHasher): + iterations = 1 + + +@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS) +class TestUtilsHashPass(SimpleTestCase): + + def test_simple(self): + encoded = make_password('lètmein') + self.assertTrue(encoded.startswith('pbkdf2_sha256$')) + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + # Blank passwords + blank_encoded = make_password('') + self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_pkbdf2(self): + encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256') + self.assertEqual(encoded, + 'pbkdf2_sha256$24000$seasalt$V9DfCAVoweeLwxC/L2mb+7swhzF0XYdyQMqmusZqiTc=') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256") + # Blank passwords + blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256') + self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_sha1(self): + encoded = make_password('lètmein', 'seasalt', 'sha1') + self.assertEqual(encoded, + 'sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "sha1") + # Blank passwords + blank_encoded = make_password('', 'seasalt', 'sha1') + self.assertTrue(blank_encoded.startswith('sha1$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_md5(self): + encoded = make_password('lètmein', 'seasalt', 'md5') + self.assertEqual(encoded, + 'md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "md5") + # Blank passwords + blank_encoded = make_password('', 'seasalt', 'md5') + self.assertTrue(blank_encoded.startswith('md5$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_unsalted_md5(self): + encoded = make_password('lètmein', '', 'unsalted_md5') + self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5") + # Alternate unsalted syntax + alt_encoded = "md5$$%s" % encoded + self.assertTrue(is_password_usable(alt_encoded)) + self.assertTrue(check_password('lètmein', alt_encoded)) + self.assertFalse(check_password('lètmeinz', alt_encoded)) + # Blank passwords + blank_encoded = make_password('', '', 'unsalted_md5') + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_unsalted_sha1(self): + encoded = make_password('lètmein', '', 'unsalted_sha1') + self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1") + # Raw SHA1 isn't acceptable + alt_encoded = encoded[6:] + self.assertFalse(check_password('lètmein', alt_encoded)) + # Blank passwords + blank_encoded = make_password('', '', 'unsalted_sha1') + self.assertTrue(blank_encoded.startswith('sha1$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + @skipUnless(crypt, "no crypt module to generate password.") + def test_crypt(self): + encoded = make_password('lètmei', 'ab', 'crypt') + self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password('lètmei', encoded)) + self.assertFalse(check_password('lètmeiz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "crypt") + # Blank passwords + blank_encoded = make_password('', 'ab', 'crypt') + self.assertTrue(blank_encoded.startswith('crypt$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + @skipUnless(bcrypt, "bcrypt not installed") + def test_bcrypt_sha256(self): + encoded = make_password('lètmein', hasher='bcrypt_sha256') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(encoded.startswith('bcrypt_sha256$')) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256") + + # Verify that password truncation no longer works + password = ('VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5' + 'JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN') + encoded = make_password(password, hasher='bcrypt_sha256') + self.assertTrue(check_password(password, encoded)) + self.assertFalse(check_password(password[:72], encoded)) + # Blank passwords + blank_encoded = make_password('', hasher='bcrypt_sha256') + self.assertTrue(blank_encoded.startswith('bcrypt_sha256$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + @skipUnless(bcrypt, "bcrypt not installed") + def test_bcrypt(self): + encoded = make_password('lètmein', hasher='bcrypt') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(encoded.startswith('bcrypt$')) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt") + # Blank passwords + blank_encoded = make_password('', hasher='bcrypt') + self.assertTrue(blank_encoded.startswith('bcrypt$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) + + def test_unusable(self): + encoded = make_password(None) + self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH) + self.assertFalse(is_password_usable(encoded)) + self.assertFalse(check_password(None, encoded)) + self.assertFalse(check_password(encoded, encoded)) + self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded)) + self.assertFalse(check_password('', encoded)) + self.assertFalse(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertRaises(ValueError, identify_hasher, encoded) + # Assert that the unusable passwords actually contain a random part. + # This might fail one day due to a hash collision. + self.assertNotEqual(encoded, make_password(None), "Random password collision?") + + def test_unspecified_password(self): + """ + Makes sure specifying no plain password with a valid encoded password + returns `False`. + """ + self.assertFalse(check_password(None, make_password('lètmein'))) + + def test_bad_algorithm(self): + with self.assertRaises(ValueError): + make_password('lètmein', hasher='lolcat') + self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash") + + def test_bad_encoded(self): + self.assertFalse(is_password_usable('lètmein_badencoded')) + self.assertFalse(is_password_usable('')) + + def test_low_level_pkbdf2(self): + hasher = PBKDF2PasswordHasher() + encoded = hasher.encode('lètmein', 'seasalt2') + self.assertEqual(encoded, + 'pbkdf2_sha256$24000$seasalt2$TUDkfilKHVC7BkaKSZgIKhm0aTtXlmcw/5C1FeS/DPk=') + self.assertTrue(hasher.verify('lètmein', encoded)) + + def test_low_level_pbkdf2_sha1(self): + hasher = PBKDF2SHA1PasswordHasher() + encoded = hasher.encode('lètmein', 'seasalt2') + self.assertEqual(encoded, + 'pbkdf2_sha1$24000$seasalt2$L37ETdd9trqrsJDwapU3P+2Edhg=') + self.assertTrue(hasher.verify('lètmein', encoded)) + + def test_upgrade(self): + self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) + for algo in ('sha1', 'md5'): + encoded = make_password('lètmein', hasher=algo) + state = {'upgraded': False} + + def setter(password): + state['upgraded'] = True + self.assertTrue(check_password('lètmein', encoded, setter)) + self.assertTrue(state['upgraded']) + + def test_no_upgrade(self): + encoded = make_password('lètmein') + state = {'upgraded': False} + + def setter(): + state['upgraded'] = True + self.assertFalse(check_password('WRONG', encoded, setter)) + self.assertFalse(state['upgraded']) + + def test_no_upgrade_on_incorrect_pass(self): + self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) + for algo in ('sha1', 'md5'): + encoded = make_password('lètmein', hasher=algo) + state = {'upgraded': False} + + def setter(): + state['upgraded'] = True + self.assertFalse(check_password('WRONG', encoded, setter)) + self.assertFalse(state['upgraded']) + + def test_pbkdf2_upgrade(self): + hasher = get_hasher('default') + self.assertEqual('pbkdf2_sha256', hasher.algorithm) + self.assertNotEqual(hasher.iterations, 1) + + old_iterations = hasher.iterations + try: + # Generate a password with 1 iteration. + hasher.iterations = 1 + encoded = make_password('letmein') + algo, iterations, salt, hash = encoded.split('$', 3) + self.assertEqual(iterations, '1') + + state = {'upgraded': False} + + def setter(password): + state['upgraded'] = True + + # Check that no upgrade is triggered + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertFalse(state['upgraded']) + + # Revert to the old iteration count and ... + hasher.iterations = old_iterations + + # ... check if the password would get updated to the new iteration count. + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertTrue(state['upgraded']) + finally: + hasher.iterations = old_iterations + + def test_pbkdf2_upgrade_new_hasher(self): + hasher = get_hasher('default') + self.assertEqual('pbkdf2_sha256', hasher.algorithm) + self.assertNotEqual(hasher.iterations, 1) + + state = {'upgraded': False} + + def setter(password): + state['upgraded'] = True + + with self.settings(PASSWORD_HASHERS=[ + 'auth_tests.test_hashers.PBKDF2SingleIterationHasher']): + encoded = make_password('letmein') + algo, iterations, salt, hash = encoded.split('$', 3) + self.assertEqual(iterations, '1') + + # Check that no upgrade is triggered + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertFalse(state['upgraded']) + + # Revert to the old iteration count and check if the password would get + # updated to the new iteration count. + with self.settings(PASSWORD_HASHERS=[ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'auth_tests.test_hashers.PBKDF2SingleIterationHasher']): + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertTrue(state['upgraded']) + + def test_load_library_no_algorithm(self): + with self.assertRaises(ValueError) as e: + BasePasswordHasher()._load_library() + self.assertEqual("Hasher 'BasePasswordHasher' doesn't specify a " + "library attribute", str(e.exception)) + + def test_load_library_importerror(self): + PlainHasher = type(str('PlainHasher'), (BasePasswordHasher,), + {'algorithm': 'plain', 'library': 'plain'}) + # Python 3.3 adds quotes around module name + with six.assertRaisesRegex(self, ValueError, + "Couldn't load 'PlainHasher' algorithm library: No module named '?plain'?"): + PlainHasher()._load_library() diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py new file mode 100644 index 0000000000..159dc8894d --- /dev/null +++ b/tests/auth_tests/test_management.py @@ -0,0 +1,565 @@ +from __future__ import unicode_literals + +import locale +import sys +from datetime import date + +from django.apps import apps +from django.contrib.auth import management, models +from django.contrib.auth.checks import check_user_model +from django.contrib.auth.management import create_permissions +from django.contrib.auth.management.commands import ( + changepassword, createsuperuser, +) +from django.contrib.auth.models import Group, User +from django.contrib.auth.tests.custom_user import ( + CustomUser, CustomUserBadRequiredFields, CustomUserNonListRequiredFields, + CustomUserNonUniqueUsername, CustomUserWithFK, Email, +) +from django.contrib.contenttypes.models import ContentType +from django.core import checks, exceptions +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase, override_settings, override_system_checks +from django.utils import six +from django.utils.encoding import force_str +from django.utils.translation import ugettext_lazy as _ + + +def mock_inputs(inputs): + """ + Decorator to temporarily replace input/getpass to allow interactive + createsuperuser. + """ + def inner(test_func): + def wrapped(*args): + class mock_getpass: + @staticmethod + def getpass(prompt=b'Password: ', stream=None): + if six.PY2: + # getpass on Windows only supports prompt as bytestring (#19807) + assert isinstance(prompt, six.binary_type) + return inputs['password'] + + def mock_input(prompt): + # prompt should be encoded in Python 2. This line will raise an + # Exception if prompt contains unencoded non-ASCII on Python 2. + prompt = str(prompt) + assert str('__proxy__') not in prompt + response = '' + for key, val in inputs.items(): + if force_str(key) in prompt.lower(): + response = val + break + return response + + old_getpass = createsuperuser.getpass + old_input = createsuperuser.input + createsuperuser.getpass = mock_getpass + createsuperuser.input = mock_input + try: + test_func(*args) + finally: + createsuperuser.getpass = old_getpass + createsuperuser.input = old_input + return wrapped + return inner + + +class MockTTY(object): + """ + A fake stdin object that pretends to be a TTY to be used in conjunction + with mock_inputs. + """ + def isatty(self): + return True + + +class GetDefaultUsernameTestCase(TestCase): + + def setUp(self): + self.old_get_system_username = management.get_system_username + + def tearDown(self): + management.get_system_username = self.old_get_system_username + + def test_actual_implementation(self): + self.assertIsInstance(management.get_system_username(), six.text_type) + + def test_simple(self): + management.get_system_username = lambda: 'joe' + self.assertEqual(management.get_default_username(), 'joe') + + def test_existing(self): + models.User.objects.create(username='joe') + management.get_system_username = lambda: 'joe' + self.assertEqual(management.get_default_username(), '') + self.assertEqual( + management.get_default_username(check_db=False), 'joe') + + def test_i18n(self): + # 'Julia' with accented 'u': + management.get_system_username = lambda: 'J\xfalia' + self.assertEqual(management.get_default_username(), 'julia') + + +class ChangepasswordManagementCommandTestCase(TestCase): + + def setUp(self): + self.user = models.User.objects.create_user(username='joe', password='qwerty') + self.stdout = six.StringIO() + self.stderr = six.StringIO() + + def tearDown(self): + self.stdout.close() + self.stderr.close() + + def test_that_changepassword_command_changes_joes_password(self): + "Executing the changepassword management command should change joe's password" + self.assertTrue(self.user.check_password('qwerty')) + command = changepassword.Command() + command._get_pass = lambda *args: 'not qwerty' + + command.execute(username="joe", stdout=self.stdout) + command_output = self.stdout.getvalue().strip() + + self.assertEqual( + command_output, + "Changing password for user 'joe'\nPassword changed successfully for user 'joe'" + ) + self.assertTrue(models.User.objects.get(username="joe").check_password("not qwerty")) + + def test_that_max_tries_exits_1(self): + """ + A CommandError should be thrown by handle() if the user enters in + mismatched passwords three times. + """ + command = changepassword.Command() + command._get_pass = lambda *args: args or 'foo' + + with self.assertRaises(CommandError): + command.execute(username="joe", stdout=self.stdout, stderr=self.stderr) + + def test_that_changepassword_command_works_with_nonascii_output(self): + """ + #21627 -- Executing the changepassword management command should allow + non-ASCII characters from the User object representation. + """ + # 'Julia' with accented 'u': + models.User.objects.create_user(username='J\xfalia', password='qwerty') + + command = changepassword.Command() + command._get_pass = lambda *args: 'not qwerty' + + command.execute(username="J\xfalia", stdout=self.stdout) + + +@override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342']) # ForeignKey(unique=True) +class CreatesuperuserManagementCommandTestCase(TestCase): + + def test_basic_usage(self): + "Check the operation of the createsuperuser management command" + # We can use the management command to create a superuser + new_io = six.StringIO() + call_command( + "createsuperuser", + interactive=False, + username="joe", + email="joe@somewhere.org", + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = User.objects.get(username="joe") + self.assertEqual(u.email, 'joe@somewhere.org') + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + @mock_inputs({'password': "nopasswd"}) + def test_nolocale(self): + """ + Check that createsuperuser does not break when no locale is set. See + ticket #16017. + """ + + old_getdefaultlocale = locale.getdefaultlocale + try: + # Temporarily remove locale information + locale.getdefaultlocale = lambda: (None, None) + + # Call the command in this new environment + call_command( + "createsuperuser", + interactive=True, + username="nolocale@somewhere.org", + email="nolocale@somewhere.org", + verbosity=0, + stdin=MockTTY(), + ) + + except TypeError: + self.fail("createsuperuser fails if the OS provides no information about the current locale") + + finally: + # Re-apply locale information + locale.getdefaultlocale = old_getdefaultlocale + + # If we were successful, a user should have been created + u = User.objects.get(username="nolocale@somewhere.org") + self.assertEqual(u.email, 'nolocale@somewhere.org') + + @mock_inputs({ + 'password': "nopasswd", + 'u\u017eivatel': 'foo', # username (cz) + 'email': 'nolocale@somewhere.org'}) + def test_non_ascii_verbose_name(self): + username_field = User._meta.get_field('username') + old_verbose_name = username_field.verbose_name + username_field.verbose_name = _('u\u017eivatel') + new_io = six.StringIO() + try: + call_command( + "createsuperuser", + interactive=True, + stdout=new_io, + stdin=MockTTY(), + ) + finally: + username_field.verbose_name = old_verbose_name + + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + + def test_verbosity_zero(self): + # We can suppress output on the management command + new_io = six.StringIO() + call_command( + "createsuperuser", + interactive=False, + username="joe2", + email="joe2@somewhere.org", + verbosity=0, + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, '') + u = User.objects.get(username="joe2") + self.assertEqual(u.email, 'joe2@somewhere.org') + self.assertFalse(u.has_usable_password()) + + def test_email_in_username(self): + new_io = six.StringIO() + call_command( + "createsuperuser", + interactive=False, + username="joe+admin@somewhere.org", + email="joe@somewhere.org", + stdout=new_io + ) + u = User._default_manager.get(username="joe+admin@somewhere.org") + self.assertEqual(u.email, 'joe@somewhere.org') + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = six.StringIO() + call_command( + "createsuperuser", + interactive=False, + email="joe@somewhere.org", + date_of_birth="1976-04-01", + stdout=new_io, + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUser._default_manager.get(email="joe@somewhere.org") + self.assertEqual(u.date_of_birth, date(1976, 4, 1)) + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user_missing_required_field(self): + "A Custom superuser won't be created when a required field isn't provided" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = six.StringIO() + with self.assertRaises(CommandError): + call_command( + "createsuperuser", + interactive=False, + username="joe@somewhere.org", + stdout=new_io, + stderr=new_io, + ) + + self.assertEqual(CustomUser._default_manager.count(), 0) + + def test_skip_if_not_in_TTY(self): + """ + If the command is not called from a TTY, it should be skipped and a + message should be displayed (#7423). + """ + class FakeStdin(object): + """A fake stdin object that has isatty() return False.""" + def isatty(self): + return False + + out = six.StringIO() + call_command( + "createsuperuser", + stdin=FakeStdin(), + stdout=out, + interactive=True, + ) + + self.assertEqual(User._default_manager.count(), 0) + self.assertIn("Superuser creation skipped", out.getvalue()) + + def test_passing_stdin(self): + """ + You can pass a stdin object as an option and it should be + available on self.stdin. + If no such option is passed, it defaults to sys.stdin. + """ + sentinel = object() + command = createsuperuser.Command() + command.check = lambda: [] + command.execute( + stdin=sentinel, + stdout=six.StringIO(), + stderr=six.StringIO(), + interactive=False, + verbosity=0, + username='janet', + email='janet@example.com', + ) + self.assertIs(command.stdin, sentinel) + + command = createsuperuser.Command() + command.check = lambda: [] + command.execute( + stdout=six.StringIO(), + stderr=six.StringIO(), + interactive=False, + verbosity=0, + username='joe', + email='joe@example.com', + ) + self.assertIs(command.stdin, sys.stdin) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserWithFK') + def test_fields_with_fk(self): + new_io = six.StringIO() + group = Group.objects.create(name='mygroup') + email = Email.objects.create(email='mymail@gmail.com') + call_command( + 'createsuperuser', + interactive=False, + username=email.pk, + email=email.email, + group=group.pk, + stdout=new_io, + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUserWithFK._default_manager.get(email=email) + self.assertEqual(u.username, email) + self.assertEqual(u.group, group) + + non_existent_email = 'mymail2@gmail.com' + with self.assertRaisesMessage(CommandError, + 'email instance with email %r does not exist.' % non_existent_email): + call_command( + 'createsuperuser', + interactive=False, + username=email.pk, + email=non_existent_email, + stdout=new_io, + ) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserWithFK') + def test_fields_with_fk_interactive(self): + new_io = six.StringIO() + group = Group.objects.create(name='mygroup') + email = Email.objects.create(email='mymail@gmail.com') + + @mock_inputs({ + 'password': 'nopasswd', + 'username (email.id)': email.pk, + 'email (email.email)': email.email, + 'group (group.id)': group.pk, + }) + def test(self): + call_command( + 'createsuperuser', + interactive=True, + stdout=new_io, + stdin=MockTTY(), + ) + + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUserWithFK._default_manager.get(email=email) + self.assertEqual(u.username, email) + self.assertEqual(u.group, group) + + test(self) + + +class CustomUserModelValidationTestCase(TestCase): + @override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields') + @override_system_checks([check_user_model]) + def test_required_fields_is_list(self): + "REQUIRED_FIELDS should be a list." + errors = checks.run_checks() + expected = [ + checks.Error( + "'REQUIRED_FIELDS' must be a list or tuple.", + hint=None, + obj=CustomUserNonListRequiredFields, + id='auth.E001', + ), + ] + self.assertEqual(errors, expected) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields') + @override_system_checks([check_user_model]) + def test_username_not_in_required_fields(self): + "USERNAME_FIELD should not appear in REQUIRED_FIELDS." + errors = checks.run_checks() + expected = [ + checks.Error( + ("The field named as the 'USERNAME_FIELD' for a custom user model " + "must not be included in 'REQUIRED_FIELDS'."), + hint=None, + obj=CustomUserBadRequiredFields, + id='auth.E002', + ), + ] + self.assertEqual(errors, expected) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername') + @override_system_checks([check_user_model]) + def test_username_non_unique(self): + "A non-unique USERNAME_FIELD should raise a model validation error." + errors = checks.run_checks() + expected = [ + checks.Error( + ("'CustomUserNonUniqueUsername.username' must be " + "unique because it is named as the 'USERNAME_FIELD'."), + hint=None, + obj=CustomUserNonUniqueUsername, + id='auth.E003', + ), + ] + self.assertEqual(errors, expected) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername', + AUTHENTICATION_BACKENDS=[ + 'my.custom.backend', + ]) + @override_system_checks([check_user_model]) + def test_username_non_unique_with_custom_backend(self): + """ A non-unique USERNAME_FIELD should raise an error only if we use the + default authentication backend. Otherwise, an warning should be raised. + """ + errors = checks.run_checks() + expected = [ + checks.Warning( + ("'CustomUserNonUniqueUsername.username' is named as " + "the 'USERNAME_FIELD', but it is not unique."), + hint=('Ensure that your authentication backend(s) can handle ' + 'non-unique usernames.'), + obj=CustomUserNonUniqueUsername, + id='auth.W004', + ) + ] + self.assertEqual(errors, expected) + + +class PermissionTestCase(TestCase): + + def setUp(self): + self._original_permissions = models.Permission._meta.permissions[:] + self._original_default_permissions = models.Permission._meta.default_permissions + self._original_verbose_name = models.Permission._meta.verbose_name + + def tearDown(self): + models.Permission._meta.permissions = self._original_permissions + models.Permission._meta.default_permissions = self._original_default_permissions + models.Permission._meta.verbose_name = self._original_verbose_name + ContentType.objects.clear_cache() + + def test_duplicated_permissions(self): + """ + Test that we show proper error message if we are trying to create + duplicate permissions. + """ + auth_app_config = apps.get_app_config('auth') + + # check duplicated default permission + models.Permission._meta.permissions = [ + ('change_permission', 'Can edit permission (duplicate)')] + six.assertRaisesRegex(self, CommandError, + "The permission codename 'change_permission' clashes with a " + "builtin permission for model 'auth.Permission'.", + create_permissions, auth_app_config, verbosity=0) + + # check duplicated custom permissions + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ('my_custom_permission', 'Some permission with duplicate permission code'), + ] + six.assertRaisesRegex(self, CommandError, + "The permission codename 'my_custom_permission' is duplicated for model " + "'auth.Permission'.", + create_permissions, auth_app_config, verbosity=0) + + # should not raise anything + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ] + create_permissions(auth_app_config, verbosity=0) + + def test_default_permissions(self): + auth_app_config = apps.get_app_config('auth') + + permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission') + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ] + create_permissions(auth_app_config, verbosity=0) + + # add/change/delete permission by default + custom permission + self.assertEqual(models.Permission.objects.filter( + content_type=permission_content_type, + ).count(), 4) + + models.Permission.objects.filter(content_type=permission_content_type).delete() + models.Permission._meta.default_permissions = [] + create_permissions(auth_app_config, verbosity=0) + + # custom permission only since default permissions is empty + self.assertEqual(models.Permission.objects.filter( + content_type=permission_content_type, + ).count(), 1) + + def test_verbose_name_length(self): + auth_app_config = apps.get_app_config('auth') + + permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission') + models.Permission.objects.filter(content_type=permission_content_type).delete() + models.Permission._meta.verbose_name = "some ridiculously long verbose name that is out of control" * 5 + + six.assertRaisesRegex(self, exceptions.ValidationError, + "The verbose_name of auth.permission is longer than 244 characters", + create_permissions, auth_app_config, verbosity=0) diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py new file mode 100644 index 0000000000..44372bce01 --- /dev/null +++ b/tests/auth_tests/test_middleware.py @@ -0,0 +1,49 @@ +from django.contrib.auth.middleware import AuthenticationMiddleware +from django.contrib.auth.models import User +from django.http import HttpRequest +from django.test import TestCase + + +class TestSessionAuthenticationMiddleware(TestCase): + def setUp(self): + self.user_password = 'test_password' + self.user = User.objects.create_user('test_user', + 'test@example.com', + self.user_password) + + self.middleware = AuthenticationMiddleware() + self.assertTrue(self.client.login( + username=self.user.username, + password=self.user_password, + )) + self.request = HttpRequest() + self.request.session = self.client.session + + def test_changed_password_doesnt_invalidate_session(self): + """ + Changing a user's password shouldn't invalidate the session if session + verification isn't activated. + """ + session_key = self.request.session.session_key + self.middleware.process_request(self.request) + self.assertIsNotNone(self.request.user) + self.assertFalse(self.request.user.is_anonymous()) + + # After password change, user should remain logged in. + self.user.set_password('new_password') + self.user.save() + self.middleware.process_request(self.request) + self.assertIsNotNone(self.request.user) + self.assertFalse(self.request.user.is_anonymous()) + self.assertEqual(session_key, self.request.session.session_key) + + def test_changed_password_invalidates_session_with_middleware(self): + with self.modify_settings(MIDDLEWARE_CLASSES={'append': ['django.contrib.auth.middleware.SessionAuthenticationMiddleware']}): + # After password change, user should be anonymous + self.user.set_password('new_password') + self.user.save() + self.middleware.process_request(self.request) + self.assertIsNotNone(self.request.user) + self.assertTrue(self.request.user.is_anonymous()) + # session should be flushed + self.assertIsNone(self.request.session.session_key) diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py new file mode 100644 index 0000000000..66e1d17696 --- /dev/null +++ b/tests/auth_tests/test_models.py @@ -0,0 +1,214 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import ( + AbstractUser, Group, Permission, User, UserManager, +) +# Needed so model is installed when tests are run independently: +from django.contrib.auth.tests.custom_user import IsActiveTestUser1 # NOQA +from django.contrib.contenttypes.models import ContentType +from django.core import mail +from django.db.models.signals import post_save +from django.test import TestCase, override_settings + + +@override_settings(USE_TZ=False) +class NaturalKeysTestCase(TestCase): + fixtures = ['authtestdata.json'] + + def test_user_natural_key(self): + staff_user = User.objects.get(username='staff') + self.assertEqual(User.objects.get_by_natural_key('staff'), staff_user) + self.assertEqual(staff_user.natural_key(), ('staff',)) + + def test_group_natural_key(self): + users_group = Group.objects.create(name='users') + self.assertEqual(Group.objects.get_by_natural_key('users'), users_group) + + +@override_settings(USE_TZ=False) +class LoadDataWithoutNaturalKeysTestCase(TestCase): + fixtures = ['regular.json'] + + def test_user_is_created_and_added_to_group(self): + user = User.objects.get(username='my_username') + group = Group.objects.get(name='my_group') + self.assertEqual(group, user.groups.get()) + + +@override_settings(USE_TZ=False) +class LoadDataWithNaturalKeysTestCase(TestCase): + fixtures = ['natural.json'] + + def test_user_is_created_and_added_to_group(self): + user = User.objects.get(username='my_username') + group = Group.objects.get(name='my_group') + self.assertEqual(group, user.groups.get()) + + +class LoadDataWithNaturalKeysAndMultipleDatabasesTestCase(TestCase): + multi_db = True + + def test_load_data_with_user_permissions(self): + # Create test contenttypes for both databases + default_objects = [ + ContentType.objects.db_manager('default').create( + model='examplemodela', + app_label='app_a', + ), + ContentType.objects.db_manager('default').create( + model='examplemodelb', + app_label='app_b', + ), + ] + other_objects = [ + ContentType.objects.db_manager('other').create( + model='examplemodelb', + app_label='app_b', + ), + ContentType.objects.db_manager('other').create( + model='examplemodela', + app_label='app_a', + ), + ] + + # Now we create the test UserPermission + Permission.objects.db_manager("default").create( + name="Can delete example model b", + codename="delete_examplemodelb", + content_type=default_objects[1], + ) + Permission.objects.db_manager("other").create( + name="Can delete example model b", + codename="delete_examplemodelb", + content_type=other_objects[0], + ) + + perm_default = Permission.objects.get_by_natural_key( + 'delete_examplemodelb', + 'app_b', + 'examplemodelb', + ) + + perm_other = Permission.objects.db_manager('other').get_by_natural_key( + 'delete_examplemodelb', + 'app_b', + 'examplemodelb', + ) + + self.assertEqual(perm_default.content_type_id, default_objects[1].id) + self.assertEqual(perm_other.content_type_id, other_objects[0].id) + + +class UserManagerTestCase(TestCase): + + def test_create_user(self): + email_lowercase = 'normal@normal.com' + user = User.objects.create_user('user', email_lowercase) + self.assertEqual(user.email, email_lowercase) + self.assertEqual(user.username, 'user') + self.assertFalse(user.has_usable_password()) + + def test_create_user_email_domain_normalize_rfc3696(self): + # According to http://tools.ietf.org/html/rfc3696#section-3 + # the "@" symbol can be part of the local part of an email address + returned = UserManager.normalize_email(r'Abc\@DEF@EXAMPLE.com') + self.assertEqual(returned, r'Abc\@DEF@example.com') + + def test_create_user_email_domain_normalize(self): + returned = UserManager.normalize_email('normal@DOMAIN.COM') + self.assertEqual(returned, 'normal@domain.com') + + def test_create_user_email_domain_normalize_with_whitespace(self): + returned = UserManager.normalize_email('email\ with_whitespace@D.COM') + self.assertEqual(returned, 'email\ with_whitespace@d.com') + + def test_empty_username(self): + self.assertRaisesMessage( + ValueError, + 'The given username must be set', + User.objects.create_user, username='' + ) + + +class AbstractUserTestCase(TestCase): + def test_email_user(self): + # valid send_mail parameters + kwargs = { + "fail_silently": False, + "auth_user": None, + "auth_password": None, + "connection": None, + "html_message": None, + } + abstract_user = AbstractUser(email='foo@bar.com') + abstract_user.email_user(subject="Subject here", + message="This is a message", from_email="from@domain.com", **kwargs) + # Test that one message has been sent. + self.assertEqual(len(mail.outbox), 1) + # Verify that test email contains the correct attributes: + message = mail.outbox[0] + self.assertEqual(message.subject, "Subject here") + self.assertEqual(message.body, "This is a message") + self.assertEqual(message.from_email, "from@domain.com") + self.assertEqual(message.to, [abstract_user.email]) + + def test_last_login_default(self): + user1 = User.objects.create(username='user1') + self.assertIsNone(user1.last_login) + + user2 = User.objects.create_user(username='user2') + self.assertIsNone(user2.last_login) + + +class IsActiveTestCase(TestCase): + """ + Tests the behavior of the guaranteed is_active attribute + """ + + def test_builtin_user_isactive(self): + user = User.objects.create(username='foo', email='foo@bar.com') + # is_active is true by default + self.assertEqual(user.is_active, True) + user.is_active = False + user.save() + user_fetched = User.objects.get(pk=user.pk) + # the is_active flag is saved + self.assertFalse(user_fetched.is_active) + + @override_settings(AUTH_USER_MODEL='auth.IsActiveTestUser1') + def test_is_active_field_default(self): + """ + tests that the default value for is_active is provided + """ + UserModel = get_user_model() + user = UserModel(username='foo') + self.assertEqual(user.is_active, True) + # you can set the attribute - but it will not save + user.is_active = False + # there should be no problem saving - but the attribute is not saved + user.save() + user_fetched = UserModel._default_manager.get(pk=user.pk) + # the attribute is always true for newly retrieved instance + self.assertEqual(user_fetched.is_active, True) + + +class TestCreateSuperUserSignals(TestCase): + """ + Simple test case for ticket #20541 + """ + def post_save_listener(self, *args, **kwargs): + self.signals_count += 1 + + def setUp(self): + self.signals_count = 0 + post_save.connect(self.post_save_listener, sender=User) + + def tearDown(self): + post_save.disconnect(self.post_save_listener, sender=User) + + def test_create_user(self): + User.objects.create_user("JohnDoe") + self.assertEqual(self.signals_count, 1) + + def test_create_superuser(self): + User.objects.create_superuser("JohnDoe", "mail@example.com", "1") + self.assertEqual(self.signals_count, 1) diff --git a/tests/auth_tests/test_remote_user.py b/tests/auth_tests/test_remote_user.py new file mode 100644 index 0000000000..d0f3f2283f --- /dev/null +++ b/tests/auth_tests/test_remote_user.py @@ -0,0 +1,234 @@ +from datetime import datetime + +from django.conf import settings +from django.contrib.auth import authenticate +from django.contrib.auth.backends import RemoteUserBackend +from django.contrib.auth.middleware import RemoteUserMiddleware +from django.contrib.auth.models import User +from django.test import TestCase, modify_settings, override_settings +from django.utils import timezone + + +@override_settings(ROOT_URLCONF='auth_tests.urls') +class RemoteUserTest(TestCase): + + middleware = 'django.contrib.auth.middleware.RemoteUserMiddleware' + backend = 'django.contrib.auth.backends.RemoteUserBackend' + header = 'REMOTE_USER' + + # Usernames to be passed in REMOTE_USER for the test_known_user test case. + known_user = 'knownuser' + known_user2 = 'knownuser2' + + def setUp(self): + self.patched_settings = modify_settings( + AUTHENTICATION_BACKENDS={'append': self.backend}, + MIDDLEWARE_CLASSES={'append': self.middleware}, + ) + self.patched_settings.enable() + + def tearDown(self): + self.patched_settings.disable() + + def test_no_remote_user(self): + """ + Tests requests where no remote user is specified and insures that no + users get created. + """ + num_users = User.objects.count() + + response = self.client.get('/remote_user/') + self.assertTrue(response.context['user'].is_anonymous()) + self.assertEqual(User.objects.count(), num_users) + + response = self.client.get('/remote_user/', **{self.header: None}) + self.assertTrue(response.context['user'].is_anonymous()) + self.assertEqual(User.objects.count(), num_users) + + response = self.client.get('/remote_user/', **{self.header: ''}) + self.assertTrue(response.context['user'].is_anonymous()) + self.assertEqual(User.objects.count(), num_users) + + def test_unknown_user(self): + """ + Tests the case where the username passed in the header does not exist + as a User. + """ + num_users = User.objects.count() + response = self.client.get('/remote_user/', **{self.header: 'newuser'}) + self.assertEqual(response.context['user'].username, 'newuser') + self.assertEqual(User.objects.count(), num_users + 1) + User.objects.get(username='newuser') + + # Another request with same user should not create any new users. + response = self.client.get('/remote_user/', **{self.header: 'newuser'}) + self.assertEqual(User.objects.count(), num_users + 1) + + def test_known_user(self): + """ + Tests the case where the username passed in the header is a valid User. + """ + User.objects.create(username='knownuser') + User.objects.create(username='knownuser2') + num_users = User.objects.count() + response = self.client.get('/remote_user/', + **{self.header: self.known_user}) + self.assertEqual(response.context['user'].username, 'knownuser') + self.assertEqual(User.objects.count(), num_users) + # Test that a different user passed in the headers causes the new user + # to be logged in. + response = self.client.get('/remote_user/', + **{self.header: self.known_user2}) + self.assertEqual(response.context['user'].username, 'knownuser2') + self.assertEqual(User.objects.count(), num_users) + + def test_last_login(self): + """ + Tests that a user's last_login is set the first time they make a + request but not updated in subsequent requests with the same session. + """ + user = User.objects.create(username='knownuser') + # Set last_login to something so we can determine if it changes. + default_login = datetime(2000, 1, 1) + if settings.USE_TZ: + default_login = default_login.replace(tzinfo=timezone.utc) + user.last_login = default_login + user.save() + + response = self.client.get('/remote_user/', + **{self.header: self.known_user}) + self.assertNotEqual(default_login, response.context['user'].last_login) + + user = User.objects.get(username='knownuser') + user.last_login = default_login + user.save() + response = self.client.get('/remote_user/', + **{self.header: self.known_user}) + self.assertEqual(default_login, response.context['user'].last_login) + + def test_header_disappears(self): + """ + Tests that a logged in user is logged out automatically when + the REMOTE_USER header disappears during the same browser session. + """ + User.objects.create(username='knownuser') + # Known user authenticates + response = self.client.get('/remote_user/', + **{self.header: self.known_user}) + self.assertEqual(response.context['user'].username, 'knownuser') + # During the session, the REMOTE_USER header disappears. Should trigger logout. + response = self.client.get('/remote_user/') + self.assertEqual(response.context['user'].is_anonymous(), True) + # verify the remoteuser middleware will not remove a user + # authenticated via another backend + User.objects.create_user(username='modeluser', password='foo') + self.client.login(username='modeluser', password='foo') + authenticate(username='modeluser', password='foo') + response = self.client.get('/remote_user/') + self.assertEqual(response.context['user'].username, 'modeluser') + + def test_user_switch_forces_new_login(self): + """ + Tests that if the username in the header changes between requests + that the original user is logged out + """ + User.objects.create(username='knownuser') + # Known user authenticates + response = self.client.get('/remote_user/', + **{self.header: self.known_user}) + self.assertEqual(response.context['user'].username, 'knownuser') + # During the session, the REMOTE_USER changes to a different user. + response = self.client.get('/remote_user/', + **{self.header: "newnewuser"}) + # Ensure that the current user is not the prior remote_user + # In backends that create a new user, username is "newnewuser" + # In backends that do not create new users, it is '' (anonymous user) + self.assertNotEqual(response.context['user'].username, 'knownuser') + + +class RemoteUserNoCreateBackend(RemoteUserBackend): + """Backend that doesn't create unknown users.""" + create_unknown_user = False + + +class RemoteUserNoCreateTest(RemoteUserTest): + """ + Contains the same tests as RemoteUserTest, but using a custom auth backend + class that doesn't create unknown users. + """ + + backend = 'auth_tests.test_remote_user.RemoteUserNoCreateBackend' + + def test_unknown_user(self): + num_users = User.objects.count() + response = self.client.get('/remote_user/', **{self.header: 'newuser'}) + self.assertTrue(response.context['user'].is_anonymous()) + self.assertEqual(User.objects.count(), num_users) + + +class CustomRemoteUserBackend(RemoteUserBackend): + """ + Backend that overrides RemoteUserBackend methods. + """ + + def clean_username(self, username): + """ + Grabs username before the @ character. + """ + return username.split('@')[0] + + def configure_user(self, user): + """ + Sets user's email address. + """ + user.email = 'user@example.com' + user.save() + return user + + +class RemoteUserCustomTest(RemoteUserTest): + """ + Tests a custom RemoteUserBackend subclass that overrides the clean_username + and configure_user methods. + """ + + backend = 'auth_tests.test_remote_user.CustomRemoteUserBackend' + # REMOTE_USER strings with email addresses for the custom backend to + # clean. + known_user = 'knownuser@example.com' + known_user2 = 'knownuser2@example.com' + + def test_known_user(self): + """ + The strings passed in REMOTE_USER should be cleaned and the known users + should not have been configured with an email address. + """ + super(RemoteUserCustomTest, self).test_known_user() + self.assertEqual(User.objects.get(username='knownuser').email, '') + self.assertEqual(User.objects.get(username='knownuser2').email, '') + + def test_unknown_user(self): + """ + The unknown user created should be configured with an email address. + """ + super(RemoteUserCustomTest, self).test_unknown_user() + newuser = User.objects.get(username='newuser') + self.assertEqual(newuser.email, 'user@example.com') + + +class CustomHeaderMiddleware(RemoteUserMiddleware): + """ + Middleware that overrides custom HTTP auth user header. + """ + header = 'HTTP_AUTHUSER' + + +class CustomHeaderRemoteUserTest(RemoteUserTest): + """ + Tests a custom RemoteUserMiddleware subclass with custom HTTP auth user + header. + """ + middleware = ( + 'auth_tests.test_remote_user.CustomHeaderMiddleware' + ) + header = 'HTTP_AUTHUSER' diff --git a/tests/auth_tests/test_signals.py b/tests/auth_tests/test_signals.py new file mode 100644 index 0000000000..893dbad605 --- /dev/null +++ b/tests/auth_tests/test_signals.py @@ -0,0 +1,78 @@ +from django.contrib.auth import signals +from django.contrib.auth.models import User +from django.test import TestCase, override_settings +from django.test.client import RequestFactory + + +@override_settings(USE_TZ=False, + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + ROOT_URLCONF='auth_tests.urls') +class SignalTestCase(TestCase): + fixtures = ['authtestdata.json'] + + def listener_login(self, user, **kwargs): + self.logged_in.append(user) + + def listener_logout(self, user, **kwargs): + self.logged_out.append(user) + + def listener_login_failed(self, sender, credentials, **kwargs): + self.login_failed.append(credentials) + + def setUp(self): + """Set up the listeners and reset the logged in/logged out counters""" + self.logged_in = [] + self.logged_out = [] + self.login_failed = [] + signals.user_logged_in.connect(self.listener_login) + signals.user_logged_out.connect(self.listener_logout) + signals.user_login_failed.connect(self.listener_login_failed) + + def tearDown(self): + """Disconnect the listeners""" + signals.user_logged_in.disconnect(self.listener_login) + signals.user_logged_out.disconnect(self.listener_logout) + signals.user_login_failed.disconnect(self.listener_login_failed) + + def test_login(self): + # Only a successful login will trigger the success signal. + self.client.login(username='testclient', password='bad') + self.assertEqual(len(self.logged_in), 0) + self.assertEqual(len(self.login_failed), 1) + self.assertEqual(self.login_failed[0]['username'], 'testclient') + # verify the password is cleansed + self.assertIn('***', self.login_failed[0]['password']) + + # Like this: + self.client.login(username='testclient', password='password') + self.assertEqual(len(self.logged_in), 1) + self.assertEqual(self.logged_in[0].username, 'testclient') + + # Ensure there were no more failures. + self.assertEqual(len(self.login_failed), 1) + + def test_logout_anonymous(self): + # The log_out function will still trigger the signal for anonymous + # users. + self.client.get('/logout/next_page/') + self.assertEqual(len(self.logged_out), 1) + self.assertEqual(self.logged_out[0], None) + + def test_logout(self): + self.client.login(username='testclient', password='password') + self.client.get('/logout/next_page/') + self.assertEqual(len(self.logged_out), 1) + self.assertEqual(self.logged_out[0].username, 'testclient') + + def test_update_last_login(self): + """Ensure that only `last_login` is updated in `update_last_login`""" + user = User.objects.get(pk=3) + old_last_login = user.last_login + + user.username = "This username shouldn't get saved" + request = RequestFactory().get('/login') + signals.user_logged_in.send(sender=user.__class__, request=request, + user=user) + user = User.objects.get(pk=3) + self.assertEqual(user.username, 'staff') + self.assertNotEqual(user.last_login, old_last_login) diff --git a/tests/auth_tests/test_templates.py b/tests/auth_tests/test_templates.py new file mode 100644 index 0000000000..75c4e8b784 --- /dev/null +++ b/tests/auth_tests/test_templates.py @@ -0,0 +1,57 @@ +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.auth.views import ( + password_change, password_change_done, password_reset, + password_reset_complete, password_reset_confirm, password_reset_done, +) +from django.test import RequestFactory, TestCase, override_settings +from django.utils.encoding import force_bytes, force_text +from django.utils.http import urlsafe_base64_encode + + +@override_settings( + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + ROOT_URLCONF='auth_tests.urls', +) +class AuthTemplateTests(TestCase): + + def test_titles(self): + rf = RequestFactory() + user = User.objects.create_user('jsmith', 'jsmith@example.com', 'pass') + user = authenticate(username=user.username, password='pass') + request = rf.get('/somepath/') + request.user = user + + response = password_reset(request, post_reset_redirect='dummy/') + self.assertContains(response, '<title>Password reset</title>') + self.assertContains(response, '<h1>Password reset</h1>') + + response = password_reset_done(request) + self.assertContains(response, '<title>Password reset sent</title>') + self.assertContains(response, '<h1>Password reset sent</h1>') + + # password_reset_confirm invalid token + response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/') + self.assertContains(response, '<title>Password reset unsuccessful</title>') + self.assertContains(response, '<h1>Password reset unsuccessful</h1>') + + # password_reset_confirm valid token + default_token_generator = PasswordResetTokenGenerator() + token = default_token_generator.make_token(user) + uidb64 = force_text(urlsafe_base64_encode(force_bytes(user.pk))) + response = password_reset_confirm(request, uidb64, token, post_reset_redirect='dummy/') + self.assertContains(response, '<title>Enter new password</title>') + self.assertContains(response, '<h1>Enter new password</h1>') + + response = password_reset_complete(request) + self.assertContains(response, '<title>Password reset complete</title>') + self.assertContains(response, '<h1>Password reset complete</h1>') + + response = password_change(request, post_change_redirect='dummy/') + self.assertContains(response, '<title>Password change</title>') + self.assertContains(response, '<h1>Password change</h1>') + + response = password_change_done(request) + self.assertContains(response, '<title>Password change successful</title>') + self.assertContains(response, '<h1>Password change successful</h1>') diff --git a/tests/auth_tests/test_tokens.py b/tests/auth_tests/test_tokens.py new file mode 100644 index 0000000000..f2b94a35f8 --- /dev/null +++ b/tests/auth_tests/test_tokens.py @@ -0,0 +1,68 @@ +import sys +import unittest +from datetime import date, timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.test import TestCase + + +class TokenGeneratorTest(TestCase): + + def test_make_token(self): + """ + Ensure that we can make a token and that it is valid + """ + user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') + p0 = PasswordResetTokenGenerator() + tk1 = p0.make_token(user) + self.assertTrue(p0.check_token(user, tk1)) + + def test_10265(self): + """ + Ensure that the token generated for a user created in the same request + will work correctly. + """ + # See ticket #10265 + user = User.objects.create_user('comebackkid', 'test3@example.com', 'testpw') + p0 = PasswordResetTokenGenerator() + tk1 = p0.make_token(user) + reload = User.objects.get(username='comebackkid') + tk2 = p0.make_token(reload) + self.assertEqual(tk1, tk2) + + def test_timeout(self): + """ + Ensure we can use the token after n days, but no greater. + """ + # Uses a mocked version of PasswordResetTokenGenerator so we can change + # the value of 'today' + class Mocked(PasswordResetTokenGenerator): + def __init__(self, today): + self._today_val = today + + def _today(self): + return self._today_val + + user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') + p0 = PasswordResetTokenGenerator() + tk1 = p0.make_token(user) + p1 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS)) + self.assertTrue(p1.check_token(user, tk1)) + + p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1)) + self.assertFalse(p2.check_token(user, tk1)) + + @unittest.skipIf(sys.version_info[:2] >= (3, 0), "Unnecessary test with Python 3") + def test_date_length(self): + """ + Make sure we don't allow overly long dates, causing a potential DoS. + """ + user = User.objects.create_user('ima1337h4x0r', 'test4@example.com', 'p4ssw0rd') + p0 = PasswordResetTokenGenerator() + + # This will put a 14-digit base36 timestamp into the token, which is too large. + self.assertRaises(ValueError, + p0._make_token_with_timestamp, + user, 175455491841851871349) diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py new file mode 100644 index 0000000000..628513c60a --- /dev/null +++ b/tests/auth_tests/test_views.py @@ -0,0 +1,906 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import itertools +import re +from importlib import import_module + +from django.apps import apps +from django.conf import settings +from django.contrib.admin.models import LogEntry +from django.contrib.auth import REDIRECT_FIELD_NAME, SESSION_KEY +from django.contrib.auth.forms import ( + AuthenticationForm, PasswordChangeForm, SetPasswordForm, +) +from django.contrib.auth.models import User +# Needed so model is installed when tests are run independently: +from django.contrib.auth.tests.custom_user import CustomUser # NOQA +from django.contrib.auth.views import login as login_view, redirect_to_login +from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.sites.requests import RequestSite +from django.core import mail +from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy +from django.http import HttpRequest, QueryDict +from django.middleware.csrf import CsrfViewMiddleware +from django.test import ( + TestCase, ignore_warnings, modify_settings, override_settings, +) +from django.test.utils import patch_logger +from django.utils.deprecation import RemovedInDjango20Warning +from django.utils.encoding import force_text +from django.utils.http import urlquote +from django.utils.six.moves.urllib.parse import ParseResult, urlparse +from django.utils.translation import LANGUAGE_SESSION_KEY + +from .settings import AUTH_TEMPLATES + + +@override_settings( + LANGUAGES=[ + ('en', 'English'), + ], + LANGUAGE_CODE='en', + TEMPLATES=AUTH_TEMPLATES, + USE_TZ=False, + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + ROOT_URLCONF='auth_tests.urls', +) +class AuthViewsTestCase(TestCase): + """ + Helper base class for all the follow test cases. + """ + fixtures = ['authtestdata.json'] + + def login(self, username='testclient', password='password'): + response = self.client.post('/login/', { + 'username': username, + 'password': password, + }) + self.assertIn(SESSION_KEY, self.client.session) + return response + + def logout(self): + response = self.client.get('/admin/logout/') + self.assertEqual(response.status_code, 200) + self.assertNotIn(SESSION_KEY, self.client.session) + + def assertFormError(self, response, error): + """Assert that error is found in response.context['form'] errors""" + form_errors = list(itertools.chain(*response.context['form'].errors.values())) + self.assertIn(force_text(error), form_errors) + + def assertURLEqual(self, url, expected, parse_qs=False): + """ + Given two URLs, make sure all their components (the ones given by + urlparse) are equal, only comparing components that are present in both + URLs. + If `parse_qs` is True, then the querystrings are parsed with QueryDict. + This is useful if you don't want the order of parameters to matter. + Otherwise, the query strings are compared as-is. + """ + fields = ParseResult._fields + + for attr, x, y in zip(fields, urlparse(url), urlparse(expected)): + if parse_qs and attr == 'query': + x, y = QueryDict(x), QueryDict(y) + if x and y and x != y: + self.fail("%r != %r (%s doesn't match)" % (url, expected, attr)) + + +@override_settings(ROOT_URLCONF='django.contrib.auth.urls') +class AuthViewNamedURLTests(AuthViewsTestCase): + + def test_named_urls(self): + "Named URLs should be reversible" + expected_named_urls = [ + ('login', [], {}), + ('logout', [], {}), + ('password_change', [], {}), + ('password_change_done', [], {}), + ('password_reset', [], {}), + ('password_reset_done', [], {}), + ('password_reset_confirm', [], { + 'uidb64': 'aaaaaaa', + 'token': '1111-aaaaa', + }), + ('password_reset_complete', [], {}), + ] + for name, args, kwargs in expected_named_urls: + try: + reverse(name, args=args, kwargs=kwargs) + except NoReverseMatch: + self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name) + + +class PasswordResetTest(AuthViewsTestCase): + + def test_email_not_found(self): + """If the provided email is not registered, don't raise any error but + also don't send any email.""" + response = self.client.get('/password_reset/') + self.assertEqual(response.status_code, 200) + response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 0) + + def test_email_found(self): + "Email is sent if a valid email address is provided for password reset" + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + self.assertIn("http://", mail.outbox[0].body) + self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + # optional multipart text/html email has been added. Make sure original, + # default functionality is 100% the same + self.assertFalse(mail.outbox[0].message().is_multipart()) + + def test_html_mail_template(self): + """ + A multipart email with text/plain and text/html is sent + if the html_email_template parameter is passed to the view + """ + response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertNotIn('<html>', message.get_payload(0).get_payload()) + self.assertIn('<html>', message.get_payload(1).get_payload()) + + def test_email_found_custom_from(self): + "Email is sent if a valid email address is provided for password reset when a custom from_email is provided." + response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) + + @ignore_warnings(category=RemovedInDjango20Warning) + @override_settings(ALLOWED_HOSTS=['adminsite.com']) + def test_admin_reset(self): + "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override." + response = self.client.post('/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='adminsite.com' + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + self.assertIn("http://adminsite.com", mail.outbox[0].body) + self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + + # Skip any 500 handler action (like sending more mail...) + @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True) + def test_poisoned_http_host(self): + "Poisoned HTTP_HOST headers can't be used for reset emails" + # This attack is based on the way browsers handle URLs. The colon + # should be used to separate the port, but if the URL contains an @, + # the colon is interpreted as part of a username for login purposes, + # making 'evil.com' the request domain. Since HTTP_HOST is used to + # produce a meaningful reset URL, we need to be certain that the + # HTTP_HOST header isn't poisoned. This is done as a check when get_host() + # is invoked, but we check here as a practical consequence. + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post( + '/password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) + + # Skip any 500 handler action (like sending more mail...) + @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True) + def test_poisoned_http_host_admin_site(self): + "Poisoned HTTP_HOST headers can't be used for reset emails on admin views" + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post( + '/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) + + def _test_confirm_start(self): + # Start by creating the email + self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertIsNotNone(urlmatch, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + def test_confirm_invalid(self): + url, path = self._test_confirm_start() + # Let's munge the token in the path, but keep the same length, + # in case the URLconf will reject a different length. + path = path[:-5] + ("0" * 4) + path[-1] + + response = self.client.get(path) + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_invalid_user(self): + # Ensure that we get a 200 response for a non-existent user, not a 404 + response = self.client.get('/reset/123456/1-1/') + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_overflow_user(self): + # Ensure that we get a 200 response for a base36 user id that overflows int + response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/') + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_invalid_post(self): + # Same as test_confirm_invalid, but trying + # to do a POST instead. + url, path = self._test_confirm_start() + path = path[:-5] + ("0" * 4) + path[-1] + + self.client.post(path, { + 'new_password1': 'anewpassword', + 'new_password2': ' anewpassword', + }) + # Check the password has not been changed + u = User.objects.get(email='staffmember@example.com') + self.assertTrue(not u.check_password("anewpassword")) + + def test_confirm_complete(self): + url, path = self._test_confirm_start() + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + # Check the password has been changed + u = User.objects.get(email='staffmember@example.com') + self.assertTrue(u.check_password("anewpassword")) + + # Check we can't use the link again + response = self.client.get(path) + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_different_passwords(self): + url, path = self._test_confirm_start() + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'x'}) + self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) + + def test_reset_redirect_default(self): + response = self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/done/') + + def test_reset_custom_redirect(self): + response = self.client.post('/password_reset/custom_redirect/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_reset_custom_redirect_named(self): + response = self.client.post('/password_reset/custom_redirect/named/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + + def test_confirm_redirect_default(self): + url, path = self._test_confirm_start() + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/reset/done/') + + def test_confirm_redirect_custom(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_confirm_redirect_custom_named(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/named/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + + def test_confirm_display_user_from_form(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + + # #16919 -- The ``password_reset_confirm`` view should pass the user + # object to the ``SetPasswordForm``, even on GET requests. + # For this test, we render ``{{ form.user }}`` in the template + # ``registration/password_reset_confirm.html`` so that we can test this. + username = User.objects.get(email='staffmember@example.com').username + self.assertContains(response, "Hello, %s." % username) + + # However, the view should NOT pass any user object on a form if the + # password reset link was invalid. + response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/') + self.assertContains(response, "Hello, .") + + +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserPasswordResetTest(AuthViewsTestCase): + fixtures = ['custom_user.json'] + + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertIsNotNone(urlmatch, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid_custom_user(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + +class ChangePasswordTest(AuthViewsTestCase): + + def fail_login(self, password='password'): + response = self.client.post('/login/', { + 'username': 'testclient', + 'password': password, + }) + self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % { + 'username': User._meta.get_field('username').verbose_name + }) + + def logout(self): + self.client.get('/logout/') + + def test_password_change_fails_with_invalid_old_password(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'donuts', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect']) + + def test_password_change_fails_with_mismatched_passwords(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'donuts', + }) + self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) + + def test_password_change_succeeds(self): + self.login() + self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.fail_login() + self.login(password='password1') + + def test_password_change_done_succeeds(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_change/done/') + + @override_settings(LOGIN_URL='/login/') + def test_password_change_done_fails(self): + response = self.client.get('/password_change/done/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/login/?next=/password_change/done/') + + def test_password_change_redirect_default(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_change/done/') + + def test_password_change_redirect_custom(self): + self.login() + response = self.client.post('/password_change/custom/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_password_change_redirect_custom_named(self): + self.login() + response = self.client.post('/password_change/custom/named/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + + +@modify_settings(MIDDLEWARE_CLASSES={ + 'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', +}) +class SessionAuthenticationTests(AuthViewsTestCase): + def test_user_password_change_updates_session(self): + """ + #21649 - Ensure contrib.auth.views.password_change updates the user's + session auth hash after a password change so the session isn't logged out. + """ + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + # if the hash isn't updated, retrieving the redirection page will fail. + self.assertRedirects(response, '/password_change/done/') + + +class LoginTest(AuthViewsTestCase): + + def test_current_site_in_context_after_login(self): + response = self.client.get(reverse('login')) + self.assertEqual(response.status_code, 200) + if apps.is_installed('django.contrib.sites'): + Site = apps.get_model('sites.Site') + site = Site.objects.get_current() + self.assertEqual(response.context['site'], site) + self.assertEqual(response.context['site_name'], site.name) + else: + self.assertIsInstance(response.context['site'], RequestSite) + self.assertIsInstance(response.context['form'], AuthenticationForm) + + def test_security_check(self, password='password'): + login_url = reverse('login') + + # Those URLs should not pass the security check + for bad_url in ('http://example.com', + 'http:///example.com', + 'https://example.com', + 'ftp://exampel.com', + '///example.com', + '//example.com', + 'javascript:alert("XSS")'): + + nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { + 'url': login_url, + 'next': REDIRECT_FIELD_NAME, + 'bad_url': urlquote(bad_url), + } + response = self.client.post(nasty_url, { + 'username': 'testclient', + 'password': password, + }) + self.assertEqual(response.status_code, 302) + self.assertNotIn(bad_url, response.url, + "%s should be blocked" % bad_url) + + # These URLs *should* still pass the security check + for good_url in ('/view/?param=http://example.com', + '/view/?param=https://example.com', + '/view?param=ftp://exampel.com', + 'view/?param=//example.com', + 'https://testserver/', + 'HTTPS://testserver/', + '//testserver/', + '/url%20with%20spaces/'): # see ticket #12534 + safe_url = '%(url)s?%(next)s=%(good_url)s' % { + 'url': login_url, + 'next': REDIRECT_FIELD_NAME, + 'good_url': urlquote(good_url), + } + response = self.client.post(safe_url, { + 'username': 'testclient', + 'password': password, + }) + self.assertEqual(response.status_code, 302) + self.assertIn(good_url, response.url, "%s should be allowed" % good_url) + + def test_login_form_contains_request(self): + # 15198 + self.client.post('/custom_requestauth_login/', { + 'username': 'testclient', + 'password': 'password', + }, follow=True) + # the custom authentication form used by this login asserts + # that a request is passed to the form successfully. + + def test_login_csrf_rotate(self, password='password'): + """ + Makes sure that a login rotates the currently-used CSRF token. + """ + # Do a GET to establish a CSRF token + # TestClient isn't used here as we're testing middleware, essentially. + req = HttpRequest() + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["CSRF_COOKIE_USED"] = True + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token1 = csrf_cookie.coded_value + + # Prepare the POST request + req = HttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = token1 + req.method = "POST" + req.POST = {'username': 'testclient', 'password': password, 'csrfmiddlewaretoken': token1} + + # Use POST request to log in + SessionMiddleware().process_request(req) + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["SERVER_NAME"] = "testserver" # Required to have redirect work in login view + req.META["SERVER_PORT"] = 80 + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token2 = csrf_cookie.coded_value + + # Check the CSRF token switched + self.assertNotEqual(token1, token2) + + def test_session_key_flushed_on_login(self): + """ + To avoid reusing another user's session, ensure a new, empty session is + created if the existing session corresponds to a different authenticated + user. + """ + self.login() + original_session_key = self.client.session.session_key + + self.login(username='staff') + self.assertNotEqual(original_session_key, self.client.session.session_key) + + def test_session_key_flushed_on_login_after_password_change(self): + """ + As above, but same user logging in after a password change. + """ + self.login() + original_session_key = self.client.session.session_key + + # If no password change, session key should not be flushed. + self.login() + self.assertEqual(original_session_key, self.client.session.session_key) + + user = User.objects.get(username='testclient') + user.set_password('foobar') + user.save() + + self.login(password='foobar') + self.assertNotEqual(original_session_key, self.client.session.session_key) + + def test_login_session_without_hash_session_key(self): + """ + Session without django.contrib.auth.HASH_SESSION_KEY should login + without an exception. + """ + user = User.objects.get(username='testclient') + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session[SESSION_KEY] = user.id + session.save() + original_session_key = session.session_key + self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key + + self.login() + self.assertNotEqual(original_session_key, self.client.session.session_key) + + +class LoginURLSettings(AuthViewsTestCase): + """Tests for settings.LOGIN_URL.""" + def assertLoginURLEquals(self, url, parse_qs=False): + response = self.client.get('/login_required/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, url, parse_qs=parse_qs) + + @override_settings(LOGIN_URL='/login/') + def test_standard_login_url(self): + self.assertLoginURLEquals('/login/?next=/login_required/') + + @override_settings(LOGIN_URL='login') + def test_named_login_url(self): + self.assertLoginURLEquals('/login/?next=/login_required/') + + @override_settings(LOGIN_URL='http://remote.example.com/login') + def test_remote_login_url(self): + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + + @override_settings(LOGIN_URL='https:///login/') + def test_https_login_url(self): + quoted_next = urlquote('http://testserver/login_required/') + expected = 'https:///login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + + @override_settings(LOGIN_URL='/login/?pretty=1') + def test_login_url_with_querystring(self): + self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True) + + @override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/') + def test_remote_login_url_with_next_querystring(self): + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + + @override_settings(LOGIN_URL=reverse_lazy('login')) + def test_lazy_login_url(self): + self.assertLoginURLEquals('/login/?next=/login_required/') + + +class LoginRedirectUrlTest(AuthViewsTestCase): + """Tests for settings.LOGIN_REDIRECT_URL.""" + def assertLoginRedirectURLEqual(self, url): + response = self.login() + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, url) + + def test_default(self): + self.assertLoginRedirectURLEqual('/accounts/profile/') + + @override_settings(LOGIN_REDIRECT_URL='/custom/') + def test_custom(self): + self.assertLoginRedirectURLEqual('/custom/') + + @override_settings(LOGIN_REDIRECT_URL='password_reset') + def test_named(self): + self.assertLoginRedirectURLEqual('/password_reset/') + + @override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/') + def test_remote(self): + self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/') + + +class RedirectToLoginTests(AuthViewsTestCase): + """Tests for the redirect_to_login view""" + @override_settings(LOGIN_URL=reverse_lazy('login')) + def test_redirect_to_login_with_lazy(self): + login_redirect_response = redirect_to_login(next='/else/where/') + expected = '/login/?next=/else/where/' + self.assertEqual(expected, login_redirect_response.url) + + @override_settings(LOGIN_URL=reverse_lazy('login')) + def test_redirect_to_login_with_lazy_and_unicode(self): + login_redirect_response = redirect_to_login(next='/else/where/झ/') + expected = '/login/?next=/else/where/%E0%A4%9D/' + self.assertEqual(expected, login_redirect_response.url) + + +class LogoutTest(AuthViewsTestCase): + + def confirm_logged_out(self): + self.assertNotIn(SESSION_KEY, self.client.session) + + def test_logout_default(self): + "Logout without next_page option renders the default template" + self.login() + response = self.client.get('/logout/') + self.assertContains(response, 'Logged out') + self.confirm_logged_out() + + def test_14377(self): + # Bug 14377 + self.login() + response = self.client.get('/logout/') + self.assertIn('site', response.context) + + def test_logout_with_overridden_redirect_url(self): + # Bug 11223 + self.login() + response = self.client.get('/logout/next_page/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/somewhere/') + + response = self.client.get('/logout/next_page/?next=/login/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/login/') + + self.confirm_logged_out() + + def test_logout_with_next_page_specified(self): + "Logout with next_page option given redirects to specified resource" + self.login() + response = self.client.get('/logout/next_page/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/somewhere/') + self.confirm_logged_out() + + def test_logout_with_redirect_argument(self): + "Logout with query string redirects to specified resource" + self.login() + response = self.client.get('/logout/?next=/login/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/login/') + self.confirm_logged_out() + + def test_logout_with_custom_redirect_argument(self): + "Logout with custom query string redirects to specified resource" + self.login() + response = self.client.get('/logout/custom_query/?follow=/somewhere/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/somewhere/') + self.confirm_logged_out() + + def test_logout_with_named_redirect(self): + "Logout resolves names or URLs passed as next_page." + self.login() + response = self.client.get('/logout/next_page/named/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + self.confirm_logged_out() + + def test_security_check(self, password='password'): + logout_url = reverse('logout') + + # Those URLs should not pass the security check + for bad_url in ('http://example.com', + 'http:///example.com', + 'https://example.com', + 'ftp://exampel.com', + '///example.com', + '//example.com', + 'javascript:alert("XSS")'): + nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { + 'url': logout_url, + 'next': REDIRECT_FIELD_NAME, + 'bad_url': urlquote(bad_url), + } + self.login() + response = self.client.get(nasty_url) + self.assertEqual(response.status_code, 302) + self.assertNotIn(bad_url, response.url, + "%s should be blocked" % bad_url) + self.confirm_logged_out() + + # These URLs *should* still pass the security check + for good_url in ('/view/?param=http://example.com', + '/view/?param=https://example.com', + '/view?param=ftp://exampel.com', + 'view/?param=//example.com', + 'https://testserver/', + 'HTTPS://testserver/', + '//testserver/', + '/url%20with%20spaces/'): # see ticket #12534 + safe_url = '%(url)s?%(next)s=%(good_url)s' % { + 'url': logout_url, + 'next': REDIRECT_FIELD_NAME, + 'good_url': urlquote(good_url), + } + self.login() + response = self.client.get(safe_url) + self.assertEqual(response.status_code, 302) + self.assertIn(good_url, response.url, "%s should be allowed" % good_url) + self.confirm_logged_out() + + def test_logout_preserve_language(self): + """Check that language stored in session is preserved after logout""" + # Create a new session with language + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session[LANGUAGE_SESSION_KEY] = 'pl' + session.save() + self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key + + self.client.get('/logout/') + self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'pl') + + +# Redirect in test_user_change_password will fail if session auth hash +# isn't updated after password change (#21649) +@modify_settings(MIDDLEWARE_CLASSES={ + 'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', +}) +@override_settings( + PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], + ROOT_URLCONF='auth_tests.urls_admin', +) +class ChangelistTests(AuthViewsTestCase): + + def setUp(self): + # Make me a superuser before logging in. + User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True) + self.login() + self.admin = User.objects.get(pk=1) + + def get_user_data(self, user): + return { + 'username': user.username, + 'password': user.password, + 'email': user.email, + 'is_active': user.is_active, + 'is_staff': user.is_staff, + 'is_superuser': user.is_superuser, + 'last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'last_login_1': user.last_login.strftime('%H:%M:%S'), + 'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'initial-last_login_1': user.last_login.strftime('%H:%M:%S'), + 'date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'first_name': user.first_name, + 'last_name': user.last_name, + } + + # #20078 - users shouldn't be allowed to guess password hashes via + # repeated password__startswith queries. + def test_changelist_disallows_password_lookups(self): + # A lookup that tries to filter on password isn't OK + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: + response = self.client.get(reverse('auth_test_admin:auth_user_changelist') + '?password__startswith=sha1$') + self.assertEqual(response.status_code, 400) + self.assertEqual(len(logger_calls), 1) + + def test_user_change_email(self): + data = self.get_user_data(self.admin) + data['email'] = 'new_' + data['email'] + response = self.client.post( + reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,)), + data + ) + self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist')) + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed email.') + + def test_user_not_change(self): + response = self.client.post( + reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,)), + self.get_user_data(self.admin) + ) + self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist')) + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'No fields changed.') + + def test_user_change_password(self): + response = self.client.post( + reverse('auth_test_admin:auth_user_password_change', args=(self.admin.pk,)), + { + 'password1': 'password1', + 'password2': 'password1', + } + ) + self.assertRedirects(response, reverse('auth_test_admin:auth_user_change', args=(self.admin.pk,))) + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed password.') + self.logout() + self.login(password='password1') + + def test_user_change_different_user_password(self): + u = User.objects.get(email='staffmember@example.com') + response = self.client.post( + reverse('auth_test_admin:auth_user_password_change', args=(u.pk,)), + { + 'password1': 'password1', + 'password2': 'password1', + } + ) + self.assertRedirects(response, reverse('auth_test_admin:auth_user_change', args=(u.pk,))) + row = LogEntry.objects.latest('id') + self.assertEqual(row.user_id, self.admin.pk) + self.assertEqual(row.object_id, str(u.pk)) + self.assertEqual(row.change_message, 'Changed password.') diff --git a/tests/auth_tests/urls.py b/tests/auth_tests/urls.py new file mode 100644 index 0000000000..6f83e0f420 --- /dev/null +++ b/tests/auth_tests/urls.py @@ -0,0 +1,101 @@ +from django.conf.urls import include, url +from django.contrib import admin +from django.contrib.auth import views +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.urls import urlpatterns +from django.contrib.messages.api import info +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from django.template import RequestContext, Template +from django.views.decorators.cache import never_cache + + +class CustomRequestAuthenticationForm(AuthenticationForm): + def __init__(self, request, *args, **kwargs): + assert isinstance(request, HttpRequest) + super(CustomRequestAuthenticationForm, self).__init__(request, *args, **kwargs) + + +@never_cache +def remote_user_auth_view(request): + "Dummy view for remote user tests" + t = Template("Username is {{ user }}.") + c = RequestContext(request, {}) + return HttpResponse(t.render(c)) + + +def auth_processor_no_attr_access(request): + render(request, 'context_processors/auth_attrs_no_access.html') + # *After* rendering, we check whether the session was accessed + return render(request, + 'context_processors/auth_attrs_test_access.html', + {'session_accessed': request.session.accessed}) + + +def auth_processor_attr_access(request): + render(request, 'context_processors/auth_attrs_access.html') + return render(request, + 'context_processors/auth_attrs_test_access.html', + {'session_accessed': request.session.accessed}) + + +def auth_processor_user(request): + return render(request, 'context_processors/auth_attrs_user.html') + + +def auth_processor_perms(request): + return render(request, 'context_processors/auth_attrs_perms.html') + + +def auth_processor_perm_in_perms(request): + return render(request, 'context_processors/auth_attrs_perm_in_perms.html') + + +def auth_processor_messages(request): + info(request, "Message 1") + return render(request, 'context_processors/auth_attrs_messages.html') + + +def userpage(request): + pass + + +def custom_request_auth_login(request): + return views.login(request, authentication_form=CustomRequestAuthenticationForm) + +# special urls for auth test cases +urlpatterns += [ + url(r'^logout/custom_query/$', views.logout, dict(redirect_field_name='follow')), + url(r'^logout/next_page/$', views.logout, dict(next_page='/somewhere/')), + url(r'^logout/next_page/named/$', views.logout, dict(next_page='password_reset')), + url(r'^remote_user/$', remote_user_auth_view), + url(r'^password_reset_from_email/$', views.password_reset, dict(from_email='staffmember@example.com')), + url(r'^password_reset/custom_redirect/$', views.password_reset, dict(post_reset_redirect='/custom/')), + url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')), + url(r'^password_reset/html_email_template/$', views.password_reset, + dict(html_email_template_name='registration/html_password_reset_email.html')), + url(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + views.password_reset_confirm, + dict(post_reset_redirect='/custom/')), + url(r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + views.password_reset_confirm, + dict(post_reset_redirect='password_reset')), + url(r'^password_change/custom/$', views.password_change, dict(post_change_redirect='/custom/')), + url(r'^password_change/custom/named/$', views.password_change, dict(post_change_redirect='password_reset')), + url(r'^admin_password_reset/$', views.password_reset, dict(is_admin_site=True)), + url(r'^login_required/$', login_required(views.password_reset)), + url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')), + + url(r'^auth_processor_no_attr_access/$', auth_processor_no_attr_access), + url(r'^auth_processor_attr_access/$', auth_processor_attr_access), + url(r'^auth_processor_user/$', auth_processor_user), + url(r'^auth_processor_perms/$', auth_processor_perms), + url(r'^auth_processor_perm_in_perms/$', auth_processor_perm_in_perms), + url(r'^auth_processor_messages/$', auth_processor_messages), + url(r'^custom_request_auth_login/$', custom_request_auth_login), + url(r'^userpage/(.+)/$', userpage, name="userpage"), + + # This line is only required to render the password reset with is_admin=True + url(r'^admin/', include(admin.site.urls)), +] diff --git a/tests/auth_tests/urls_admin.py b/tests/auth_tests/urls_admin.py new file mode 100644 index 0000000000..8d2fe3fd10 --- /dev/null +++ b/tests/auth_tests/urls_admin.py @@ -0,0 +1,18 @@ +""" +Test URLs for auth admins. +""" + +from django.conf.urls import include, url +from django.contrib import admin +from django.contrib.auth.admin import GroupAdmin, UserAdmin +from django.contrib.auth.models import Group, User +from django.contrib.auth.urls import urlpatterns + +# Create a silo'd admin site for just the user/group admins. +site = admin.AdminSite(name='auth_test_admin') +site.register(User, UserAdmin) +site.register(Group, GroupAdmin) + +urlpatterns += [ + url(r'^admin/', include(site.urls)), +] |
