diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -20,6 +20,7 @@ import logging from pylons import tmpl_context as c +from pyramid.httpexceptions import HTTPFound from rhodecode.lib.utils2 import StrictAttributeDict @@ -41,6 +42,7 @@ class BaseAppView(object): self.context = context self.session = request.session self._rhodecode_user = request.user # auth user + self._rhodecode_db_user = self._rhodecode_user.get_instance() def _get_local_tmpl_context(self): c = TemplateArgs() diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -23,6 +23,15 @@ from rhodecode.apps._base import ADMIN_P def includeme(config): + + config.add_route( + name='my_account_password', + pattern=ADMIN_PREFIX + '/my_account/password') + + config.add_route( + name='my_account_password_update', + pattern=ADMIN_PREFIX + '/my_account/password') + config.add_route( name='my_account_auth_tokens', pattern=ADMIN_PREFIX + '/my_account/auth_tokens') diff --git a/rhodecode/apps/my_account/tests/test_my_account_password.py b/rhodecode/apps/my_account/tests/test_my_account_password.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/my_account/tests/test_my_account_password.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest +import mock + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import check_password +from rhodecode.model.meta import Session +from rhodecode.model.user import UserModel +from rhodecode.tests import assert_session_flash +from rhodecode.tests.fixture import Fixture, TestController, error_function + +fixture = Fixture() + + +def route_path(name, **kwargs): + return { + 'home': '/', + 'my_account_password': + ADMIN_PREFIX + '/my_account/password', + }[name].format(**kwargs) + + +test_user_1 = 'testme' +test_user_1_password = '0jd83nHNS/d23n' + + +class TestMyAccountPassword(TestController): + def test_valid_change_password(self, user_util): + new_password = 'my_new_valid_password' + user = user_util.create_user(password=test_user_1_password) + self.log_user(user.username, test_user_1_password) + + form_data = [ + ('current_password', test_user_1_password), + ('__start__', 'new_password:mapping'), + ('new_password', new_password), + ('new_password-confirm', new_password), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + response = self.app.post(route_path('my_account_password'), form_data).follow() + assert 'Successfully updated password' in response + + # check_password depends on user being in session + Session().add(user) + try: + assert check_password(new_password, user.password) + finally: + Session().expunge(user) + + @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [ + ('', 'abcdef123', 'abcdef123'), + ('wrong_pw', 'abcdef123', 'abcdef123'), + (test_user_1_password, test_user_1_password, test_user_1_password), + (test_user_1_password, '', ''), + (test_user_1_password, 'abcdef123', ''), + (test_user_1_password, '', 'abcdef123'), + (test_user_1_password, 'not_the', 'same_pw'), + (test_user_1_password, 'short', 'short'), + ]) + def test_invalid_change_password(self, current_pw, new_pw, confirm_pw, + user_util): + user = user_util.create_user(password=test_user_1_password) + self.log_user(user.username, test_user_1_password) + + form_data = [ + ('current_password', current_pw), + ('__start__', 'new_password:mapping'), + ('new_password', new_pw), + ('new_password-confirm', confirm_pw), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + response = self.app.post(route_path('my_account_password'), form_data) + + assert_response = response.assert_response() + assert assert_response.get_elements('.error-block') + + @mock.patch.object(UserModel, 'update_user', error_function) + def test_invalid_change_password_exception(self, user_util): + user = user_util.create_user(password=test_user_1_password) + self.log_user(user.username, test_user_1_password) + + form_data = [ + ('current_password', test_user_1_password), + ('__start__', 'new_password:mapping'), + ('new_password', '123456'), + ('new_password-confirm', '123456'), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + response = self.app.post(route_path('my_account_password'), form_data) + assert_session_flash( + response, 'Error occurred during update of user password') + + def test_password_is_updated_in_session_on_password_change(self, user_util): + old_password = 'abcdef123' + new_password = 'abcdef124' + + user = user_util.create_user(password=old_password) + session = self.log_user(user.username, old_password) + old_password_hash = session['password'] + + form_data = [ + ('current_password', old_password), + ('__start__', 'new_password:mapping'), + ('new_password', new_password), + ('new_password-confirm', new_password), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + self.app.post(route_path('my_account_password'), form_data) + + response = self.app.get(route_path('home')) + new_password_hash = response.session['rhodecode_user']['password'] + + assert old_password_hash != new_password_hash \ No newline at end of file diff --git a/rhodecode/apps/my_account/views.py b/rhodecode/apps/my_account/views.py --- a/rhodecode/apps/my_account/views.py +++ b/rhodecode/apps/my_account/views.py @@ -24,11 +24,14 @@ from pyramid.httpexceptions import HTTPF from pyramid.view import view_config from rhodecode.apps._base import BaseAppView +from rhodecode import forms from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired -from rhodecode.lib.utils2 import safe_int from rhodecode.lib import helpers as h +from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.meta import Session +from rhodecode.model.user import UserModel +from rhodecode.model.validation_schema.schemas import user_schema log = logging.getLogger(__name__) @@ -42,7 +45,6 @@ class MyAccountView(BaseAppView): def load_default_context(self): c = self._get_local_tmpl_context() - c.user = c.auth_user.get_instance() c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS self._register_global_c(c) @@ -51,6 +53,69 @@ class MyAccountView(BaseAppView): @LoginRequired() @NotAnonymous() @view_config( + route_name='my_account_password', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_password(self): + c = self.load_default_context() + c.active = 'password' + c.extern_type = c.user.extern_type + + schema = user_schema.ChangePasswordSchema().bind( + username=c.user.username) + + form = forms.Form( + schema, buttons=(forms.buttons.save, forms.buttons.reset)) + + c.form = form + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_password', request_method='POST', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_password_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'password' + c.extern_type = c.user.extern_type + + schema = user_schema.ChangePasswordSchema().bind( + username=c.user.username) + + form = forms.Form( + schema, buttons=(forms.buttons.save, forms.buttons.reset)) + + if c.extern_type != 'rhodecode': + raise HTTPFound(self.request.route_path('my_account_password')) + + controls = self.request.POST.items() + try: + valid_data = form.validate(controls) + UserModel().update_user(c.user.user_id, **valid_data) + c.user.update_userdata(force_password_change=False) + Session().commit() + except forms.ValidationFailure as e: + c.form = e + return self._get_template_context(c) + + except Exception: + log.exception("Exception updating password") + h.flash(_('Error occurred during update of user password'), + category='error') + else: + instance = c.auth_user.get_instance() + self.session.setdefault('rhodecode_user', {}).update( + {'password': md5(instance.password)}) + self.session.save() + h.flash(_("Successfully updated password"), category='success') + + raise HTTPFound(self.request.route_path('my_account_password')) + + @LoginRequired() + @NotAnonymous() + @view_config( route_name='my_account_auth_tokens', request_method='GET', renderer='rhodecode:templates/admin/my_account/my_account.mako') def my_account_auth_tokens(self): diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -512,8 +512,10 @@ def make_map(config): m.connect('my_account', '/my_account', action='my_account_update', conditions={'method': ['POST']}) + # NOTE(marcink): this needs to be kept for password force flag to be + # handler, remove after migration to pyramid m.connect('my_account_password', '/my_account/password', - action='my_account_password', conditions={'method': ['GET', 'POST']}) + action='my_account_password', conditions={'method': ['GET']}) m.connect('my_account_repos', '/my_account/repos', action='my_account_repos', conditions={'method': ['GET']}) diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -34,19 +34,17 @@ from pylons.controllers.util import redi from pylons.i18n.translation import _ from sqlalchemy.orm import joinedload -from rhodecode import forms from rhodecode.lib import helpers as h from rhodecode.lib import auth from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, AuthUser) from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import jsonify -from rhodecode.lib.utils2 import safe_int, md5, str2bool +from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.lib.ext_json import json from rhodecode.lib.channelstream import channelstream_request, \ ChannelstreamException -from rhodecode.model.validation_schema.schemas import user_schema from rhodecode.model.db import ( Repository, PullRequest, UserEmailMap, User, UserFollowing) from rhodecode.model.forms import UserForm @@ -194,47 +192,6 @@ class MyAccountController(BaseController force_defaults=False ) - @auth.CSRFRequired(except_methods=['GET']) - def my_account_password(self): - c.active = 'password' - self.__load_data() - c.extern_type = c.user.extern_type - - schema = user_schema.ChangePasswordSchema().bind( - username=c.rhodecode_user.username) - - form = forms.Form(schema, - buttons=(forms.buttons.save, forms.buttons.reset)) - - if request.method == 'POST' and c.extern_type == 'rhodecode': - controls = request.POST.items() - try: - valid_data = form.validate(controls) - UserModel().update_user(c.rhodecode_user.user_id, **valid_data) - instance = c.rhodecode_user.get_instance() - instance.update_userdata(force_password_change=False) - Session().commit() - except forms.ValidationFailure as e: - request.session.flash( - _('Error occurred during update of user password'), - queue='error') - form = e - except Exception: - log.exception("Exception updating password") - request.session.flash( - _('Error occurred during update of user password'), - queue='error') - else: - session.setdefault('rhodecode_user', {}).update( - {'password': md5(instance.password)}) - session.save() - request.session.flash( - _("Successfully updated password"), queue='success') - return redirect(url('my_account_password')) - - c.form = form - return render('admin/my_account/my_account.mako') - def my_account_repos(self): c.active = 'repos' self.__load_data() diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -27,7 +27,7 @@