diff --git a/seahub/auth/views.py b/seahub/auth/views.py index 79e432f8dc..16f29ffc87 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -23,6 +23,7 @@ from seahub.auth.forms import AuthenticationForm, CaptchaAuthenticationForm from seahub.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm from seahub.auth.tokens import default_token_generator from seahub.base.accounts import User +from seahub.options.models import UserOptions from seahub.utils import is_ldap_user from seahub.utils.http import is_safe_url from seahub.utils.ip import get_remote_ip @@ -134,6 +135,11 @@ def login(request, template_name='registration/login.html', # 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 + # captcha & passwod is valid, log user in request.session['remember_me'] = remember_me return log_user_in(request, form.get_user(), redirect_to) @@ -143,6 +149,11 @@ def login(request, template_name='registration/login.html', else: 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) @@ -365,6 +376,12 @@ def password_change(request, template_name='registration/password_change_form.ht form = password_change_form(user=request.user, data=request.POST) if form.is_valid(): form.save() + + if request.session.get('force_passwd_change', False): + del request.session['force_passwd_change'] + UserOptions.objects.unset_force_passwd_change( + request.user.username) + update_session_auth_hash(request, request.user) return HttpResponseRedirect(post_change_redirect) else: @@ -375,6 +392,7 @@ def password_change(request, template_name='registration/password_change_form.ht 'min_len': config.USER_PASSWORD_MIN_LENGTH, 'strong_pwd_required': config.USER_STRONG_PASSWORD_REQUIRED, 'level': config.USER_PASSWORD_STRENGTH_LEVEL, + 'force_passwd_change': request.session.get('force_passwd_change', False), }, context_instance=RequestContext(request)) def password_change_done(request, template_name='registration/password_change_done.html'): diff --git a/seahub/base/middleware.py b/seahub/base/middleware.py index 6d9e35b115..61ff887d31 100644 --- a/seahub/base/middleware.py +++ b/seahub/base/middleware.py @@ -1,4 +1,8 @@ +import re + from django.core.cache import cache +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect import seaserv @@ -12,6 +16,7 @@ try: from seahub.settings import MULTI_TENANCY except ImportError: MULTI_TENANCY = False +from seahub.settings import SITE_ROOT class BaseMiddleware(object): """ @@ -70,3 +75,21 @@ class InfobarMiddleware(object): def process_response(self, request, response): return response + + +class ForcePasswdChangeMiddleware(object): + def _request_in_black_list(self, request): + path = request.path + black_list = (r'^%s$' % SITE_ROOT, r'home/.+', r'repo/.+', + r'[f|d]/[a-f][0-9]{10}', r'group/\d+', r'groups/', + r'share/', r'profile/', r'notification/list/') + + for patt in black_list: + if re.search(patt, path) is not None: + return True + return False + + def process_request(self, request): + if request.session.get('force_passwd_change', False): + if self._request_in_black_list(request): + return HttpResponseRedirect(reverse('auth_password_change')) diff --git a/seahub/options/models.py b/seahub/options/models.py index 3ceabd1c4d..2bb27fda25 100644 --- a/seahub/options/models.py +++ b/seahub/options/models.py @@ -18,6 +18,9 @@ KEY_SUB_LIB = "sub_lib" VAL_SUB_LIB_ENABLED = "1" VAL_SUB_LIB_DISABLED = "0" +KEY_FORCE_PASSWD_CHANGE = "force_passwd_change" +VAL_FORCE_PASSWD_CHANGE = "1" + KEY_DEFAULT_REPO = "default_repo" class CryptoOptionNotSetError(Exception): @@ -43,6 +46,11 @@ class UserOptionsManager(models.Manager): return user_option + def unset_user_option(self, username, k): + """Remove user's option. + """ + super(UserOptionsManager, self).filter(email=username, option_key=k).delete() + def enable_server_crypto(self, username): """ @@ -185,7 +193,24 @@ class UserOptionsManager(models.Manager): return user_option.option_val except UserOptions.DoesNotExist: return None - + + def passwd_change_required(self, username): + """Check whether user need to change password. + """ + try: + r = super(UserOptionsManager, self).get( + email=username, option_key=KEY_FORCE_PASSWD_CHANGE) + return r.option_val == VAL_FORCE_PASSWD_CHANGE + except UserOptions.DoesNotExist: + return False + + def set_force_passwd_change(self, username): + return self.set_user_option(username, KEY_FORCE_PASSWD_CHANGE, + VAL_FORCE_PASSWD_CHANGE) + + def unset_force_passwd_change(self, username): + return self.unset_user_option(username, KEY_FORCE_PASSWD_CHANGE) + class UserOptions(models.Model): email = LowerCaseCharField(max_length=255, db_index=True) option_key = models.CharField(max_length=50) diff --git a/seahub/settings.py b/seahub/settings.py index 54291d4f06..14aa9510ce 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -117,7 +117,8 @@ MIDDLEWARE_CLASSES = ( 'seahub.auth.middleware.AuthenticationMiddleware', 'seahub.base.middleware.BaseMiddleware', 'seahub.base.middleware.InfobarMiddleware', - 'seahub.password_session.middleware.CheckPasswordHash' + 'seahub.password_session.middleware.CheckPasswordHash', + 'seahub.base.middleware.ForcePasswdChangeMiddleware', ) SITE_ROOT_URLCONF = 'seahub.urls' diff --git a/seahub/templates/registration/password_change_form.html b/seahub/templates/registration/password_change_form.html index 64825a0ea2..a23833055b 100644 --- a/seahub/templates/registration/password_change_form.html +++ b/seahub/templates/registration/password_change_form.html @@ -7,6 +7,10 @@ {% endblock %} {% block main_panel %} +{% if force_passwd_change %} +

{% trans "Please update your password before continue." %}

+{% endif %} +

{% trans "Password Modification" %}

{% csrf_token %} diff --git a/seahub/test_utils.py b/seahub/test_utils.py index 2f53ad1da8..ea0cf1e9c2 100644 --- a/seahub/test_utils.py +++ b/seahub/test_utils.py @@ -104,7 +104,7 @@ class Fixtures(Exam): class BaseTestCase(TestCase, Fixtures): def login_as(self, user): - self.client.post( + return self.client.post( reverse('auth_login'), {'login': user.username, - 'password': 'secret'} + 'password': self.user_password} ) diff --git a/seahub/utils/http.py b/seahub/utils/http.py index 2dbb5d3f2a..feb3f848ae 100644 --- a/seahub/utils/http.py +++ b/seahub/utils/http.py @@ -1,5 +1,7 @@ """Copied from latest django/utils/http.py::is_safe_url """ +from __future__ import unicode_literals + import unicodedata import urlparse import json diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 7c1622d7b5..cd66529472 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -46,6 +46,7 @@ from seahub.views.ajax import (get_related_users_by_org_repo, get_related_users_by_repo) from seahub.views import get_system_default_repo_id, gen_path_link from seahub.forms import SetUserQuotaForm, AddUserForm, BatchAddUserForm +from seahub.options.models import UserOptions from seahub.profile.models import Profile, DetailedProfile from seahub.signals import repo_deleted from seahub.share.models import FileShare, UploadLinkShare @@ -1236,7 +1237,9 @@ def user_reset(request, email): new_password = INIT_PASSWD user.set_password(new_password) user.save() + clear_token(user.username) + UserOptions.objects.set_force_passwd_change(user.username) if IS_EMAIL_CONFIGURED: if SEND_EMAIL_ON_RESETTING_USER_PASSWD: @@ -1309,6 +1312,7 @@ def user_add(request): if user: User.objects.update_role(email, role) + UserOptions.objects.set_force_passwd_change(email) if request.user.org: org_id = request.user.org.org_id diff --git a/tests/seahub/auth/test_login.py b/tests/seahub/auth/test_login.py new file mode 100644 index 0000000000..66b157fded --- /dev/null +++ b/tests/seahub/auth/test_login.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.core.urlresolvers import reverse + +from seahub.options.models import UserOptions +from seahub.test_utils import BaseTestCase + + +class LoginTest(BaseTestCase): + def test_can_login(self): + resp = self.login_as(self.user) + + self.assertEqual(302, resp.status_code) + self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL) + + def test_force_passwd_change_when_login(self): + UserOptions.objects.set_force_passwd_change(self.user.username) + + resp = self.login_as(self.user) + + self.assertEqual(302, resp.status_code) + self.assertRedirects(resp, '/accounts/password/change/') + + resp = self.client.get(reverse('auth_password_change')) + self.assertEqual(200, resp.status_code) + self.assertEqual(resp.context['force_passwd_change'], True) diff --git a/tests/seahub/auth/test_password_change.py b/tests/seahub/auth/test_password_change.py new file mode 100644 index 0000000000..5e5ea9768c --- /dev/null +++ b/tests/seahub/auth/test_password_change.py @@ -0,0 +1,26 @@ +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase + + +class PasswordChangeTest(BaseTestCase): + def test_can_render(self): + self.login_as(self.user) + + resp = self.client.get(reverse('auth_password_change')) + + self.assertEqual(200, resp.status_code) + self.assertContains(resp, 'Password Modification') + + def test_can_change(self): + self.login_as(self.user) + + resp = self.client.post( + reverse('auth_password_change'), { + 'old_password': self.user_password, + 'new_password1': '123', + 'new_password2': '123', + } + ) + self.assertEqual(302, resp.status_code) + self.assertRedirects(resp, reverse('auth_password_change_done')) diff --git a/tests/seahub/views/test_sysadmin.py b/tests/seahub/views/test_sysadmin.py index cbfd7452c7..f4b4946ab5 100644 --- a/tests/seahub/views/test_sysadmin.py +++ b/tests/seahub/views/test_sysadmin.py @@ -6,9 +6,11 @@ from django.http.cookie import parse_cookie from tests.common.utils import randstring from seahub.base.accounts import User -from seahub.utils.ms_excel import write_xls as real_write_xls -from seahub.test_utils import BaseTestCase +from seahub.options.models import (UserOptions, KEY_FORCE_PASSWD_CHANGE, + VAL_FORCE_PASSWD_CHANGE) from seahub.share.models import FileShare +from seahub.test_utils import BaseTestCase +from seahub.utils.ms_excel import write_xls as real_write_xls from seaserv import ccnet_threaded_rpc, seafile_api @@ -50,6 +52,9 @@ class UserResetTest(BaseTestCase): self.login_as(self.admin) def test_can_reset(self): + assert len(UserOptions.objects.filter( + email=self.user.username, option_key=KEY_FORCE_PASSWD_CHANGE)) == 0 + old_passwd = self.user.enc_password resp = self.client.post( reverse('user_reset', args=[self.user.email]) @@ -58,7 +63,9 @@ class UserResetTest(BaseTestCase): u = User.objects.get(email=self.user.username) assert u.enc_password != old_passwd - + assert UserOptions.objects.get( + email=self.user.username, + option_key=KEY_FORCE_PASSWD_CHANGE).option_val == VAL_FORCE_PASSWD_CHANGE class BatchUserMakeAdminTest(BaseTestCase): def setUp(self): @@ -98,6 +105,29 @@ class BatchUserMakeAdminTest(BaseTestCase): # assert u.enc_password == old_passwd +class UserAddTest(BaseTestCase): + def setUp(self): + self.new_user = 'new_user@test.com' + self.login_as(self.admin) + self.remove_user(self.new_user) + + def test_can_add(self): + assert len(UserOptions.objects.filter( + email=self.new_user, option_key=KEY_FORCE_PASSWD_CHANGE)) == 0 + + resp = self.client.post( + reverse('user_add',), { + 'email': self.new_user, + 'password1': '123', + 'password2': '123', + }, HTTP_X_REQUESTED_WITH='XMLHttpRequest' + ) + + self.assertEqual(200, resp.status_code) + assert UserOptions.objects.get( + email=self.new_user, + option_key=KEY_FORCE_PASSWD_CHANGE).option_val == VAL_FORCE_PASSWD_CHANGE + class UserRemoveTest(BaseTestCase): def setUp(self): self.login_as(self.admin)