test_password_history.py 9.63 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
# -*- coding: utf-8 -*-
"""
This test file will verify proper password history enforcement
"""
from django.test import TestCase
from django.utils import timezone
from mock import patch
from student.tests.factories import UserFactory, AdminFactory

from student.models import PasswordHistory
from freezegun import freeze_time
from datetime import timedelta

from django.test.utils import override_settings


@patch.dict("django.conf.settings.FEATURES", {'ADVANCED_SECURITY': True})
class TestPasswordHistory(TestCase):
    """
    All the tests that assert proper behavior regarding password history
    """

    def _change_password(self, user, password):
        """
        Helper method to change password on user and record in the PasswordHistory
        """
        user.set_password(password)
        user.save()
        history = PasswordHistory()
        history.create(user)

    def _user_factory_with_history(self, is_staff=False, set_initial_history=True):
        """
        Helper method to generate either an Admin or a User
        """
        if is_staff:
            user = AdminFactory()
        else:
            user = UserFactory()

        user.date_joined = timezone.now()

        if set_initial_history:
            history = PasswordHistory()
            history.create(user)

        return user

    @patch.dict("django.conf.settings.FEATURES", {'ADVANCED_SECURITY': False})
    def test_disabled_feature(self):
        """
        Test that behavior is normal when this feature is not turned on
        """
        user = UserFactory()
        staff = AdminFactory()

        # if feature is disabled user can keep reusing same password
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "test"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "test"))

        self.assertFalse(PasswordHistory.should_user_reset_password_now(user))
        self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))

    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE': 2})
    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE': 1})
    def test_accounts_password_reuse(self):
        """
        Assert against the password reuse policy
        """
        user = self._user_factory_with_history()
        staff = self._user_factory_with_history(is_staff=True)

        # students need to user at least one different passwords before reuse
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(user, "test"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "different"))
        self._change_password(user, "different")

        self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "test"))

        # staff needs to use at least two different passwords before reuse
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "different"))
        self._change_password(staff, "different")

        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test"))
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "different"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "third"))
        self._change_password(staff, "third")

        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "test"))

92
    @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.PBKDF2PasswordHasher',))
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE': 2})
    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE': 1})
    def test_pbkdf2_sha256_password_reuse(self):
        """
        Assert against the password reuse policy but using the normal Django PBKDF2
        """
        user = self._user_factory_with_history()
        staff = self._user_factory_with_history(is_staff=True)

        # students need to user at least one different passwords before reuse
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(user, "test"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "different"))
        self._change_password(user, "different")

        self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "test"))

        # staff needs to use at least two different passwords before reuse
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "different"))
        self._change_password(staff, "different")

        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test"))
        self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "different"))
        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "third"))
        self._change_password(staff, "third")

        self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "test"))

    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STAFF_ACCOUNTS_PASSWORD_RESETS': 1})
    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': 5})
    def test_forced_password_change(self):
        """
        Assert when passwords must be reset
        """
        student = self._user_factory_with_history()
        staff = self._user_factory_with_history(is_staff=True)
        grandfathered_student = self._user_factory_with_history(set_initial_history=False)

        self.assertFalse(PasswordHistory.should_user_reset_password_now(student))
        self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))
        self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student))

        staff_reset_time = timezone.now() + timedelta(days=1)
        with freeze_time(staff_reset_time):
            self.assertFalse(PasswordHistory.should_user_reset_password_now(student))
            self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student))
            self.assertTrue(PasswordHistory.should_user_reset_password_now(staff))

            self._change_password(staff, 'Different')
            self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))

        student_reset_time = timezone.now() + timedelta(days=5)

        with freeze_time(student_reset_time):
            self.assertTrue(PasswordHistory.should_user_reset_password_now(student))
            self.assertTrue(PasswordHistory.should_user_reset_password_now(grandfathered_student))
            self.assertTrue(PasswordHistory.should_user_reset_password_now(staff))

            self._change_password(student, 'Different')
            self.assertFalse(PasswordHistory.should_user_reset_password_now(student))

            self._change_password(grandfathered_student, 'Different')
            self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student))

            self._change_password(staff, 'Different')
            self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))

    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STAFF_ACCOUNTS_PASSWORD_RESETS': None})
    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': None})
    def test_no_forced_password_change(self):
        """
        Assert that if we skip configuration, then user will never have to force reset password
        """
        student = self._user_factory_with_history()
        staff = self._user_factory_with_history(is_staff=True)

        # also create a user who doesn't have any history
        grandfathered_student = UserFactory()
        grandfathered_student.date_joined = timezone.now()

        self.assertFalse(PasswordHistory.should_user_reset_password_now(student))
        self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))
        self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student))

        staff_reset_time = timezone.now() + timedelta(days=100)
        with freeze_time(staff_reset_time):
            self.assertFalse(PasswordHistory.should_user_reset_password_now(student))
            self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student))
            self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))

    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
    def test_too_frequent_password_resets(self):
        """
        Assert that a user should not be able to password reset too frequently
        """
        student = self._user_factory_with_history()
        grandfathered_student = self._user_factory_with_history(set_initial_history=False)

        self.assertTrue(PasswordHistory.is_password_reset_too_soon(student))
        self.assertFalse(PasswordHistory.is_password_reset_too_soon(grandfathered_student))

        staff_reset_time = timezone.now() + timedelta(days=100)
        with freeze_time(staff_reset_time):
            self.assertFalse(PasswordHistory.is_password_reset_too_soon(student))

    @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': None})
    def test_disabled_too_frequent_password_resets(self):
        """
        Verify properly default behavior when feature is disabled
        """
        student = self._user_factory_with_history()

        self.assertFalse(PasswordHistory.is_password_reset_too_soon(student))