Show More
@@ -0,0 +1,106 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 | ||||
|
23 | from rhodecode.config.routing import ADMIN_PREFIX | |||
|
24 | from rhodecode.tests import ( | |||
|
25 | TestController, clear_all_caches, url, | |||
|
26 | TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) | |||
|
27 | from rhodecode.tests.fixture import Fixture | |||
|
28 | from rhodecode.tests.utils import AssertResponse | |||
|
29 | ||||
|
30 | fixture = Fixture() | |||
|
31 | ||||
|
32 | # Hardcode URLs because we don't have a request object to use | |||
|
33 | # pyramids URL generation methods. | |||
|
34 | index_url = '/' | |||
|
35 | login_url = ADMIN_PREFIX + '/login' | |||
|
36 | logut_url = ADMIN_PREFIX + '/logout' | |||
|
37 | register_url = ADMIN_PREFIX + '/register' | |||
|
38 | pwd_reset_url = ADMIN_PREFIX + '/password_reset' | |||
|
39 | pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation' | |||
|
40 | ||||
|
41 | ||||
|
42 | class TestPasswordReset(TestController): | |||
|
43 | ||||
|
44 | @pytest.mark.parametrize( | |||
|
45 | 'pwd_reset_setting, show_link, show_reset', [ | |||
|
46 | ('hg.password_reset.enabled', True, True), | |||
|
47 | ('hg.password_reset.hidden', False, True), | |||
|
48 | ('hg.password_reset.disabled', False, False), | |||
|
49 | ]) | |||
|
50 | def test_password_reset_settings( | |||
|
51 | self, pwd_reset_setting, show_link, show_reset): | |||
|
52 | clear_all_caches() | |||
|
53 | self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) | |||
|
54 | params = { | |||
|
55 | 'csrf_token': self.csrf_token, | |||
|
56 | 'anonymous': 'True', | |||
|
57 | 'default_register': 'hg.register.auto_activate', | |||
|
58 | 'default_register_message': '', | |||
|
59 | 'default_password_reset': pwd_reset_setting, | |||
|
60 | 'default_extern_activate': 'hg.extern_activate.auto', | |||
|
61 | } | |||
|
62 | resp = self.app.post(url('admin_permissions_application'), params=params) | |||
|
63 | self.logout_user() | |||
|
64 | ||||
|
65 | login_page = self.app.get(login_url) | |||
|
66 | asr_login = AssertResponse(login_page) | |||
|
67 | index_page = self.app.get(index_url) | |||
|
68 | asr_index = AssertResponse(index_page) | |||
|
69 | ||||
|
70 | if show_link: | |||
|
71 | asr_login.one_element_exists('a.pwd_reset') | |||
|
72 | asr_index.one_element_exists('a.pwd_reset') | |||
|
73 | else: | |||
|
74 | asr_login.no_element_exists('a.pwd_reset') | |||
|
75 | asr_index.no_element_exists('a.pwd_reset') | |||
|
76 | ||||
|
77 | response = self.app.get(pwd_reset_url) | |||
|
78 | ||||
|
79 | assert_response = AssertResponse(response) | |||
|
80 | if show_reset: | |||
|
81 | response.mustcontain('Send password reset email') | |||
|
82 | assert_response.one_element_exists('#email') | |||
|
83 | assert_response.one_element_exists('#send') | |||
|
84 | else: | |||
|
85 | response.mustcontain('Password reset is disabled.') | |||
|
86 | assert_response.no_element_exists('#email') | |||
|
87 | assert_response.no_element_exists('#send') | |||
|
88 | ||||
|
89 | def test_password_form_disabled(self): | |||
|
90 | self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) | |||
|
91 | params = { | |||
|
92 | 'csrf_token': self.csrf_token, | |||
|
93 | 'anonymous': 'True', | |||
|
94 | 'default_register': 'hg.register.auto_activate', | |||
|
95 | 'default_register_message': '', | |||
|
96 | 'default_password_reset': 'hg.password_reset.disabled', | |||
|
97 | 'default_extern_activate': 'hg.extern_activate.auto', | |||
|
98 | } | |||
|
99 | self.app.post(url('admin_permissions_application'), params=params) | |||
|
100 | self.logout_user() | |||
|
101 | ||||
|
102 | response = self.app.post( | |||
|
103 | pwd_reset_url, {'email': 'lisa@rhodecode.com',} | |||
|
104 | ) | |||
|
105 | response = response.follow() | |||
|
106 | response.mustcontain('Password reset is disabled.') |
@@ -18,6 +18,7 b'' | |||||
18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
20 |
|
20 | |||
|
21 | import time | |||
21 | import collections |
|
22 | import collections | |
22 | import datetime |
|
23 | import datetime | |
23 | import formencode |
|
24 | import formencode | |
@@ -37,9 +38,10 b' from rhodecode.lib.auth import (' | |||||
37 | from rhodecode.lib.base import get_ip_addr |
|
38 | from rhodecode.lib.base import get_ip_addr | |
38 | from rhodecode.lib.exceptions import UserCreationError |
|
39 | from rhodecode.lib.exceptions import UserCreationError | |
39 | from rhodecode.lib.utils2 import safe_str |
|
40 | from rhodecode.lib.utils2 import safe_str | |
40 | from rhodecode.model.db import User |
|
41 | from rhodecode.model.db import User, UserApiKeys | |
41 | from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm |
|
42 | from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm | |
42 | from rhodecode.model.meta import Session |
|
43 | from rhodecode.model.meta import Session | |
|
44 | from rhodecode.model.auth_token import AuthTokenModel | |||
43 | from rhodecode.model.settings import SettingsModel |
|
45 | from rhodecode.model.settings import SettingsModel | |
44 | from rhodecode.model.user import UserModel |
|
46 | from rhodecode.model.user import UserModel | |
45 | from rhodecode.translation import _ |
|
47 | from rhodecode.translation import _ | |
@@ -289,17 +291,24 b' class LoginView(object):' | |||||
289 | 'errors': {}, |
|
291 | 'errors': {}, | |
290 | } |
|
292 | } | |
291 |
|
293 | |||
|
294 | # always send implicit message to prevent from discovery of | |||
|
295 | # matching emails | |||
|
296 | msg = _('If such email exists, a password reset link was sent to it.') | |||
|
297 | ||||
292 | if self.request.POST: |
|
298 | if self.request.POST: | |
|
299 | if h.HasPermissionAny('hg.password_reset.disabled')(): | |||
|
300 | _email = self.request.POST.get('email', '') | |||
|
301 | log.error('Failed attempt to reset password for `%s`.', _email) | |||
|
302 | self.session.flash(_('Password reset has been disabled.'), | |||
|
303 | queue='error') | |||
|
304 | return HTTPFound(self.request.route_path('reset_password')) | |||
|
305 | ||||
293 | password_reset_form = PasswordResetForm()() |
|
306 | password_reset_form = PasswordResetForm()() | |
294 | try: |
|
307 | try: | |
295 | form_result = password_reset_form.to_python( |
|
308 | form_result = password_reset_form.to_python( | |
296 | self.request.params) |
|
309 | self.request.params) | |
297 | if h.HasPermissionAny('hg.password_reset.disabled')(): |
|
310 | user_email = form_result['email'] | |
298 | log.error('Failed attempt to reset password for %s.', form_result['email'] ) |
|
311 | ||
299 | self.session.flash( |
|
|||
300 | _('Password reset has been disabled.'), |
|
|||
301 | queue='error') |
|
|||
302 | return HTTPFound(self.request.route_path('reset_password')) |
|
|||
303 | if captcha.active: |
|
312 | if captcha.active: | |
304 | response = submit( |
|
313 | response = submit( | |
305 | self.request.params.get('recaptcha_challenge_field'), |
|
314 | self.request.params.get('recaptcha_challenge_field'), | |
@@ -310,43 +319,66 b' class LoginView(object):' | |||||
310 | _value = form_result |
|
319 | _value = form_result | |
311 | _msg = _('Bad captcha') |
|
320 | _msg = _('Bad captcha') | |
312 | error_dict = {'recaptcha_field': _msg} |
|
321 | error_dict = {'recaptcha_field': _msg} | |
313 |
raise formencode.Invalid( |
|
322 | raise formencode.Invalid( | |
314 |
|
|
323 | _msg, _value, None, error_dict=error_dict) | |
|
324 | # Generate reset URL and send mail. | |||
|
325 | user = User.get_by_email(user_email) | |||
315 |
|
326 | |||
316 | # Generate reset URL and send mail. |
|
327 | # generate password reset token that expires in 10minutes | |
317 | user_email = form_result['email'] |
|
328 | desc = 'Generated token for password reset from {}'.format( | |
318 | user = User.get_by_email(user_email) |
|
329 | datetime.datetime.now().isoformat()) | |
|
330 | reset_token = AuthTokenModel().create( | |||
|
331 | user, lifetime=10, | |||
|
332 | description=desc, | |||
|
333 | role=UserApiKeys.ROLE_PASSWORD_RESET) | |||
|
334 | Session().commit() | |||
|
335 | ||||
|
336 | log.debug('Successfully created password recovery token') | |||
319 | password_reset_url = self.request.route_url( |
|
337 | password_reset_url = self.request.route_url( | |
320 | 'reset_password_confirmation', |
|
338 | 'reset_password_confirmation', | |
321 |
_query={'key': |
|
339 | _query={'key': reset_token.api_key}) | |
322 | UserModel().reset_password_link( |
|
340 | UserModel().reset_password_link( | |
323 | form_result, password_reset_url) |
|
341 | form_result, password_reset_url) | |
324 |
|
||||
325 | # Display success message and redirect. |
|
342 | # Display success message and redirect. | |
326 | self.session.flash( |
|
343 | self.session.flash(msg, queue='success') | |
327 | _('Your password reset link was sent'), |
|
344 | return HTTPFound(self.request.route_path('reset_password')) | |
328 | queue='success') |
|
|||
329 | return HTTPFound(self.request.route_path('login')) |
|
|||
330 |
|
345 | |||
331 | except formencode.Invalid as errors: |
|
346 | except formencode.Invalid as errors: | |
332 | render_ctx.update({ |
|
347 | render_ctx.update({ | |
333 | 'defaults': errors.value, |
|
348 | 'defaults': errors.value, | |
334 | 'errors': errors.error_dict, |
|
|||
335 | }) |
|
349 | }) | |
|
350 | log.debug('faking response on invalid password reset') | |||
|
351 | # make this take 2s, to prevent brute forcing. | |||
|
352 | time.sleep(2) | |||
|
353 | self.session.flash(msg, queue='success') | |||
|
354 | return HTTPFound(self.request.route_path('reset_password')) | |||
336 |
|
355 | |||
337 | return render_ctx |
|
356 | return render_ctx | |
338 |
|
357 | |||
339 | @view_config(route_name='reset_password_confirmation', |
|
358 | @view_config(route_name='reset_password_confirmation', | |
340 | request_method='GET') |
|
359 | request_method='GET') | |
341 | def password_reset_confirmation(self): |
|
360 | def password_reset_confirmation(self): | |
|
361 | ||||
342 | if self.request.GET and self.request.GET.get('key'): |
|
362 | if self.request.GET and self.request.GET.get('key'): | |
|
363 | # make this take 2s, to prevent brute forcing. | |||
|
364 | time.sleep(2) | |||
|
365 | ||||
|
366 | token = AuthTokenModel().get_auth_token( | |||
|
367 | self.request.GET.get('key')) | |||
|
368 | ||||
|
369 | # verify token is the correct role | |||
|
370 | if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET: | |||
|
371 | log.debug('Got token with role:%s expected is %s', | |||
|
372 | getattr(token, 'role', 'EMPTY_TOKEN'), | |||
|
373 | UserApiKeys.ROLE_PASSWORD_RESET) | |||
|
374 | self.session.flash( | |||
|
375 | _('Given reset token is invalid'), queue='error') | |||
|
376 | return HTTPFound(self.request.route_path('reset_password')) | |||
|
377 | ||||
343 | try: |
|
378 | try: | |
344 | user = User.get_by_auth_token(self.request.GET.get('key')) |
|
379 | owner = token.user | |
345 | password_reset_url = self.request.route_url( |
|
380 | data = {'email': owner.email, 'token': token.api_key} | |
346 |
|
|
381 | UserModel().reset_password(data) | |
347 | _query={'key': user.api_key}) |
|
|||
348 | data = {'email': user.email} |
|
|||
349 | UserModel().reset_password(data, password_reset_url) |
|
|||
350 | self.session.flash( |
|
382 | self.session.flash( | |
351 | _('Your password reset was successful, ' |
|
383 | _('Your password reset was successful, ' | |
352 | 'a new password has been sent to your email'), |
|
384 | 'a new password has been sent to your email'), |
@@ -41,7 +41,7 b' class AuthTokenModel(BaseModel):' | |||||
41 | """ |
|
41 | """ | |
42 | :param user: user or user_id |
|
42 | :param user: user or user_id | |
43 | :param description: description of ApiKey |
|
43 | :param description: description of ApiKey | |
44 |
:param lifetime: expiration time in |
|
44 | :param lifetime: expiration time in minutes | |
45 | :param role: role for the apikey |
|
45 | :param role: role for the apikey | |
46 | """ |
|
46 | """ | |
47 | from rhodecode.lib.auth import generate_auth_token |
|
47 | from rhodecode.lib.auth import generate_auth_token | |
@@ -85,3 +85,13 b' class AuthTokenModel(BaseModel):' | |||||
85 | .filter(or_(UserApiKeys.expires == -1, |
|
85 | .filter(or_(UserApiKeys.expires == -1, | |
86 | UserApiKeys.expires >= time.time())) |
|
86 | UserApiKeys.expires >= time.time())) | |
87 | return user_auth_tokens |
|
87 | return user_auth_tokens | |
|
88 | ||||
|
89 | def get_auth_token(self, auth_token): | |||
|
90 | auth_token = UserApiKeys.query().filter( | |||
|
91 | UserApiKeys.api_key == auth_token) | |||
|
92 | auth_token = auth_token \ | |||
|
93 | .filter(or_(UserApiKeys.expires == -1, | |||
|
94 | UserApiKeys.expires >= time.time()))\ | |||
|
95 | .first() | |||
|
96 | ||||
|
97 | return auth_token |
@@ -943,6 +943,8 b' class UserApiKeys(Base, BaseModel):' | |||||
943 | ROLE_VCS = 'token_role_vcs' |
|
943 | ROLE_VCS = 'token_role_vcs' | |
944 | ROLE_API = 'token_role_api' |
|
944 | ROLE_API = 'token_role_api' | |
945 | ROLE_FEED = 'token_role_feed' |
|
945 | ROLE_FEED = 'token_role_feed' | |
|
946 | ROLE_PASSWORD_RESET = 'token_password_reset' | |||
|
947 | ||||
946 | ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] |
|
948 | ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] | |
947 |
|
949 | |||
948 | user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
950 | user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
@@ -538,7 +538,7 b' class UserModel(BaseModel):' | |||||
538 |
|
538 | |||
539 | return True |
|
539 | return True | |
540 |
|
540 | |||
541 |
def reset_password(self, data |
|
541 | def reset_password(self, data): | |
542 | from rhodecode.lib.celerylib import tasks, run_task |
|
542 | from rhodecode.lib.celerylib import tasks, run_task | |
543 | from rhodecode.model.notification import EmailNotificationModel |
|
543 | from rhodecode.model.notification import EmailNotificationModel | |
544 | from rhodecode.lib import auth |
|
544 | from rhodecode.lib import auth | |
@@ -554,8 +554,15 b' class UserModel(BaseModel):' | |||||
554 | user.update_userdata(force_password_change=True) |
|
554 | user.update_userdata(force_password_change=True) | |
555 |
|
555 | |||
556 | Session().add(user) |
|
556 | Session().add(user) | |
|
557 | ||||
|
558 | # now delete the token in question | |||
|
559 | UserApiKeys = AuthTokenModel.cls | |||
|
560 | UserApiKeys().query().filter( | |||
|
561 | UserApiKeys.api_key == data['token']).delete() | |||
|
562 | ||||
557 | Session().commit() |
|
563 | Session().commit() | |
558 | log.info('successfully reset password for `%s`', user_email) |
|
564 | log.info('successfully reset password for `%s`', user_email) | |
|
565 | ||||
559 | if new_passwd is None: |
|
566 | if new_passwd is None: | |
560 | raise Exception('unable to generate new password') |
|
567 | raise Exception('unable to generate new password') | |
561 |
|
568 | |||
@@ -563,7 +570,6 b' class UserModel(BaseModel):' | |||||
563 |
|
570 | |||
564 | email_kwargs = { |
|
571 | email_kwargs = { | |
565 | 'new_password': new_passwd, |
|
572 | 'new_password': new_passwd, | |
566 | 'password_reset_url': pwd_reset_url, |
|
|||
567 | 'user': user, |
|
573 | 'user': user, | |
568 | 'email': user_email, |
|
574 | 'email': user_email, | |
569 | 'date': datetime.datetime.now() |
|
575 | 'date': datetime.datetime.now() | |
@@ -571,7 +577,8 b' class UserModel(BaseModel):' | |||||
571 |
|
577 | |||
572 | (subject, headers, email_body, |
|
578 | (subject, headers, email_body, | |
573 | email_body_plaintext) = EmailNotificationModel().render_email( |
|
579 | email_body_plaintext) = EmailNotificationModel().render_email( | |
574 |
EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, |
|
580 | EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, | |
|
581 | **email_kwargs) | |||
575 |
|
582 | |||
576 | recipients = [user_email] |
|
583 | recipients = [user_email] | |
577 |
|
584 |
@@ -16,6 +16,7 b' There was a request to reset your passwo' | |||||
16 | You can continue, and generate new password by clicking following URL: |
|
16 | You can continue, and generate new password by clicking following URL: | |
17 | ${password_reset_url} |
|
17 | ${password_reset_url} | |
18 |
|
18 | |||
|
19 | This link will be active for 10 minutes. | |||
19 | ${self.plaintext_footer()} |
|
20 | ${self.plaintext_footer()} | |
20 | </%def> |
|
21 | </%def> | |
21 |
|
22 | |||
@@ -28,4 +29,5 b' There was a request to reset your passwo' | |||||
28 | <strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong> |
|
29 | <strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong> | |
29 | </p><p> |
|
30 | </p><p> | |
30 | <a href="${password_reset_url}">${_('Generate new password here')}.</a> |
|
31 | <a href="${password_reset_url}">${_('Generate new password here')}.</a> | |
|
32 | This link will be active for 10 minutes. | |||
31 | </p> |
|
33 | </p> |
@@ -9,12 +9,11 b' Your new RhodeCode password' | |||||
9 | <%def name="body_plaintext()" filter="n,trim"> |
|
9 | <%def name="body_plaintext()" filter="n,trim"> | |
10 | Hi ${user.username}, |
|
10 | Hi ${user.username}, | |
11 |
|
11 | |||
12 | There was a request to reset your password using the email address ${email} on ${h.format_date(date)} |
|
12 | Below is your new access password for RhodeCode. | |
13 |
|
13 | |||
14 | *If you didn't do this, please contact your RhodeCode administrator.* |
|
14 | *If you didn't do this, please contact your RhodeCode administrator.* | |
15 |
|
15 | |||
16 | You can continue, and generate new password by clicking following URL: |
|
16 | password: ${new_password} | |
17 | ${password_reset_url} |
|
|||
18 |
|
17 | |||
19 | ${self.plaintext_footer()} |
|
18 | ${self.plaintext_footer()} | |
20 | </%def> |
|
19 | </%def> | |
@@ -27,4 +26,4 b' Below is your new access password for Rh' | |||||
27 | <br/> |
|
26 | <br/> | |
28 | <strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong> |
|
27 | <strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong> | |
29 | </p> |
|
28 | </p> | |
30 |
<p>password: < |
|
29 | <p>password: <pre>${new_password}</pre> |
@@ -25,15 +25,13 b' import pytest' | |||||
25 |
|
25 | |||
26 | from rhodecode.config.routing import ADMIN_PREFIX |
|
26 | from rhodecode.config.routing import ADMIN_PREFIX | |
27 | from rhodecode.tests import ( |
|
27 | from rhodecode.tests import ( | |
28 | TestController, assert_session_flash, clear_all_caches, url, |
|
28 | assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN) | |
29 | HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) |
|
|||
30 | from rhodecode.tests.fixture import Fixture |
|
29 | from rhodecode.tests.fixture import Fixture | |
31 | from rhodecode.tests.utils import AssertResponse, get_session_from_response |
|
30 | from rhodecode.tests.utils import AssertResponse, get_session_from_response | |
32 |
from rhodecode.lib.auth import check_password |
|
31 | from rhodecode.lib.auth import check_password | |
33 | from rhodecode.lib import helpers as h |
|
|||
34 | from rhodecode.model.auth_token import AuthTokenModel |
|
32 | from rhodecode.model.auth_token import AuthTokenModel | |
35 | from rhodecode.model import validators |
|
33 | from rhodecode.model import validators | |
36 | from rhodecode.model.db import User, Notification |
|
34 | from rhodecode.model.db import User, Notification, UserApiKeys | |
37 | from rhodecode.model.meta import Session |
|
35 | from rhodecode.model.meta import Session | |
38 |
|
36 | |||
39 | fixture = Fixture() |
|
37 | fixture = Fixture() | |
@@ -49,7 +47,7 b" pwd_reset_confirm_url = ADMIN_PREFIX + '" | |||||
49 |
|
47 | |||
50 |
|
48 | |||
51 | @pytest.mark.usefixtures('app') |
|
49 | @pytest.mark.usefixtures('app') | |
52 | class TestLoginController: |
|
50 | class TestLoginController(object): | |
53 | destroy_users = set() |
|
51 | destroy_users = set() | |
54 |
|
52 | |||
55 | @classmethod |
|
53 | @classmethod | |
@@ -374,54 +372,42 b' class TestLoginController:' | |||||
374 | def test_forgot_password_wrong_mail(self): |
|
372 | def test_forgot_password_wrong_mail(self): | |
375 | bad_email = 'marcin@wrongmail.org' |
|
373 | bad_email = 'marcin@wrongmail.org' | |
376 | response = self.app.post( |
|
374 | response = self.app.post( | |
377 | pwd_reset_url, |
|
375 | pwd_reset_url, {'email': bad_email, } | |
378 | {'email': bad_email, } |
|
|||
379 | ) |
|
376 | ) | |
|
377 | assert_session_flash(response, | |||
|
378 | 'If such email exists, a password reset link was sent to it.') | |||
380 |
|
379 | |||
381 | msg = validators.ValidSystemEmail()._messages['non_existing_email'] |
|
380 | def test_forgot_password(self, user_util): | |
382 | msg = h.html_escape(msg % {'email': bad_email}) |
|
|||
383 | response.mustcontain() |
|
|||
384 |
|
||||
385 | def test_forgot_password(self): |
|
|||
386 | response = self.app.get(pwd_reset_url) |
|
381 | response = self.app.get(pwd_reset_url) | |
387 | assert response.status == '200 OK' |
|
382 | assert response.status == '200 OK' | |
388 |
|
383 | |||
389 | username = 'test_password_reset_1' |
|
384 | user = user_util.create_user() | |
390 | password = 'qweqwe' |
|
385 | user_id = user.user_id | |
391 | email = 'marcin@python-works.com' |
|
386 | email = user.email | |
392 | name = 'passwd' |
|
|||
393 | lastname = 'reset' |
|
|||
394 |
|
387 | |||
395 | new = User() |
|
388 | response = self.app.post(pwd_reset_url, {'email': email, }) | |
396 | new.username = username |
|
|||
397 | new.password = password |
|
|||
398 | new.email = email |
|
|||
399 | new.name = name |
|
|||
400 | new.lastname = lastname |
|
|||
401 | new.api_key = generate_auth_token(username) |
|
|||
402 | Session().add(new) |
|
|||
403 | Session().commit() |
|
|||
404 |
|
389 | |||
405 | response = self.app.post(pwd_reset_url, |
|
390 | assert_session_flash(response, | |
406 | {'email': email, }) |
|
391 | 'If such email exists, a password reset link was sent to it.') | |
407 |
|
||||
408 | assert_session_flash( |
|
|||
409 | response, 'Your password reset link was sent') |
|
|||
410 |
|
||||
411 | response = response.follow() |
|
|||
412 |
|
392 | |||
413 | # BAD KEY |
|
393 | # BAD KEY | |
414 |
|
394 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey') | ||
415 | key = "bad" |
|
|||
416 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) |
|
|||
417 | response = self.app.get(confirm_url) |
|
395 | response = self.app.get(confirm_url) | |
418 | assert response.status == '302 Found' |
|
396 | assert response.status == '302 Found' | |
419 | assert response.location.endswith(pwd_reset_url) |
|
397 | assert response.location.endswith(pwd_reset_url) | |
|
398 | assert_session_flash(response, 'Given reset token is invalid') | |||
|
399 | ||||
|
400 | response.follow() # cleanup flash | |||
420 |
|
401 | |||
421 | # GOOD KEY |
|
402 | # GOOD KEY | |
|
403 | key = UserApiKeys.query()\ | |||
|
404 | .filter(UserApiKeys.user_id == user_id)\ | |||
|
405 | .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\ | |||
|
406 | .first() | |||
422 |
|
407 | |||
423 | key = User.get_by_username(username).api_key |
|
408 | assert key | |
424 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) |
|
409 | ||
|
410 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key) | |||
425 | response = self.app.get(confirm_url) |
|
411 | response = self.app.get(confirm_url) | |
426 | assert response.status == '302 Found' |
|
412 | assert response.status == '302 Found' | |
427 | assert response.location.endswith(login_url) |
|
413 | assert response.location.endswith(login_url) | |
@@ -431,7 +417,7 b' class TestLoginController:' | |||||
431 | 'Your password reset was successful, ' |
|
417 | 'Your password reset was successful, ' | |
432 | 'a new password has been sent to your email') |
|
418 | 'a new password has been sent to your email') | |
433 |
|
419 | |||
434 |
|
|
420 | response.follow() | |
435 |
|
421 | |||
436 | def _get_api_whitelist(self, values=None): |
|
422 | def _get_api_whitelist(self, values=None): | |
437 | config = {'api_access_controllers_whitelist': values or []} |
|
423 | config = {'api_access_controllers_whitelist': values or []} | |
@@ -522,70 +508,3 b' class TestLoginController:' | |||||
522 | repo_name=HG_REPO, revision='tip', |
|
508 | repo_name=HG_REPO, revision='tip', | |
523 | api_key=new_auth_token.api_key), |
|
509 | api_key=new_auth_token.api_key), | |
524 | status=302) |
|
510 | status=302) | |
525 |
|
||||
526 |
|
||||
527 | class TestPasswordReset(TestController): |
|
|||
528 |
|
||||
529 | @pytest.mark.parametrize( |
|
|||
530 | 'pwd_reset_setting, show_link, show_reset', [ |
|
|||
531 | ('hg.password_reset.enabled', True, True), |
|
|||
532 | ('hg.password_reset.hidden', False, True), |
|
|||
533 | ('hg.password_reset.disabled', False, False), |
|
|||
534 | ]) |
|
|||
535 | def test_password_reset_settings( |
|
|||
536 | self, pwd_reset_setting, show_link, show_reset): |
|
|||
537 | clear_all_caches() |
|
|||
538 | self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) |
|
|||
539 | params = { |
|
|||
540 | 'csrf_token': self.csrf_token, |
|
|||
541 | 'anonymous': 'True', |
|
|||
542 | 'default_register': 'hg.register.auto_activate', |
|
|||
543 | 'default_register_message': '', |
|
|||
544 | 'default_password_reset': pwd_reset_setting, |
|
|||
545 | 'default_extern_activate': 'hg.extern_activate.auto', |
|
|||
546 | } |
|
|||
547 | resp = self.app.post(url('admin_permissions_application'), params=params) |
|
|||
548 | self.logout_user() |
|
|||
549 |
|
||||
550 | login_page = self.app.get(login_url) |
|
|||
551 | asr_login = AssertResponse(login_page) |
|
|||
552 | index_page = self.app.get(index_url) |
|
|||
553 | asr_index = AssertResponse(index_page) |
|
|||
554 |
|
||||
555 | if show_link: |
|
|||
556 | asr_login.one_element_exists('a.pwd_reset') |
|
|||
557 | asr_index.one_element_exists('a.pwd_reset') |
|
|||
558 | else: |
|
|||
559 | asr_login.no_element_exists('a.pwd_reset') |
|
|||
560 | asr_index.no_element_exists('a.pwd_reset') |
|
|||
561 |
|
||||
562 | pwdreset_page = self.app.get(pwd_reset_url) |
|
|||
563 |
|
||||
564 | asr_reset = AssertResponse(pwdreset_page) |
|
|||
565 | if show_reset: |
|
|||
566 | assert 'Send password reset email' in pwdreset_page |
|
|||
567 | asr_reset.one_element_exists('#email') |
|
|||
568 | asr_reset.one_element_exists('#send') |
|
|||
569 | else: |
|
|||
570 | assert 'Password reset is disabled.' in pwdreset_page |
|
|||
571 | asr_reset.no_element_exists('#email') |
|
|||
572 | asr_reset.no_element_exists('#send') |
|
|||
573 |
|
||||
574 | def test_password_form_disabled(self): |
|
|||
575 | self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) |
|
|||
576 | params = { |
|
|||
577 | 'csrf_token': self.csrf_token, |
|
|||
578 | 'anonymous': 'True', |
|
|||
579 | 'default_register': 'hg.register.auto_activate', |
|
|||
580 | 'default_register_message': '', |
|
|||
581 | 'default_password_reset': 'hg.password_reset.disabled', |
|
|||
582 | 'default_extern_activate': 'hg.extern_activate.auto', |
|
|||
583 | } |
|
|||
584 | self.app.post(url('admin_permissions_application'), params=params) |
|
|||
585 | self.logout_user() |
|
|||
586 |
|
||||
587 | pwdreset_page = self.app.post( |
|
|||
588 | pwd_reset_url, |
|
|||
589 | {'email': 'lisa@rhodecode.com',} |
|
|||
590 | ) |
|
|||
591 | assert 'Password reset is disabled.' in pwdreset_page |
|
General Comments 0
You need to be logged in to leave comments.
Login now