Show More
@@ -0,0 +1,137 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2010-2017 RhodeCode GmbH | |
|
4 | # | |
|
5 | # This program is free software: you can redistribute it and/or modify | |
|
6 | # it under the terms of the GNU Affero General Public License, version 3 | |
|
7 | # (only), as published by the Free Software Foundation. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU Affero General Public License | |
|
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
16 | # | |
|
17 | # This program is dual-licensed. If you wish to learn more about the | |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
20 | ||
|
21 | import pytest | |
|
22 | import mock | |
|
23 | ||
|
24 | from rhodecode.apps._base import ADMIN_PREFIX | |
|
25 | from rhodecode.lib import helpers as h | |
|
26 | from rhodecode.lib.auth import check_password | |
|
27 | from rhodecode.model.meta import Session | |
|
28 | from rhodecode.model.user import UserModel | |
|
29 | from rhodecode.tests import assert_session_flash | |
|
30 | from rhodecode.tests.fixture import Fixture, TestController, error_function | |
|
31 | ||
|
32 | fixture = Fixture() | |
|
33 | ||
|
34 | ||
|
35 | def route_path(name, **kwargs): | |
|
36 | return { | |
|
37 | 'home': '/', | |
|
38 | 'my_account_password': | |
|
39 | ADMIN_PREFIX + '/my_account/password', | |
|
40 | }[name].format(**kwargs) | |
|
41 | ||
|
42 | ||
|
43 | test_user_1 = 'testme' | |
|
44 | test_user_1_password = '0jd83nHNS/d23n' | |
|
45 | ||
|
46 | ||
|
47 | class TestMyAccountPassword(TestController): | |
|
48 | def test_valid_change_password(self, user_util): | |
|
49 | new_password = 'my_new_valid_password' | |
|
50 | user = user_util.create_user(password=test_user_1_password) | |
|
51 | self.log_user(user.username, test_user_1_password) | |
|
52 | ||
|
53 | form_data = [ | |
|
54 | ('current_password', test_user_1_password), | |
|
55 | ('__start__', 'new_password:mapping'), | |
|
56 | ('new_password', new_password), | |
|
57 | ('new_password-confirm', new_password), | |
|
58 | ('__end__', 'new_password:mapping'), | |
|
59 | ('csrf_token', self.csrf_token), | |
|
60 | ] | |
|
61 | response = self.app.post(route_path('my_account_password'), form_data).follow() | |
|
62 | assert 'Successfully updated password' in response | |
|
63 | ||
|
64 | # check_password depends on user being in session | |
|
65 | Session().add(user) | |
|
66 | try: | |
|
67 | assert check_password(new_password, user.password) | |
|
68 | finally: | |
|
69 | Session().expunge(user) | |
|
70 | ||
|
71 | @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [ | |
|
72 | ('', 'abcdef123', 'abcdef123'), | |
|
73 | ('wrong_pw', 'abcdef123', 'abcdef123'), | |
|
74 | (test_user_1_password, test_user_1_password, test_user_1_password), | |
|
75 | (test_user_1_password, '', ''), | |
|
76 | (test_user_1_password, 'abcdef123', ''), | |
|
77 | (test_user_1_password, '', 'abcdef123'), | |
|
78 | (test_user_1_password, 'not_the', 'same_pw'), | |
|
79 | (test_user_1_password, 'short', 'short'), | |
|
80 | ]) | |
|
81 | def test_invalid_change_password(self, current_pw, new_pw, confirm_pw, | |
|
82 | user_util): | |
|
83 | user = user_util.create_user(password=test_user_1_password) | |
|
84 | self.log_user(user.username, test_user_1_password) | |
|
85 | ||
|
86 | form_data = [ | |
|
87 | ('current_password', current_pw), | |
|
88 | ('__start__', 'new_password:mapping'), | |
|
89 | ('new_password', new_pw), | |
|
90 | ('new_password-confirm', confirm_pw), | |
|
91 | ('__end__', 'new_password:mapping'), | |
|
92 | ('csrf_token', self.csrf_token), | |
|
93 | ] | |
|
94 | response = self.app.post(route_path('my_account_password'), form_data) | |
|
95 | ||
|
96 | assert_response = response.assert_response() | |
|
97 | assert assert_response.get_elements('.error-block') | |
|
98 | ||
|
99 | @mock.patch.object(UserModel, 'update_user', error_function) | |
|
100 | def test_invalid_change_password_exception(self, user_util): | |
|
101 | user = user_util.create_user(password=test_user_1_password) | |
|
102 | self.log_user(user.username, test_user_1_password) | |
|
103 | ||
|
104 | form_data = [ | |
|
105 | ('current_password', test_user_1_password), | |
|
106 | ('__start__', 'new_password:mapping'), | |
|
107 | ('new_password', '123456'), | |
|
108 | ('new_password-confirm', '123456'), | |
|
109 | ('__end__', 'new_password:mapping'), | |
|
110 | ('csrf_token', self.csrf_token), | |
|
111 | ] | |
|
112 | response = self.app.post(route_path('my_account_password'), form_data) | |
|
113 | assert_session_flash( | |
|
114 | response, 'Error occurred during update of user password') | |
|
115 | ||
|
116 | def test_password_is_updated_in_session_on_password_change(self, user_util): | |
|
117 | old_password = 'abcdef123' | |
|
118 | new_password = 'abcdef124' | |
|
119 | ||
|
120 | user = user_util.create_user(password=old_password) | |
|
121 | session = self.log_user(user.username, old_password) | |
|
122 | old_password_hash = session['password'] | |
|
123 | ||
|
124 | form_data = [ | |
|
125 | ('current_password', old_password), | |
|
126 | ('__start__', 'new_password:mapping'), | |
|
127 | ('new_password', new_password), | |
|
128 | ('new_password-confirm', new_password), | |
|
129 | ('__end__', 'new_password:mapping'), | |
|
130 | ('csrf_token', self.csrf_token), | |
|
131 | ] | |
|
132 | self.app.post(route_path('my_account_password'), form_data) | |
|
133 | ||
|
134 | response = self.app.get(route_path('home')) | |
|
135 | new_password_hash = response.session['rhodecode_user']['password'] | |
|
136 | ||
|
137 | assert old_password_hash != new_password_hash No newline at end of file |
@@ -20,6 +20,7 b'' | |||
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | from pylons import tmpl_context as c |
|
23 | from pyramid.httpexceptions import HTTPFound | |
|
23 | 24 | |
|
24 | 25 | from rhodecode.lib.utils2 import StrictAttributeDict |
|
25 | 26 | |
@@ -41,6 +42,7 b' class BaseAppView(object):' | |||
|
41 | 42 | self.context = context |
|
42 | 43 | self.session = request.session |
|
43 | 44 | self._rhodecode_user = request.user # auth user |
|
45 | self._rhodecode_db_user = self._rhodecode_user.get_instance() | |
|
44 | 46 | |
|
45 | 47 | def _get_local_tmpl_context(self): |
|
46 | 48 | c = TemplateArgs() |
@@ -23,6 +23,15 b' from rhodecode.apps._base import ADMIN_P' | |||
|
23 | 23 | |
|
24 | 24 | |
|
25 | 25 | def includeme(config): |
|
26 | ||
|
27 | config.add_route( | |
|
28 | name='my_account_password', | |
|
29 | pattern=ADMIN_PREFIX + '/my_account/password') | |
|
30 | ||
|
31 | config.add_route( | |
|
32 | name='my_account_password_update', | |
|
33 | pattern=ADMIN_PREFIX + '/my_account/password') | |
|
34 | ||
|
26 | 35 | config.add_route( |
|
27 | 36 | name='my_account_auth_tokens', |
|
28 | 37 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens') |
@@ -24,11 +24,14 b' from pyramid.httpexceptions import HTTPF' | |||
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import BaseAppView |
|
27 | from rhodecode import forms | |
|
27 | 28 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired |
|
28 | from rhodecode.lib.utils2 import safe_int | |
|
29 | 29 | from rhodecode.lib import helpers as h |
|
30 | from rhodecode.lib.utils2 import safe_int, md5 | |
|
30 | 31 | from rhodecode.model.auth_token import AuthTokenModel |
|
31 | 32 | from rhodecode.model.meta import Session |
|
33 | from rhodecode.model.user import UserModel | |
|
34 | from rhodecode.model.validation_schema.schemas import user_schema | |
|
32 | 35 | |
|
33 | 36 | log = logging.getLogger(__name__) |
|
34 | 37 | |
@@ -42,7 +45,6 b' class MyAccountView(BaseAppView):' | |||
|
42 | 45 | |
|
43 | 46 | def load_default_context(self): |
|
44 | 47 | c = self._get_local_tmpl_context() |
|
45 | ||
|
46 | 48 | c.user = c.auth_user.get_instance() |
|
47 | 49 | c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS |
|
48 | 50 | self._register_global_c(c) |
@@ -51,6 +53,69 b' class MyAccountView(BaseAppView):' | |||
|
51 | 53 | @LoginRequired() |
|
52 | 54 | @NotAnonymous() |
|
53 | 55 | @view_config( |
|
56 | route_name='my_account_password', request_method='GET', | |
|
57 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |
|
58 | def my_account_password(self): | |
|
59 | c = self.load_default_context() | |
|
60 | c.active = 'password' | |
|
61 | c.extern_type = c.user.extern_type | |
|
62 | ||
|
63 | schema = user_schema.ChangePasswordSchema().bind( | |
|
64 | username=c.user.username) | |
|
65 | ||
|
66 | form = forms.Form( | |
|
67 | schema, buttons=(forms.buttons.save, forms.buttons.reset)) | |
|
68 | ||
|
69 | c.form = form | |
|
70 | return self._get_template_context(c) | |
|
71 | ||
|
72 | @LoginRequired() | |
|
73 | @NotAnonymous() | |
|
74 | @CSRFRequired() | |
|
75 | @view_config( | |
|
76 | route_name='my_account_password', request_method='POST', | |
|
77 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |
|
78 | def my_account_password_update(self): | |
|
79 | _ = self.request.translate | |
|
80 | c = self.load_default_context() | |
|
81 | c.active = 'password' | |
|
82 | c.extern_type = c.user.extern_type | |
|
83 | ||
|
84 | schema = user_schema.ChangePasswordSchema().bind( | |
|
85 | username=c.user.username) | |
|
86 | ||
|
87 | form = forms.Form( | |
|
88 | schema, buttons=(forms.buttons.save, forms.buttons.reset)) | |
|
89 | ||
|
90 | if c.extern_type != 'rhodecode': | |
|
91 | raise HTTPFound(self.request.route_path('my_account_password')) | |
|
92 | ||
|
93 | controls = self.request.POST.items() | |
|
94 | try: | |
|
95 | valid_data = form.validate(controls) | |
|
96 | UserModel().update_user(c.user.user_id, **valid_data) | |
|
97 | c.user.update_userdata(force_password_change=False) | |
|
98 | Session().commit() | |
|
99 | except forms.ValidationFailure as e: | |
|
100 | c.form = e | |
|
101 | return self._get_template_context(c) | |
|
102 | ||
|
103 | except Exception: | |
|
104 | log.exception("Exception updating password") | |
|
105 | h.flash(_('Error occurred during update of user password'), | |
|
106 | category='error') | |
|
107 | else: | |
|
108 | instance = c.auth_user.get_instance() | |
|
109 | self.session.setdefault('rhodecode_user', {}).update( | |
|
110 | {'password': md5(instance.password)}) | |
|
111 | self.session.save() | |
|
112 | h.flash(_("Successfully updated password"), category='success') | |
|
113 | ||
|
114 | raise HTTPFound(self.request.route_path('my_account_password')) | |
|
115 | ||
|
116 | @LoginRequired() | |
|
117 | @NotAnonymous() | |
|
118 | @view_config( | |
|
54 | 119 | route_name='my_account_auth_tokens', request_method='GET', |
|
55 | 120 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
56 | 121 | def my_account_auth_tokens(self): |
@@ -512,8 +512,10 b' def make_map(config):' | |||
|
512 | 512 | m.connect('my_account', '/my_account', |
|
513 | 513 | action='my_account_update', conditions={'method': ['POST']}) |
|
514 | 514 | |
|
515 | # NOTE(marcink): this needs to be kept for password force flag to be | |
|
516 | # handler, remove after migration to pyramid | |
|
515 | 517 | m.connect('my_account_password', '/my_account/password', |
|
516 |
action='my_account_password', conditions={'method': ['GET' |
|
|
518 | action='my_account_password', conditions={'method': ['GET']}) | |
|
517 | 519 | |
|
518 | 520 | m.connect('my_account_repos', '/my_account/repos', |
|
519 | 521 | action='my_account_repos', conditions={'method': ['GET']}) |
@@ -34,19 +34,17 b' from pylons.controllers.util import redi' | |||
|
34 | 34 | from pylons.i18n.translation import _ |
|
35 | 35 | from sqlalchemy.orm import joinedload |
|
36 | 36 | |
|
37 | from rhodecode import forms | |
|
38 | 37 | from rhodecode.lib import helpers as h |
|
39 | 38 | from rhodecode.lib import auth |
|
40 | 39 | from rhodecode.lib.auth import ( |
|
41 | 40 | LoginRequired, NotAnonymous, AuthUser) |
|
42 | 41 | from rhodecode.lib.base import BaseController, render |
|
43 | 42 | from rhodecode.lib.utils import jsonify |
|
44 |
from rhodecode.lib.utils2 import safe_int, |
|
|
43 | from rhodecode.lib.utils2 import safe_int, str2bool | |
|
45 | 44 | from rhodecode.lib.ext_json import json |
|
46 | 45 | from rhodecode.lib.channelstream import channelstream_request, \ |
|
47 | 46 | ChannelstreamException |
|
48 | 47 | |
|
49 | from rhodecode.model.validation_schema.schemas import user_schema | |
|
50 | 48 | from rhodecode.model.db import ( |
|
51 | 49 | Repository, PullRequest, UserEmailMap, User, UserFollowing) |
|
52 | 50 | from rhodecode.model.forms import UserForm |
@@ -194,47 +192,6 b' class MyAccountController(BaseController' | |||
|
194 | 192 | force_defaults=False |
|
195 | 193 | ) |
|
196 | 194 | |
|
197 | @auth.CSRFRequired(except_methods=['GET']) | |
|
198 | def my_account_password(self): | |
|
199 | c.active = 'password' | |
|
200 | self.__load_data() | |
|
201 | c.extern_type = c.user.extern_type | |
|
202 | ||
|
203 | schema = user_schema.ChangePasswordSchema().bind( | |
|
204 | username=c.rhodecode_user.username) | |
|
205 | ||
|
206 | form = forms.Form(schema, | |
|
207 | buttons=(forms.buttons.save, forms.buttons.reset)) | |
|
208 | ||
|
209 | if request.method == 'POST' and c.extern_type == 'rhodecode': | |
|
210 | controls = request.POST.items() | |
|
211 | try: | |
|
212 | valid_data = form.validate(controls) | |
|
213 | UserModel().update_user(c.rhodecode_user.user_id, **valid_data) | |
|
214 | instance = c.rhodecode_user.get_instance() | |
|
215 | instance.update_userdata(force_password_change=False) | |
|
216 | Session().commit() | |
|
217 | except forms.ValidationFailure as e: | |
|
218 | request.session.flash( | |
|
219 | _('Error occurred during update of user password'), | |
|
220 | queue='error') | |
|
221 | form = e | |
|
222 | except Exception: | |
|
223 | log.exception("Exception updating password") | |
|
224 | request.session.flash( | |
|
225 | _('Error occurred during update of user password'), | |
|
226 | queue='error') | |
|
227 | else: | |
|
228 | session.setdefault('rhodecode_user', {}).update( | |
|
229 | {'password': md5(instance.password)}) | |
|
230 | session.save() | |
|
231 | request.session.flash( | |
|
232 | _("Successfully updated password"), queue='success') | |
|
233 | return redirect(url('my_account_password')) | |
|
234 | ||
|
235 | c.form = form | |
|
236 | return render('admin/my_account/my_account.mako') | |
|
237 | ||
|
238 | 195 | def my_account_repos(self): |
|
239 | 196 | c.active = 'repos' |
|
240 | 197 | self.__load_data() |
@@ -27,7 +27,7 b'' | |||
|
27 | 27 | <div class="sidebar"> |
|
28 | 28 | <ul class="nav nav-pills nav-stacked"> |
|
29 | 29 | <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('Profile')}</a></li> |
|
30 |
<li class="${'active' if c.active=='password' else ''}"><a href="${h. |
|
|
30 | <li class="${'active' if c.active=='password' else ''}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li> | |
|
31 | 31 | <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li> |
|
32 | 32 | ## TODO: Find a better integration of oauth views into navigation. |
|
33 | 33 | <% my_account_oauth_url = h.route_path_or_none('my_account_oauth') %> |
@@ -21,9 +21,7 b'' | |||
|
21 | 21 | import pytest |
|
22 | 22 | |
|
23 | 23 | from rhodecode.lib import helpers as h |
|
24 | from rhodecode.lib.auth import check_password | |
|
25 | from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys | |
|
26 | from rhodecode.model.meta import Session | |
|
24 | from rhodecode.model.db import User, UserFollowing, Repository | |
|
27 | 25 | from rhodecode.tests import ( |
|
28 | 26 | TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, |
|
29 | 27 | assert_session_flash) |
@@ -253,74 +251,3 b' class TestMyAccountController(TestContro' | |||
|
253 | 251 | edit=False, old_data={})._messages['username_exists'] |
|
254 | 252 | msg = h.html_escape(msg % {'username': 'test_admin'}) |
|
255 | 253 | response.mustcontain(u"%s" % msg) |
|
256 | ||
|
257 | def test_valid_change_password(self, user_util): | |
|
258 | new_password = 'my_new_valid_password' | |
|
259 | user = user_util.create_user(password=self.test_user_1_password) | |
|
260 | session = self.log_user(user.username, self.test_user_1_password) | |
|
261 | form_data = [ | |
|
262 | ('current_password', self.test_user_1_password), | |
|
263 | ('__start__', 'new_password:mapping'), | |
|
264 | ('new_password', new_password), | |
|
265 | ('new_password-confirm', new_password), | |
|
266 | ('__end__', 'new_password:mapping'), | |
|
267 | ('csrf_token', self.csrf_token), | |
|
268 | ] | |
|
269 | response = self.app.post(url('my_account_password'), form_data).follow() | |
|
270 | assert 'Successfully updated password' in response | |
|
271 | ||
|
272 | # check_password depends on user being in session | |
|
273 | Session().add(user) | |
|
274 | try: | |
|
275 | assert check_password(new_password, user.password) | |
|
276 | finally: | |
|
277 | Session().expunge(user) | |
|
278 | ||
|
279 | @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [ | |
|
280 | ('', 'abcdef123', 'abcdef123'), | |
|
281 | ('wrong_pw', 'abcdef123', 'abcdef123'), | |
|
282 | (test_user_1_password, test_user_1_password, test_user_1_password), | |
|
283 | (test_user_1_password, '', ''), | |
|
284 | (test_user_1_password, 'abcdef123', ''), | |
|
285 | (test_user_1_password, '', 'abcdef123'), | |
|
286 | (test_user_1_password, 'not_the', 'same_pw'), | |
|
287 | (test_user_1_password, 'short', 'short'), | |
|
288 | ]) | |
|
289 | def test_invalid_change_password(self, current_pw, new_pw, confirm_pw, | |
|
290 | user_util): | |
|
291 | user = user_util.create_user(password=self.test_user_1_password) | |
|
292 | session = self.log_user(user.username, self.test_user_1_password) | |
|
293 | old_password_hash = session['password'] | |
|
294 | form_data = [ | |
|
295 | ('current_password', current_pw), | |
|
296 | ('__start__', 'new_password:mapping'), | |
|
297 | ('new_password', new_pw), | |
|
298 | ('new_password-confirm', confirm_pw), | |
|
299 | ('__end__', 'new_password:mapping'), | |
|
300 | ('csrf_token', self.csrf_token), | |
|
301 | ] | |
|
302 | response = self.app.post(url('my_account_password'), form_data) | |
|
303 | assert 'Error occurred' in response | |
|
304 | ||
|
305 | def test_password_is_updated_in_session_on_password_change(self, user_util): | |
|
306 | old_password = 'abcdef123' | |
|
307 | new_password = 'abcdef124' | |
|
308 | ||
|
309 | user = user_util.create_user(password=old_password) | |
|
310 | session = self.log_user(user.username, old_password) | |
|
311 | old_password_hash = session['password'] | |
|
312 | ||
|
313 | form_data = [ | |
|
314 | ('current_password', old_password), | |
|
315 | ('__start__', 'new_password:mapping'), | |
|
316 | ('new_password', new_password), | |
|
317 | ('new_password-confirm', new_password), | |
|
318 | ('__end__', 'new_password:mapping'), | |
|
319 | ('csrf_token', self.csrf_token), | |
|
320 | ] | |
|
321 | self.app.post(url('my_account_password'), form_data) | |
|
322 | ||
|
323 | response = self.app.get(url('home')) | |
|
324 | new_password_hash = response.session['rhodecode_user']['password'] | |
|
325 | ||
|
326 | assert old_password_hash != new_password_hash |
General Comments 0
You need to be logged in to leave comments.
Login now