mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-21 19:37:28 +00:00
Merge pull request #1146 from haiwen/freeze_on_login_failed
Freeze on login failed
This commit is contained in:
@@ -24,6 +24,7 @@ from seahub.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChange
|
|||||||
from seahub.auth.tokens import default_token_generator
|
from seahub.auth.tokens import default_token_generator
|
||||||
from seahub.base.accounts import User
|
from seahub.base.accounts import User
|
||||||
from seahub.options.models import UserOptions
|
from seahub.options.models import UserOptions
|
||||||
|
from seahub.profile.models import Profile
|
||||||
from seahub.utils import is_ldap_user
|
from seahub.utils import is_ldap_user
|
||||||
from seahub.utils.http import is_safe_url
|
from seahub.utils.http import is_safe_url
|
||||||
from seahub.utils.ip import get_remote_ip
|
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 + username)
|
||||||
cache.delete(LOGIN_ATTEMPT_PREFIX + ip)
|
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
|
@csrf_protect
|
||||||
@never_cache
|
@never_cache
|
||||||
def login(request, template_name='registration/login.html',
|
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',
|
remember_me = True if request.REQUEST.get('remember_me',
|
||||||
'') == 'on' else False
|
'') == 'on' else False
|
||||||
|
|
||||||
if failed_attempt >= settings.LOGIN_ATTEMPT_LIMIT:
|
if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT:
|
||||||
# have captcha
|
if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True:
|
||||||
form = CaptchaAuthenticationForm(data=request.POST)
|
# log user in if password is valid otherwise freeze account
|
||||||
if form.is_valid():
|
form = authentication_form(data=request.POST)
|
||||||
if UserOptions.objects.passwd_change_required(
|
if form.is_valid():
|
||||||
form.get_user().username):
|
return _handle_login_form_valid(request, form.get_user(),
|
||||||
redirect_to = reverse('auth_password_change')
|
redirect_to, remember_me)
|
||||||
request.session['force_passwd_change'] = True
|
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
|
try:
|
||||||
request.session['remember_me'] = remember_me
|
user = User.objects.get(email)
|
||||||
return log_user_in(request, form.get_user(), redirect_to)
|
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:
|
else:
|
||||||
# show page with captcha and increase failed login attempts
|
# log user in if password is valid otherwise show captcha
|
||||||
_incr_login_faied_attempts(username=username, ip=ip)
|
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:
|
else:
|
||||||
|
# login failed attempts < limit
|
||||||
form = authentication_form(data=request.POST)
|
form = authentication_form(data=request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if UserOptions.objects.passwd_change_required(
|
return _handle_login_form_valid(request, form.get_user(),
|
||||||
form.get_user().username):
|
redirect_to, remember_me)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
|
# increase failed attempts
|
||||||
login = urlquote(request.REQUEST.get('login', '').strip())
|
login = urlquote(request.REQUEST.get('login', '').strip())
|
||||||
failed_attempt = _incr_login_faied_attempts(username=login,
|
failed_attempt = _incr_login_faied_attempts(username=login,
|
||||||
ip=ip)
|
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' %
|
logger.warn('Login attempt limit reached, email/username: %s, ip: %s, attemps: %d' %
|
||||||
(login, ip, failed_attempt))
|
(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:
|
else:
|
||||||
form = authentication_form(data=request.POST)
|
form = authentication_form(data=request.POST)
|
||||||
else:
|
else:
|
||||||
### GET
|
### 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' %
|
logger.warn('Login attempt limit reached, ip: %s, attempts: %d' %
|
||||||
(ip, failed_attempt))
|
(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:
|
else:
|
||||||
form = authentication_form(request)
|
form = authentication_form(request)
|
||||||
|
|
||||||
|
@@ -1,23 +1,23 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.mail import send_mail
|
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.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import RequestSite
|
from django.contrib.sites.models import RequestSite
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
|
|
||||||
from seahub.auth.models import get_hexdigest
|
|
||||||
from seahub.auth import login
|
from seahub.auth import login
|
||||||
from registration import signals
|
from registration import signals
|
||||||
#from registration.forms import RegistrationForm
|
|
||||||
import seaserv
|
import seaserv
|
||||||
from seaserv import ccnet_threaded_rpc, unset_repo_passwd, is_passwd_set, \
|
from seaserv import ccnet_threaded_rpc, unset_repo_passwd, is_passwd_set, \
|
||||||
seafile_api
|
seafile_api
|
||||||
|
|
||||||
from seahub.profile.models import Profile, DetailedProfile
|
from seahub.profile.models import Profile, DetailedProfile
|
||||||
from seahub.utils import is_valid_username, is_user_password_strong, \
|
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:
|
try:
|
||||||
from seahub.settings import CLOUD_MODE
|
from seahub.settings import CLOUD_MODE
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -233,6 +233,30 @@ class User(object):
|
|||||||
"Sends an e-mail to this User."
|
"Sends an e-mail to this User."
|
||||||
send_mail(subject, message, from_email, [self.email])
|
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):
|
def remove_repo_passwds(self):
|
||||||
"""
|
"""
|
||||||
Remove all repo decryption passwords stored on server.
|
Remove all repo decryption passwords stored on server.
|
||||||
|
@@ -442,6 +442,7 @@ LOGGING = {
|
|||||||
#Login Attempt
|
#Login Attempt
|
||||||
LOGIN_ATTEMPT_LIMIT = 3
|
LOGIN_ATTEMPT_LIMIT = 3
|
||||||
LOGIN_ATTEMPT_TIMEOUT = 15 * 60 # in seconds (default: 15 minutes)
|
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).
|
# Age of cookie, in seconds (default: 1 day).
|
||||||
SESSION_COOKIE_AGE = 24 * 60 * 60
|
SESSION_COOKIE_AGE = 24 * 60 * 60
|
||||||
@@ -629,6 +630,9 @@ CONSTANCE_CONFIG = {
|
|||||||
'ACTIVATE_AFTER_REGISTRATION': (ACTIVATE_AFTER_REGISTRATION,''),
|
'ACTIVATE_AFTER_REGISTRATION': (ACTIVATE_AFTER_REGISTRATION,''),
|
||||||
'REGISTRATION_SEND_MAIL': (REGISTRATION_SEND_MAIL ,''),
|
'REGISTRATION_SEND_MAIL': (REGISTRATION_SEND_MAIL ,''),
|
||||||
'LOGIN_REMEMBER_DAYS': (LOGIN_REMEMBER_DAYS,''),
|
'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_USER_CREATE_ORG_REPO': (ENABLE_USER_CREATE_ORG_REPO, ''),
|
||||||
|
|
||||||
'ENABLE_ENCRYPTED_LIBRARY': (ENABLE_ENCRYPTED_LIBRARY,''),
|
'ENABLE_ENCRYPTED_LIBRARY': (ENABLE_ENCRYPTED_LIBRARY,''),
|
||||||
|
@@ -20,9 +20,11 @@
|
|||||||
<input type="hidden" name="next" value="{% if next %}{{ next|escape }}{% else %}{{ SITE_ROOT }}{% endif %}" />
|
<input type="hidden" name="next" value="{% if next %}{{ next|escape }}{% else %}{{ SITE_ROOT }}{% endif %}" />
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
{% if form.captcha.errors %}
|
{% if form.captcha.errors %}
|
||||||
<p class="error">{{ form.captcha.errors}}</p>
|
<p class="error">{{ form.captcha.errors}}</p>
|
||||||
|
{% elif form.errors.freeze_account %}
|
||||||
|
<p class="error">{{ form.errors.freeze_account }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="error">{% trans "Incorrect email or password" %}</p>
|
<p class="error">{% trans "Incorrect email or password" %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="error hide"></p>
|
<p class="error hide"></p>
|
||||||
|
@@ -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 %}
|
{% 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" %}
|
{% include "snippets/web_settings_form.html" %}
|
||||||
{% endwith %}
|
{% 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 %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Password</h4>
|
<h4>Password</h4>
|
||||||
|
19
seahub/templates/sysadmin/user_freeze_email.html
Normal file
19
seahub/templates/sysadmin/user_freeze_email.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'email_base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block email_con %}
|
||||||
|
|
||||||
|
{% autoescape off %}
|
||||||
|
|
||||||
|
<p style="color:#121214;font-size:14px;">{% trans "Hi," %}</p>
|
||||||
|
|
||||||
|
<p style="font-size:14px;color:#434144;">
|
||||||
|
{% blocktrans %}Account {{ user }} froze due to excessive failed logins. Please check at:{% endblocktrans%}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="{{ url_base }}{% url 'user_search' %}?email={{user|urlencode}}" target="_blank">{{ url_base }}{% url 'user_search' %}?email={{user|urlencode}}</a>
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@@ -1323,3 +1323,14 @@ def is_org_repo_creation_allowed(request):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return config.ENABLE_USER_CREATE_ORG_REPO
|
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
|
||||||
|
@@ -2184,7 +2184,8 @@ def sys_settings(request):
|
|||||||
'ENABLE_REPO_HISTORY_SETTING', 'USER_STRONG_PASSWORD_REQUIRED',
|
'ENABLE_REPO_HISTORY_SETTING', 'USER_STRONG_PASSWORD_REQUIRED',
|
||||||
'ENABLE_ENCRYPTED_LIBRARY', 'USER_PASSWORD_MIN_LENGTH',
|
'ENABLE_ENCRYPTED_LIBRARY', 'USER_PASSWORD_MIN_LENGTH',
|
||||||
'USER_PASSWORD_STRENGTH_LEVEL', 'SHARE_LINK_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',)
|
STRING_WEB_SETTINGS = ('SERVICE_URL', 'FILE_SERVER_ROOT',)
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from constance import config
|
||||||
|
|
||||||
from seahub.options.models import UserOptions
|
from seahub.options.models import UserOptions
|
||||||
from seahub.test_utils import BaseTestCase
|
from seahub.test_utils import BaseTestCase
|
||||||
|
|
||||||
@@ -58,4 +60,38 @@ class LoginTest(BaseTestCase):
|
|||||||
resp = self.client.get(reverse('auth_password_change'))
|
resp = self.client.get(reverse('auth_password_change'))
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assertEqual(resp.context['force_passwd_change'], True)
|
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']
|
||||||
|
|
||||||
|
17
tests/seahub/base/test_accounts.py
Normal file
17
tests/seahub/base/test_accounts.py
Normal file
@@ -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
|
Reference in New Issue
Block a user