diff --git a/seahub/auth/views.py b/seahub/auth/views.py index a6ea65a8e9..4ca6c2f360 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -109,8 +109,11 @@ def _clear_login_failed_attempts(request): """ username = request.user.username ip = get_remote_ip(request) - cache.delete(LOGIN_ATTEMPT_PREFIX + username) + cache.delete(LOGIN_ATTEMPT_PREFIX + urlquote(username)) cache.delete(LOGIN_ATTEMPT_PREFIX + ip) + p = Profile.objects.get_profile_by_user(username) + if p and p.login_id: + cache.delete(LOGIN_ATTEMPT_PREFIX + urlquote(p.login_id)) def _handle_login_form_valid(request, user, redirect_to, remember_me): if UserOptions.objects.passwd_change_required( diff --git a/seahub/test_utils.py b/seahub/test_utils.py index f52754673c..59b9ecd671 100644 --- a/seahub/test_utils.py +++ b/seahub/test_utils.py @@ -145,8 +145,15 @@ class BaseTestCase(TestCase, Fixtures): self.remove_repo(self.repo.id) def login_as(self, user): + if isinstance(user, basestring): + login = user + elif isinstance(user, User): + login = user.username + else: + assert False + return self.client.post( - reverse('auth_login'), {'login': user.username, + reverse('auth_login'), {'login': login, 'password': self.user_password} ) diff --git a/tests/seahub/auth/views/test_login.py b/tests/seahub/auth/views/test_login.py index 9711380b8f..83538c52bb 100644 --- a/tests/seahub/auth/views/test_login.py +++ b/tests/seahub/auth/views/test_login.py @@ -1,9 +1,15 @@ from django.conf import settings +from django.core.cache import cache from django.core.urlresolvers import reverse +from django.utils.http import urlquote from constance import config +from seahub.base.accounts import User +from seahub.auth.forms import AuthenticationForm, CaptchaAuthenticationForm +from seahub.auth.views import LOGIN_ATTEMPT_PREFIX from seahub.options.models import UserOptions +from seahub.profile.models import Profile from seahub.test_utils import BaseTestCase @@ -17,6 +23,20 @@ class LoginTest(BaseTestCase): self.assertEqual(302, resp.status_code) self.assertRegexpMatches(resp['Location'], r'http://testserver%s' % settings.LOGIN_REDIRECT_URL) + def test_can_login_with_login_id(self): + p = Profile.objects.add_or_update(self.user.username, 'nickname') + login_id = 'test_login_id' + p.login_id = login_id + p.save() + assert Profile.objects.get_username_by_login_id(login_id) == self.user.username + + resp = self.client.post( + reverse('auth_login'), {'login': login_id, + 'password': self.user_password} + ) + self.assertEqual(302, resp.status_code) + self.assertRegexpMatches(resp['Location'], r'http://testserver%s' % settings.LOGIN_REDIRECT_URL) + def test_redirect_to_after_success_login(self): resp = self.client.post( reverse('auth_login') + '?next=/foo/', @@ -62,17 +82,15 @@ class LoginTest(BaseTestCase): self.assertEqual(resp.context['force_passwd_change'], True) -class LoginCaptchaTest(BaseTestCase): - def setUp(self): - config.LOGIN_ATTEMPT_LIMIT = 1 - config.FREEZE_USER_ON_LOGIN_FAILED = False +class LoginTestMixin(): + """Utility methods for login test. + """ + def _bad_passwd_login(self, user=None): + if user is None: + user = self.user - def tearDown(self): - self.clear_cache() - - def _bad_passwd_login(self): resp = self.client.post( - reverse('auth_login'), {'login': self.user.username, + reverse('auth_login'), {'login': user.username, 'password': 'badpassword'} ) return resp @@ -81,17 +99,115 @@ class LoginCaptchaTest(BaseTestCase): resp = self.client.get(reverse('auth_login')) return resp + def _get_user_login_failed_attempt(self, username): + return cache.get(LOGIN_ATTEMPT_PREFIX + urlquote(username), 0) + + +class LoginCaptchaTest(BaseTestCase, LoginTestMixin): + def setUp(self): + self.clear_cache() # make sure cache is clean + + config.LOGIN_ATTEMPT_LIMIT = 3 + config.FREEZE_USER_ON_LOGIN_FAILED = False + + def tearDown(self): + self.clear_cache() + def test_can_show_captcha(self): - resp = self._bad_passwd_login() - print resp.context['form'] - - resp = self._bad_passwd_login() - print resp.context['form'] - - resp = self._bad_passwd_login() - print resp.context['form'] - - print '-------------' resp = self._login_page() - print resp.context['form'] + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert self._get_user_login_failed_attempt(self.user.username) == 0 + # first failed login + resp = self._bad_passwd_login() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.user.username) == 1 + + # second failed login + resp = self._bad_passwd_login() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.user.username) == 2 + + # third failed login, and show the captha + resp = self._bad_passwd_login() + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is True + assert self._get_user_login_failed_attempt(self.user.username) == 3 + + def test_can_clear_failed_attempt_after_login(self): + resp = self._login_page() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert self._get_user_login_failed_attempt(self.user.username) == 0 + + # first failed login + resp = self._bad_passwd_login() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.user.username) == 1 + + # successful login + self.login_as(self.user) + + assert self._get_user_login_failed_attempt(self.user.username) == 0 + + def test_login_with_login_id(self): + p = Profile.objects.add_or_update(self.user.username, 'nickname') + login_id = 'test_login_id' + p.login_id = login_id + p.save() + assert Profile.objects.get_username_by_login_id(login_id) == self.user.username + + # first failed login + resp = self._bad_passwd_login() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.user.username) == 1 + + # successful login using login id + self.login_as(login_id) + + assert self._get_user_login_failed_attempt(self.user.username) == 0 + + +class FreezeUserOnLoginFailedTest(BaseTestCase, LoginTestMixin): + def setUp(self): + self.clear_cache() # make sure cache is clean + + config.LOGIN_ATTEMPT_LIMIT = 3 + config.FREEZE_USER_ON_LOGIN_FAILED = True + + self.tmp_user = self.create_user() + + def tearDown(self): + self.clear_cache() + self.remove_user(self.tmp_user.username) + + def test_can_freeze(self): + assert bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True + + resp = self._login_page() + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert self._get_user_login_failed_attempt(self.tmp_user.username) == 0 + + # first failed login + resp = self._bad_passwd_login(user=self.tmp_user) + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.tmp_user.username) == 1 + assert User.objects.get(self.tmp_user.username).is_active is True + + # second failed login + resp = self._bad_passwd_login(user=self.tmp_user) + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.tmp_user.username) == 2 + assert User.objects.get(self.tmp_user.username).is_active is True + + # third failed login, and freeze user instead of showing captha + resp = self._bad_passwd_login(user=self.tmp_user) + assert isinstance(resp.context['form'], AuthenticationForm) is True + assert isinstance(resp.context['form'], CaptchaAuthenticationForm) is False + assert self._get_user_login_failed_attempt(self.tmp_user.username) == 3 + + assert User.objects.get(self.tmp_user.username).is_active is False