Example #1
0
class TestUsernamePasswordNonmatch(TestCase):
    """
    Test that registration username and password fields differ
    """
    def setUp(self):
        super(TestUsernamePasswordNonmatch, self).setUp()
        self.url = reverse('create_account')

        self.url_params = {
            'username': '******',
            'email': '*****@*****.**',
            'name': 'username',
            'terms_of_service': 'true',
            'honor_code': 'true',
        }

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'
        )
    ])
    def test_with_username_password_match(self):
        self.url_params['username'] = "******"
        self.url_params['password'] = "******"
        response = self.client.post(self.url, self.url_params)
        self.assertEqual(response.status_code, 400)
        obj = json.loads(response.content.decode('utf-8'))
        self.assertEqual(
            obj['password'][0]['user_message'],
            "The password is too similar to the username.",
        )

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'
        )
    ])
    def test_with_username_password_nonmatch(self):
        self.url_params['username'] = "******"
        self.url_params['password'] = "******"
        response = self.client.post(self.url, self.url_params)
        self.assertEqual(response.status_code, 200)
        obj = json.loads(response.content.decode('utf-8'))
        self.assertTrue(obj['success'])
Example #2
0
class TestPasswordPolicy(TestCase):
    """
    Go through some password policy tests to make sure things are properly working
    """
    def setUp(self):
        super(TestPasswordPolicy, self).setUp()  # lint-amnesty, pylint: disable=super-with-arguments
        self.url = reverse('create_account')
        self.request_factory = RequestFactory()
        self.url_params = {
            'username': '******',
            'email': '*****@*****.**',
            'name': 'username',
            'terms_of_service': 'true',
            'honor_code': 'true',
        }

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 6})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_length_too_short(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0]['user_message'] ==\
               'This password is too short. It must contain at least 6 characters.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 6})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_length_long_enough(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
            {'max_length': 12})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_length_too_long(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0]['user_message'] ==\
               'This password is too long. It must contain no more than 12 characters.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.UppercaseValidator',
            {'min_upper': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_not_enough_uppercase(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password must contain at least 3 uppercase letters.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.UppercaseValidator',
            {'min_upper': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_enough_uppercase(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.LowercaseValidator',
            {'min_lower': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_not_enough_lowercase(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password must contain at least 3 lowercase letters.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.LowercaseValidator',
            {'min_lower': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_password_enough_lowercase(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.PunctuationValidator',
            {'min_punctuation': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_not_enough_punctuations(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password must contain at least 3 punctuation marks.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.PunctuationValidator',
            {'min_punctuation': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_enough_punctuations(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.NumericValidator',
            {'min_numeric': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_not_enough_numeric_characters(self):
        # The unicode ២ is the number 2 in Khmer and the ٧ is the Arabic-Indic number 7
        self.url_params['password'] = u'thisShouldFail២٧'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password must contain at least 3 numbers.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.NumericValidator',
            {'min_numeric': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_enough_numeric_characters(self):
        # The unicode ២ is the number 2 in Khmer
        self.url_params['password'] = u'thisShouldPass២33'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
            {'min_alphabetic': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_not_enough_alphabetic_characters(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password must contain at least 3 letters.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
            {'min_alphabetic': 3})  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_enough_alphabetic_characters(self):
        self.url_params['password'] = u'𝒯𝓗Ï𝓼𝒫å𝓼𝓼𝔼𝓼'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.UppercaseValidator',
            {'min_upper': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.NumericValidator',
            {'min_numeric': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.PunctuationValidator',
            {'min_punctuation': 3}),  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_multiple_errors_fail(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        error_strings = [
            "This password must contain at least 3 uppercase letters.",
            "This password must contain at least 3 numbers.",
            "This password must contain at least 3 punctuation marks.",
        ]
        for i in range(3):
            assert obj['password'][i]['user_message'] == error_strings[i]

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.UppercaseValidator',
            {'min_upper': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.LowercaseValidator',
            {'min_lower': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.NumericValidator',
            {'min_numeric': 3}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.PunctuationValidator',
            {'min_punctuation': 3}),  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_multiple_errors_pass(self):
        self.url_params['password'] = u'tH1s Sh0u!d P3#$!'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.CommonPasswordValidator')
    ])
    def test_common_password_fail(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 400
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['password'][0][
            'user_message'] == 'This password is too common.'

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.CommonPasswordValidator')
    ])
    def test_common_password_pass(self):
        self.url_params['password'] = '******'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 6}),  # lint-amnesty, pylint: disable=line-too-long
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
            {'max_length': 75}),  # lint-amnesty, pylint: disable=line-too-long
    ])
    def test_with_unicode(self):
        self.url_params['password'] = u'四節比分和七年前'
        response = self.client.post(self.url, self.url_params)
        assert response.status_code == 200
        obj = json.loads(response.content.decode('utf-8'))
        assert obj['success']
Example #3
0
class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
    """
    Tests that clicking reset password sends email, and doesn't activate the user
    """
    request_factory = RequestFactory()
    ENABLED_CACHES = ['default']

    def setUp(self):  # pylint: disable=arguments-differ
        super(ResetPasswordTests, self).setUp(
            'openedx.core.djangoapps.user_authn.views.password_reset.tracker')
        self.user = UserFactory.create()
        self.user.is_active = False
        self.user.save()
        self.token = default_token_generator.make_token(self.user)
        self.uidb36 = int_to_base36(self.user.id)

        self.user_bad_passwd = UserFactory.create()
        self.user_bad_passwd.is_active = False
        self.user_bad_passwd.password = UNUSABLE_PASSWORD_PREFIX
        self.user_bad_passwd.save()

    def setup_request_session_with_token(self, request):
        """
        Internal helper to setup request session and add token in session.
        """
        process_request(request)
        request.session[INTERNAL_RESET_SESSION_TOKEN] = self.token

    @property
    def password_reset_confirm_url(self):
        """
        Returns Password reset confirm URL
        """
        return reverse("password_reset_confirm",
                       kwargs={
                           "uidb36": self.uidb36,
                           "token": self.token
                       })

    def send_password_reset_request(self):
        """
        Sends GET request on password reset url.
        """
        request = self.request_factory.get(self.password_reset_confirm_url)
        self.setup_request_session_with_token(request)
        return request

    @patch(
        'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
        Mock(side_effect=mock_render_to_string, autospec=True))
    def test_user_bad_password_reset(self):
        """
        Tests password reset behavior for user with password marked UNUSABLE_PASSWORD_PREFIX
        """

        bad_pwd_req = self.request_factory.post(
            '/password_reset/', {'email': self.user_bad_passwd.email})
        bad_pwd_req.user = AnonymousUser()
        bad_pwd_resp = password_reset(bad_pwd_req)
        # If they've got an unusable password, we return a successful response code
        self.assertEqual(bad_pwd_resp.status_code, 200)
        obj = json.loads(bad_pwd_resp.content.decode('utf-8'))
        self.assertEqual(
            obj, {
                'success': True,
                'value': "('registration/password_reset_done.html', [])",
            })
        self.assert_no_events_were_emitted()

    @patch(
        'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
        Mock(side_effect=mock_render_to_string, autospec=True))
    def test_nonexist_email_password_reset(self):
        """
        Now test the exception cases with of reset_password called with invalid email.
        """

        bad_email_req = self.request_factory.post(
            '/password_reset/', {'email': self.user.email + "makeItFail"})
        bad_email_req.user = AnonymousUser()
        bad_email_resp = password_reset(bad_email_req)
        # Note: even if the email is bad, we return a successful response code
        # This prevents someone potentially trying to "brute-force" find out which
        # emails are and aren't registered with edX
        self.assertEqual(bad_email_resp.status_code, 200)
        obj = json.loads(bad_email_resp.content.decode('utf-8'))
        self.assertEqual(
            obj, {
                'success': True,
                'value': "('registration/password_reset_done.html', [])",
            })
        self.assert_no_events_were_emitted()

    @patch(
        'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
        Mock(side_effect=mock_render_to_string, autospec=True))
    def test_password_reset_ratelimited_for_non_existing_user(self):
        """
        Test that reset password endpoint only allow one request per minute
        for non-existing user.
        """
        self.assert_password_reset_ratelimitted('*****@*****.**',
                                                AnonymousUser())
        self.assert_no_events_were_emitted()

    @patch(
        'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
        Mock(side_effect=mock_render_to_string, autospec=True))
    def test_password_reset_ratelimited_for_existing_user(self):
        """
        Test that reset password endpoint only allow one request per minute
        for existing user.
        """
        self.assert_password_reset_ratelimitted(self.user.email, self.user)
        self.assert_event_emission_count(SETTING_CHANGE_INITIATED, 1)

    def assert_password_reset_ratelimitted(self, email, user):
        """
        Assert that password reset endpoint allow one request per minute per email.
        """
        cache.clear()
        password_reset_req = self.request_factory.post('/password_reset/',
                                                       {'email': email})
        password_reset_req.user = user
        password_reset_req.site = Mock(domain='example.com')
        good_resp = password_reset(password_reset_req)
        self.assertEqual(good_resp.status_code, 200)

        # then the rate limiter should kick in and give a HttpForbidden response
        bad_resp = password_reset(password_reset_req)
        self.assertEqual(bad_resp.status_code, 403)

        cache.clear()

    def assert_email_sent_successfully(self, expected):
        """
        Verify that the password confirm email has been sent to the user.
        """
        from_email = configuration_helpers.get_value(
            'email_from_address', settings.DEFAULT_FROM_EMAIL)
        sent_message = mail.outbox[0]
        body = sent_message.body

        self.assertIn(expected['subject'], sent_message.subject)
        self.assertIn(expected['body'], body)
        self.assertEqual(sent_message.from_email, from_email)
        self.assertEqual(len(sent_message.to), 1)
        self.assertIn(self.user.email, sent_message.to)

    def test_ratelimitted_from_same_ip_with_different_email(self):
        """
        Test that password reset endpoint allow only one request per minute per IP.
        """
        cache.clear()
        good_req = self.request_factory.post(
            '/password_reset/', {'email': '*****@*****.**'})
        good_req.user = AnonymousUser()
        good_resp = password_reset(good_req)
        self.assertEqual(good_resp.status_code, 200)

        # change the email ID and verify that the rate limiter should kick in and
        # give a Forbidden response if the request is from same IP.
        bad_req = self.request_factory.post(
            '/password_reset/', {'email': '*****@*****.**'})
        bad_req.user = AnonymousUser()
        bad_resp = password_reset(bad_req)
        self.assertEqual(bad_resp.status_code, 403)

        cache.clear()

    def test_ratelimited_from_different_ips_with_same_email(self):
        """
        Test that password reset endpoint allow only two requests per hour
        per email address.
        """
        cache.clear()
        self.request_password_reset(200)
        # now reset the time to 1 min from now in future and change the email and
        # verify that it will allow another request from same IP
        reset_time = datetime.now(UTC) + timedelta(seconds=61)
        with freeze_time(reset_time):
            for status in [200, 403]:
                self.request_password_reset(status)

            # Even changing the IP will not allow more than two requests for same email.
            new_ip = "8.8.8.8"
            self.request_password_reset(403, new_ip=new_ip)

        cache.clear()

    def request_password_reset(self, status, new_ip=None):
        extra_args = {}
        if new_ip:
            extra_args = {'REMOTE_ADDR': new_ip}

        reset_request = self.request_factory.post(
            '/password_reset/', {'email': '*****@*****.**'},
            **extra_args)

        if new_ip:
            self.assertEqual(reset_request.META.get('REMOTE_ADDR'), new_ip)

        reset_request.user = AnonymousUser()
        response = password_reset(reset_request)
        self.assertEqual(response.status_code, status)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls',
                         "Test only valid in LMS")
    @ddt.data((
        'plain_text',
        "You're receiving this e-mail because you requested a password reset"
    ), ('html',
        "You're receiving this e-mail because you requested a password reset"
        ))
    @ddt.unpack
    def test_reset_password_email(self, body_type, expected_output):
        """Tests contents of reset password email, and that user is not active"""
        good_req = self.request_factory.post('/password_reset/',
                                             {'email': self.user.email})
        good_req.user = self.user
        good_req.site = Mock(domain='example.com')
        dot_application = dot_factories.ApplicationFactory(user=self.user)
        dot_access_token = dot_factories.AccessTokenFactory(
            user=self.user, application=dot_application)
        dot_factories.RefreshTokenFactory(user=self.user,
                                          application=dot_application,
                                          access_token=dot_access_token)
        good_resp = password_reset(good_req)
        self.assertEqual(good_resp.status_code, 200)
        self.assertFalse(
            dot_models.AccessToken.objects.filter(user=self.user).exists())
        self.assertFalse(
            dot_models.RefreshToken.objects.filter(user=self.user).exists())
        obj = json.loads(good_resp.content.decode('utf-8'))
        self.assertTrue(obj['success'])
        self.assertIn('e-mailed you instructions for setting your password',
                      obj['value'])

        from_email = configuration_helpers.get_value(
            'email_from_address', settings.DEFAULT_FROM_EMAIL)
        sent_message = mail.outbox[0]

        bodies = {
            'plain_text': sent_message.body,
            'html': sent_message.alternatives[0][0],
        }

        body = bodies[body_type]

        self.assertIn("Password reset", sent_message.subject)
        self.assertIn(expected_output, body)
        self.assertEqual(sent_message.from_email, from_email)
        self.assertEqual(len(sent_message.to), 1)
        self.assertIn(self.user.email, sent_message.to)

        self.assert_event_emitted(
            SETTING_CHANGE_INITIATED,
            user_id=self.user.id,
            setting=u'password',
            old=None,
            new=None,
        )

        # Test that the user is not active
        self.user = User.objects.get(pk=self.user.pk)
        self.assertFalse(self.user.is_active)

        self.assertIn('password_reset_confirm/', body)
        re.search(
            r'password_reset_confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/',
            body).groupdict()

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls',
                         "Test only valid in LMS")
    @ddt.data((False, 'http://'), (True, 'https://'))
    @ddt.unpack
    def test_reset_password_email_https(self, is_secure, protocol):
        """
        Tests that the right url protocol is included in the reset password link
        """
        req = self.request_factory.post('/password_reset/',
                                        {'email': self.user.email})
        req.site = Mock(domain='example.com')
        req.is_secure = Mock(return_value=is_secure)
        req.user = self.user
        password_reset(req)
        sent_message = mail.outbox[0]
        msg = sent_message.body
        expected_msg = "Please go to the following page and choose a new password:\n\n" + protocol

        self.assertIn(expected_msg, msg)

        self.assert_event_emitted(SETTING_CHANGE_INITIATED,
                                  user_id=self.user.id,
                                  setting=u'password',
                                  old=None,
                                  new=None)

    @override_settings(FEATURES=ENABLE_LOGISTRATION_MICROFRONTEND)
    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls',
                         "Test only valid in LMS")
    @ddt.data(('Crazy Awesome Site', 'Crazy Awesome Site'), ('edX', 'edX'))
    @ddt.unpack
    def test_reset_password_email_site(self, site_name, platform_name):
        """
        Tests that the right url domain and platform name is included in
        the reset password email
        """
        with patch("django.conf.settings.PLATFORM_NAME", platform_name):
            with patch("django.conf.settings.SITE_NAME", site_name):
                req = self.request_factory.post('/password_reset/',
                                                {'email': self.user.email})
                req.user = self.user
                req.site = Mock(domain='example.com')
                password_reset(req)
                sent_message = mail.outbox[0]
                msg = sent_message.body

                reset_msg = u"you requested a password reset for your user account at {}"
                reset_msg = reset_msg.format(site_name)

                self.assertIn(reset_msg, msg)
                self.assertIn(settings.LOGISTRATION_MICROFRONTEND_URL, msg)

                sign_off = u"The {} Team".format(platform_name)
                self.assertIn(sign_off, msg)

                self.assert_event_emitted(SETTING_CHANGE_INITIATED,
                                          user_id=self.user.id,
                                          setting=u'password',
                                          old=None,
                                          new=None)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls',
                         "Test only valid in LMS")
    @patch("openedx.core.djangoapps.site_configuration.helpers.get_value",
           fake_get_value)
    @ddt.data('plain_text', 'html')
    def test_reset_password_email_configuration_override(self, body_type):
        """
        Tests that the right url domain and platform name is included in
        the reset password email
        """
        req = self.request_factory.post('/password_reset/',
                                        {'email': self.user.email})
        req.get_host = Mock(return_value=None)
        req.site = Mock(domain='example.com')
        req.user = self.user

        with patch('crum.get_current_request', return_value=req):
            password_reset(req)

        sent_message = mail.outbox[0]
        bodies = {
            'plain_text': sent_message.body,
            'html': sent_message.alternatives[0][0],
        }

        body = bodies[body_type]

        reset_msg = u"you requested a password reset for your user account at {}".format(
            fake_get_value('PLATFORM_NAME'))

        self.assertIn(reset_msg, body)

        self.assert_event_emitted(SETTING_CHANGE_INITIATED,
                                  user_id=self.user.id,
                                  setting=u'password',
                                  old=None,
                                  new=None)
        self.assertEqual(sent_message.from_email,
                         "*****@*****.**")

    @ddt.data(
        ('invalidUid', 'invalid_token'),
        (None, 'invalid_token'),
        ('invalidUid', None),
    )
    @ddt.unpack
    def test_reset_password_bad_token(self, uidb36, token):
        """
        Tests bad token and uidb36 in password reset
        """
        if uidb36 is None:
            uidb36 = self.uidb36
        if token is None:
            token = self.token

        bad_request = self.request_factory.get(
            reverse("password_reset_confirm",
                    kwargs={
                        "uidb36": uidb36,
                        "token": token
                    }))
        process_request(bad_request)
        bad_request.user = AnonymousUser()
        PasswordResetConfirmWrapper.as_view()(bad_request,
                                              uidb36=uidb36,
                                              token=token)
        self.user = User.objects.get(pk=self.user.pk)
        self.assertFalse(self.user.is_active)

    def test_reset_password_good_token(self):
        """
        Tests good token and uidb36 in password reset.

        Scenario:
        When the password reset url is opened
        Then the page is redirected to url without token
        And token gets set in session
        When the redirected page is visited with token in session
        Then reset password page renders
        And inactive user is set to active
        """
        good_reset_req = self.send_password_reset_request()
        good_reset_req.user = self.user
        redirect_response = PasswordResetConfirmWrapper.as_view()(
            good_reset_req, uidb36=self.uidb36, token=self.token)

        good_reset_req = self.request_factory.get(redirect_response.url)
        self.setup_request_session_with_token(good_reset_req)
        good_reset_req.user = self.user
        # set-password is the new token representation in the redirect url
        PasswordResetConfirmWrapper.as_view()(good_reset_req,
                                              uidb36=self.uidb36,
                                              token='set-password')

        self.user = User.objects.get(pk=self.user.pk)
        self.assertTrue(self.user.is_active)

    def test_reset_password_good_token_with_anonymous_user(self):
        """
        Tests good token and uidb36 in password reset for anonymous user.

        Scenario:
        When the password reset url is opened with anonymous user in request
        Then the page is redirected to url without token
        And token gets set in session
        When the redirected page is visited with token in session
        Then reset password page renders
        And inactive user associated with token is set to active
        """
        good_reset_req = self.send_password_reset_request()
        good_reset_req.user = AnonymousUser()
        redirect_response = PasswordResetConfirmWrapper.as_view()(
            good_reset_req, uidb36=self.uidb36, token=self.token)

        good_reset_req = self.request_factory.get(redirect_response.url)
        self.setup_request_session_with_token(good_reset_req)
        good_reset_req.user = AnonymousUser()
        # set-password is the new token representation in the redirect url
        PasswordResetConfirmWrapper.as_view()(good_reset_req,
                                              uidb36=self.uidb36,
                                              token='set-password')

        self.user = User.objects.get(pk=self.user.pk)
        self.assertTrue(self.user.is_active)

    def test_password_reset_fail(self):
        """
        Tests that if we provide mismatched passwords, user is not marked as active.
        """
        self.assertFalse(self.user.is_active)

        request_params = {
            'new_password1': 'password1',
            'new_password2': 'password2'
        }
        confirm_request = self.request_factory.post(
            self.password_reset_confirm_url, data=request_params)
        self.setup_request_session_with_token(confirm_request)
        confirm_request.user = self.user

        # Make a password reset request with mismatching passwords.
        resp = PasswordResetConfirmWrapper.as_view()(confirm_request,
                                                     uidb36=self.uidb36,
                                                     token=self.token)

        # Verify the response status code is: 200 with password reset fail and also verify that
        # the user is not marked as active.
        self.assertEqual(resp.status_code, 200)
        self.assertFalse(User.objects.get(pk=self.user.pk).is_active)

    def test_password_reset_retired_user_fail(self):
        """
        Tests that if a retired user attempts to reset their password, it fails.
        """
        self.assertFalse(self.user.is_active)

        # Retire the user.
        UserRetirementRequest.create_retirement_request(self.user)

        reset_req = self.request_factory.get(self.password_reset_confirm_url)
        reset_req.user = self.user
        resp = PasswordResetConfirmWrapper.as_view()(reset_req,
                                                     uidb36=self.uidb36,
                                                     token=self.token)

        # Verify the response status code is: 200 with password reset fail and also verify that
        # the user is not marked as active.
        self.assertEqual(resp.status_code, 200)
        self.assertFalse(User.objects.get(pk=self.user.pk).is_active)

    def test_password_reset_normalize_password(self):
        # pylint: disable=anomalous-unicode-escape-in-string
        """
        Tests that if we provide a not properly normalized password, it is saved using our normalization
        method of NFKC.
        In this test, the input password is u'p\u212bssword'. It should be normalized to u'p\xc5ssword'
        """
        password = u'p\u212bssword'
        request_params = {'new_password1': password, 'new_password2': password}
        confirm_request = self.request_factory.post(
            self.password_reset_confirm_url, data=request_params)
        process_request(confirm_request)
        confirm_request.session[INTERNAL_RESET_SESSION_TOKEN] = self.token
        confirm_request.user = self.user
        confirm_request.site = Mock(domain='example.com')
        __ = PasswordResetConfirmWrapper.as_view()(confirm_request,
                                                   uidb36=self.uidb36,
                                                   token=self.token)

        user = User.objects.get(pk=self.user.pk)
        salt_val = user.password.split('$')[1]
        expected_user_password = make_password(
            unicodedata.normalize('NFKC', u'p\u212bssword'), salt_val)
        self.assertEqual(expected_user_password, user.password)

        self.assert_email_sent_successfully({
            'subject':
            'Password reset completed',
            'body':
            'This is to confirm that you have successfully changed your password'
        })

    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
            {'min_length': 2}),
        create_validator_config(
            'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
            {'max_length': 10}),
    ])
    @ddt.data(
        {
            'password':
            '******',
            'error_message':
            'This password is too short. It must contain at least 2 characters.',
        }, {
            'password':
            '******',
            'error_message':
            'This password is too long. It must contain no more than 10 characters.',
        })
    def test_password_reset_with_invalid_length(self, password_dict):
        """
        Tests that if we provide password characters less then PASSWORD_MIN_LENGTH,
        or more than PASSWORD_MAX_LENGTH, password reset will fail with error message.
        """
        request_params = {
            'new_password1': password_dict['password'],
            'new_password2': password_dict['password']
        }
        confirm_request = self.request_factory.post(
            self.password_reset_confirm_url, data=request_params)
        self.setup_request_session_with_token(confirm_request)
        confirm_request.user = self.user

        # Make a password reset request with minimum/maximum passwords characters.
        response = PasswordResetConfirmWrapper.as_view()(confirm_request,
                                                         uidb36=self.uidb36,
                                                         token=self.token)

        self.assertEqual(response.context_data['err_msg'],
                         password_dict['error_message'])

    @patch.object(PasswordResetConfirmView, 'dispatch')
    @patch("openedx.core.djangoapps.site_configuration.helpers.get_value",
           fake_get_value)
    def test_reset_password_good_token_configuration_override(
            self, reset_confirm):
        """
        Tests password reset confirmation page for site configuration override.
        """
        good_reset_req = self.send_password_reset_request()
        good_reset_req.user = self.user
        PasswordResetConfirmWrapper.as_view()(good_reset_req,
                                              uidb36=self.uidb36,
                                              token=self.token)
        confirm_kwargs = reset_confirm.call_args[1]
        self.assertEqual(confirm_kwargs['extra_context']['platform_name'],
                         'Fake University')
        self.user = User.objects.get(pk=self.user.pk)
        self.assertTrue(self.user.is_active)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls',
                         "Test only valid in LMS")
    @ddt.data('Crazy Awesome Site', 'edX')
    def test_reset_password_email_subject(self, platform_name):
        """
        Tests that the right platform name is included in
        the reset password email subject
        """
        with patch("django.conf.settings.PLATFORM_NAME", platform_name):
            req = self.request_factory.post('/password_reset/',
                                            {'email': self.user.email})
            req.user = self.user
            req.site = Mock(domain='example.com')
            password_reset(req)
            sent_message = mail.outbox[0]
            subj = sent_message.subject

            self.assertIn(platform_name, subj)

    def test_reset_password_with_other_user_link(self):
        """
        Tests that user should not be able to reset password through other user's token
        """
        reset_request = self.request_factory.get(
            self.password_reset_confirm_url)
        reset_request.user = UserFactory.create()

        self.assertRaises(Http404,
                          PasswordResetConfirmWrapper.as_view(),
                          reset_request,
                          uidb36=self.uidb36,
                          token=self.token)
Example #4
0
class PasswordPolicyValidatorsTestCase(unittest.TestCase):
    """
    Tests for password validator utility functions

    The general framework I went with for testing the validators was to test:
        1) requiring a single check (also checks proper singular message)
        2) requiring multiple instances of the check (also checks proper plural message)
        3) successful check
    """
    def validation_errors_checker(self, password, msg, user=None):
        """
        This helper function is used to check the proper error messages are
        being displayed based on the password and validator.

        Parameters:
            password (unicode): the password to validate on
            user (django.contrib.auth.models.User): user object to use in validation.
                This is an optional parameter unless the validator requires a
                user object.
            msg (str): The expected ValidationError message
        """
        if msg is None:
            validate_password(password, user)
        else:
            with pytest.raises(ValidationError) as cm:
                validate_password(password, user)
            assert msg in ' '.join(cm.value.messages)

    def test_unicode_password(self):
        """ Tests that validate_password enforces unicode """
        unicode_str = u'𤭮'
        byte_str = unicode_str.encode('utf-8')

        # Sanity checks and demonstration of why this test is useful
        assert len(byte_str) == 4
        assert len(unicode_str) == 1

        # Test length check
        self.validation_errors_checker(
            byte_str,
            'This password is too short. It must contain at least 2 characters.'
        )
        self.validation_errors_checker(byte_str + byte_str, None)

        # Test badly encoded password
        self.validation_errors_checker(b'\xff\xff', 'Invalid password.')

    def test_password_unicode_normalization(self):
        """ Tests that validate_password normalizes passwords """
        # s ̣ ̇ (s with combining dot below and combining dot above)
        not_normalized_password = u'\u0073\u0323\u0307'
        assert len(not_normalized_password) == 3

        # When we normalize we expect the not_normalized password to fail
        # because it should be normalized to u'\u1E69' -> ṩ
        self.validation_errors_checker(
            not_normalized_password,
            'This password is too short. It must contain at least 2 characters.'
        )

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 2})
            ],  # lint-amnesty, pylint: disable=line-too-long
            'at least 2 characters.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 2}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
                    {'min_alphabetic': 2}),  # lint-amnesty, pylint: disable=line-too-long
            ],
            'characters, including 2 letters.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 2}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
                    {'min_alphabetic': 2}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.NumericValidator',
                    {'min_numeric': 1}),  # lint-amnesty, pylint: disable=line-too-long
            ],
            'characters, including 2 letters & 1 number.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 2}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.UppercaseValidator',
                    {'min_upper': 3}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.NumericValidator',
                    {'min_numeric': 1}),  # lint-amnesty, pylint: disable=line-too-long
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.SymbolValidator',
                    {'min_symbol': 2}),  # lint-amnesty, pylint: disable=line-too-long
            ],
            'including 3 uppercase letters & 1 number & 2 symbols.'),
    )
    @unpack
    def test_password_instructions(self, config, msg):
        """ Tests password instructions """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            assert msg in password_validators_instruction_texts()

    @data(
        (u'userna', u'username', '*****@*****.**',
         'The password is too similar to the username.'),
        (u'password', u'username', '*****@*****.**',
         'The password is too similar to the email address.'),
        (u'password', u'username', '*****@*****.**',
         'The password is too similar to the email address.'),
        (u'password', u'username', '*****@*****.**', None),
    )
    @unpack
    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'
        )
    ])
    def test_user_attribute_similarity_validation_errors(
            self, password, username, email, msg):
        """ Tests validate_password error messages for the UserAttributeSimilarityValidator """
        user = User(username=username, email=email)
        self.validation_errors_checker(password, msg, user)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'',
            'This password is too short. It must contain at least 1 character.'
        ),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 8})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'd',
            'This password is too short. It must contain at least 8 characters.'
        ),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MinimumLengthValidator',
                    {'min_length': 8})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'longpassword',
            None),
    )
    @unpack
    def test_minimum_length_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the MinimumLengthValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
                    {'max_length': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'longpassword',
            'This password is too long. It must contain no more than 1 character.'
        ),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
                    {'max_length': 10})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'longpassword',
            'This password is too long. It must contain no more than 10 characters.'
        ),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.MaximumLengthValidator',
                    {'max_length': 20})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'shortpassword',
            None),
    )
    @unpack
    def test_maximum_length_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the MaximumLengthValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (u'password', 'This password is too common.'),
        (u'good_password', None),
    )
    @unpack
    @override_settings(AUTH_PASSWORD_VALIDATORS=[
        create_validator_config(
            'django.contrib.auth.password_validation.CommonPasswordValidator')
    ])
    def test_common_password_validation_errors(self, password, msg):
        """ Tests validate_password error messages for the CommonPasswordValidator """
        self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
                    {'min_alphabetic': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'12345',
            'This password must contain at least 1 letter.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
                    {'min_alphabetic': 5})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'test123',
            'This password must contain at least 5 letters.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.AlphabeticValidator',
                    {'min_alphabetic': 2})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'password',
            None),
    )
    @unpack
    def test_alphabetic_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the AlphabeticValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.NumericValidator',
                    {'min_numeric': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'test',
            'This password must contain at least 1 number.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.NumericValidator',
                    {'min_numeric': 4})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'test123',
            'This password must contain at least 4 numbers.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.NumericValidator',
                    {'min_numeric': 2})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'password123',
            None),
    )
    @unpack
    def test_numeric_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the NumericValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.UppercaseValidator',
                    {'min_upper': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'lowercase',
            'This password must contain at least 1 uppercase letter.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.UppercaseValidator',
                    {'min_upper': 6})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'NOTenough',
            'This password must contain at least 6 uppercase letters.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.UppercaseValidator',
                    {'min_upper': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'camelCase',
            None),
    )
    @unpack
    def test_upper_case_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the UppercaseValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.LowercaseValidator',
                    {'min_lower': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'UPPERCASE',
            'This password must contain at least 1 lowercase letter.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.LowercaseValidator',
                    {'min_lower': 4})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'notENOUGH',
            'This password must contain at least 4 lowercase letters.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.LowercaseValidator',
                    {'min_lower': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'goodPassword',
            None),
    )
    @unpack
    def test_lower_case_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the LowercaseValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.PunctuationValidator',
                    {'min_punctuation': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'no punctuation',
            'This password must contain at least 1 punctuation mark.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.PunctuationValidator',
                    {'min_punctuation': 7})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'p@$$w0rd$!',
            'This password must contain at least 7 punctuation marks.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.PunctuationValidator',
                    {'min_punctuation': 3})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'excl@m@t!on',
            None),
    )
    @unpack
    def test_punctuation_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the PunctuationValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)

    @data(
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.SymbolValidator',
                    {'min_symbol': 1})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'no symbol',
            'This password must contain at least 1 symbol.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.SymbolValidator',
                    {'min_symbol': 3})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'☹️boo☹️',
            'This password must contain at least 3 symbols.'),
        (
            [
                create_validator_config(
                    'common.djangoapps.util.password_policy_validators.SymbolValidator',
                    {'min_symbol': 2})
            ],  # lint-amnesty, pylint: disable=line-too-long
            u'☪symbols!☹️',
            None),
    )
    @unpack
    def test_symbol_validation_errors(self, config, password, msg):
        """ Tests validate_password error messages for the SymbolValidator """
        with override_settings(AUTH_PASSWORD_VALIDATORS=config):
            self.validation_errors_checker(password, msg)