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 @@