diff --git a/seahub/auth/views.py b/seahub/auth/views.py index 16f29ffc87..5ab1f1499d 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -24,6 +24,7 @@ from seahub.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChange from seahub.auth.tokens import default_token_generator from seahub.base.accounts import User from seahub.options.models import UserOptions +from seahub.profile.models import Profile from seahub.utils import is_ldap_user from seahub.utils.http import is_safe_url from seahub.utils.ip import get_remote_ip @@ -111,6 +112,16 @@ def _clear_login_failed_attempts(request): cache.delete(LOGIN_ATTEMPT_PREFIX + username) cache.delete(LOGIN_ATTEMPT_PREFIX + ip) +def _handle_login_form_valid(request, user, redirect_to, remember_me): + if UserOptions.objects.passwd_change_required( + user.username): + redirect_to = reverse('auth_password_change') + request.session['force_passwd_change'] = True + + # password is valid, log user in + request.session['remember_me'] = remember_me + return log_user_in(request, user, redirect_to) + @csrf_protect @never_cache def login(request, template_name='registration/login.html', @@ -131,49 +142,67 @@ def login(request, template_name='registration/login.html', remember_me = True if request.REQUEST.get('remember_me', '') == 'on' else False - if failed_attempt >= settings.LOGIN_ATTEMPT_LIMIT: - # have captcha - form = CaptchaAuthenticationForm(data=request.POST) - if form.is_valid(): - if UserOptions.objects.passwd_change_required( - form.get_user().username): - redirect_to = reverse('auth_password_change') - request.session['force_passwd_change'] = True + if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: + if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: + # log user in if password is valid otherwise freeze account + form = authentication_form(data=request.POST) + if form.is_valid(): + return _handle_login_form_valid(request, form.get_user(), + redirect_to, remember_me) + else: + # freeze user account anyway + login = request.REQUEST.get('login', '') + email = Profile.objects.get_username_by_login_id(login) + if email is None: + email = login - # captcha & passwod is valid, log user in - request.session['remember_me'] = remember_me - return log_user_in(request, form.get_user(), redirect_to) + try: + user = User.objects.get(email) + if user.is_active: + user.freeze_user(notify_admins=True) + except User.DoesNotExist: + pass + form.errors['freeze_account'] = _('This account has been frozen due to too many failed login attempts.') else: - # show page with captcha and increase failed login attempts - _incr_login_faied_attempts(username=username, ip=ip) + # log user in if password is valid otherwise show captcha + form = CaptchaAuthenticationForm(data=request.POST) + if form.is_valid(): + return _handle_login_form_valid(request, form.get_user(), + redirect_to, remember_me) + else: + # show page with captcha and increase failed login attempts + _incr_login_faied_attempts(username=username, ip=ip) else: + # login failed attempts < limit form = authentication_form(data=request.POST) if form.is_valid(): - if UserOptions.objects.passwd_change_required( - form.get_user().username): - redirect_to = reverse('auth_password_change') - request.session['force_passwd_change'] = True - - # password is valid, log user in - request.session['remember_me'] = remember_me - return log_user_in(request, form.get_user(), redirect_to) + return _handle_login_form_valid(request, form.get_user(), + redirect_to, remember_me) else: + # increase failed attempts login = urlquote(request.REQUEST.get('login', '').strip()) failed_attempt = _incr_login_faied_attempts(username=login, ip=ip) - if failed_attempt >= settings.LOGIN_ATTEMPT_LIMIT: + if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: logger.warn('Login attempt limit reached, email/username: %s, ip: %s, attemps: %d' % (login, ip, failed_attempt)) - form = CaptchaAuthenticationForm() + + if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: + form = authentication_form(data=request.POST) + else: + form = CaptchaAuthenticationForm() else: form = authentication_form(data=request.POST) else: ### GET - if failed_attempt >= settings.LOGIN_ATTEMPT_LIMIT: + if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: logger.warn('Login attempt limit reached, ip: %s, attempts: %d' % (ip, failed_attempt)) - form = CaptchaAuthenticationForm(request) + if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: + form = authentication_form(data=request.POST) + else: + form = CaptchaAuthenticationForm() else: form = authentication_form(request) diff --git a/seahub/base/accounts.py b/seahub/base/accounts.py index 456ebc9856..a534fe020b 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -1,23 +1,23 @@ # encoding: utf-8 from django import forms from django.core.mail import send_mail -from django.utils.encoding import smart_str +from django.utils import translation from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.contrib.sites.models import RequestSite from django.contrib.sites.models import Site -from seahub.auth.models import get_hexdigest from seahub.auth import login from registration import signals -#from registration.forms import RegistrationForm import seaserv from seaserv import ccnet_threaded_rpc, unset_repo_passwd, is_passwd_set, \ seafile_api from seahub.profile.models import Profile, DetailedProfile from seahub.utils import is_valid_username, is_user_password_strong, \ - clear_token + clear_token, get_system_admins +from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY + try: from seahub.settings import CLOUD_MODE except ImportError: @@ -233,6 +233,30 @@ class User(object): "Sends an e-mail to this User." send_mail(subject, message, from_email, [self.email]) + def freeze_user(self, notify_admins=False): + self.is_active = False + self.save() + + if notify_admins: + admins = get_system_admins() + for u in admins: + # save current language + cur_language = translation.get_language() + + # get and active user language + user_language = Profile.objects.get_user_language(u.email) + translation.activate(user_language) + + send_html_email_with_dj_template( + u.email, dj_template='sysadmin/user_freeze_email.html', + subject=_('Account %s froze on %s.') % (self.email, settings.SITE_NAME), + context={'user': self.email}, + priority=MAIL_PRIORITY.now + ) + + # restore current language + translation.activate(cur_language) + def remove_repo_passwds(self): """ Remove all repo decryption passwords stored on server. diff --git a/seahub/settings.py b/seahub/settings.py index 2d82ecef10..a92b287753 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -442,6 +442,7 @@ LOGGING = { #Login Attempt LOGIN_ATTEMPT_LIMIT = 3 LOGIN_ATTEMPT_TIMEOUT = 15 * 60 # in seconds (default: 15 minutes) +FREEZE_USER_ON_LOGIN_FAILED = False # deactivate user account when login attempts exceed limit # Age of cookie, in seconds (default: 1 day). SESSION_COOKIE_AGE = 24 * 60 * 60 @@ -629,6 +630,9 @@ CONSTANCE_CONFIG = { 'ACTIVATE_AFTER_REGISTRATION': (ACTIVATE_AFTER_REGISTRATION,''), 'REGISTRATION_SEND_MAIL': (REGISTRATION_SEND_MAIL ,''), 'LOGIN_REMEMBER_DAYS': (LOGIN_REMEMBER_DAYS,''), + 'LOGIN_ATTEMPT_LIMIT': (LOGIN_ATTEMPT_LIMIT, ''), + 'FREEZE_USER_ON_LOGIN_FAILED': (FREEZE_USER_ON_LOGIN_FAILED, ''), + 'ENABLE_USER_CREATE_ORG_REPO': (ENABLE_USER_CREATE_ORG_REPO, ''), 'ENABLE_ENCRYPTED_LIBRARY': (ENABLE_ENCRYPTED_LIBRARY,''), diff --git a/seahub/templates/registration/login.html b/seahub/templates/registration/login.html index be4b50e9ac..f2b560253d 100644 --- a/seahub/templates/registration/login.html +++ b/seahub/templates/registration/login.html @@ -20,9 +20,11 @@ {% if form.errors %} {% if form.captcha.errors %} -
{{ form.captcha.errors}}
+{{ form.captcha.errors}}
+ {% elif form.errors.freeze_account %} +{{ form.errors.freeze_account }}
{% else %} -{% trans "Incorrect email or password" %}
+{% trans "Incorrect email or password" %}
{% endif %} {% else %} diff --git a/seahub/templates/sysadmin/settings.html b/seahub/templates/sysadmin/settings.html index 6825b53686..1dff351939 100644 --- a/seahub/templates/sysadmin/settings.html +++ b/seahub/templates/sysadmin/settings.html @@ -40,6 +40,15 @@ {% with type="input" setting_display_name="keep sign in" help_tip="Number of days that keep user sign in." setting_name="LOGIN_REMEMBER_DAYS" setting_val=config_dict.LOGIN_REMEMBER_DAYS %} {% include "snippets/web_settings_form.html" %} {% endwith %} + + {% with type="input" setting_display_name="LOGIN_ATTEMPT_LIMIT" help_tip="The maximum number of failed login attempts before showing CAPTCHA." setting_name="LOGIN_ATTEMPT_LIMIT" setting_val=config_dict.LOGIN_ATTEMPT_LIMIT %} + {% include "snippets/web_settings_form.html" %} + {% endwith %} + + {% with type="checkbox" setting_display_name="FREEZE_USER_ON_LOGIN_FAILED" help_tip="Freeze user account when failed login attempts exceed limit." setting_name="FREEZE_USER_ON_LOGIN_FAILED" setting_val=config_dict.FREEZE_USER_ON_LOGIN_FAILED %} + {% include "snippets/web_settings_form.html" %} + {% endwith %} +{% trans "Hi," %}
+ ++{% blocktrans %}Account {{ user }} froze due to excessive failed logins. Please check at:{% endblocktrans%} +
+ +{{ url_base }}{% url 'user_search' %}?email={{user|urlencode}} + +{% endautoescape %} + +{% endblock %} diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index c24652cba3..12e1456c14 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -1323,3 +1323,14 @@ def is_org_repo_creation_allowed(request): return True else: return config.ENABLE_USER_CREATE_ORG_REPO + +def get_system_admins(): + db_users = seaserv.get_emailusers('DB', -1, -1) + ldpa_imported_users = seaserv.get_emailusers('LDAPImport', -1, -1) + + admins = [] + for user in db_users + ldpa_imported_users: + if user.is_staff: + admins.append(user) + + return admins diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 48128c2af0..4d0b8ad166 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -2184,7 +2184,8 @@ def sys_settings(request): 'ENABLE_REPO_HISTORY_SETTING', 'USER_STRONG_PASSWORD_REQUIRED', 'ENABLE_ENCRYPTED_LIBRARY', 'USER_PASSWORD_MIN_LENGTH', 'USER_PASSWORD_STRENGTH_LEVEL', 'SHARE_LINK_PASSWORD_MIN_LENGTH', - 'ENABLE_USER_CREATE_ORG_REPO', 'FORCE_PASSWORD_CHANGE' + 'ENABLE_USER_CREATE_ORG_REPO', 'FORCE_PASSWORD_CHANGE', + 'LOGIN_ATTEMPT_LIMIT', 'FREEZE_USER_ON_LOGIN_FAILED', ) STRING_WEB_SETTINGS = ('SERVICE_URL', 'FILE_SERVER_ROOT',) diff --git a/tests/seahub/auth/views/test_login.py b/tests/seahub/auth/views/test_login.py index af75a51f38..9711380b8f 100644 --- a/tests/seahub/auth/views/test_login.py +++ b/tests/seahub/auth/views/test_login.py @@ -1,6 +1,8 @@ from django.conf import settings from django.core.urlresolvers import reverse +from constance import config + from seahub.options.models import UserOptions from seahub.test_utils import BaseTestCase @@ -58,4 +60,38 @@ class LoginTest(BaseTestCase): resp = self.client.get(reverse('auth_password_change')) self.assertEqual(200, resp.status_code) 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 + + def tearDown(self): + self.clear_cache() + + def _bad_passwd_login(self): + resp = self.client.post( + reverse('auth_login'), {'login': self.user.username, + 'password': 'badpassword'} + ) + return resp + + def _login_page(self): + resp = self.client.get(reverse('auth_login')) + return resp + + 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'] + diff --git a/tests/seahub/base/test_accounts.py b/tests/seahub/base/test_accounts.py new file mode 100644 index 0000000000..d89beadde2 --- /dev/null +++ b/tests/seahub/base/test_accounts.py @@ -0,0 +1,17 @@ +from seahub.test_utils import BaseTestCase +from seahub.base.accounts import User + +from post_office.models import Email + +class UserTest(BaseTestCase): + def test_freeze_user(self): + assert len(Email.objects.all()) == 0 + + u = User.objects.get(self.user.username) + u.freeze_user(notify_admins=True) + + assert u.is_active is False + + assert len(Email.objects.all()) > 0 + # email = Email.objects.all()[0] + # print email.html_message