##// END OF EJS Templates
password-reset: strengthten security on password reset logic....
marcink -
r1471:9ea7077d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -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.')
@@ -1,358 +1,390 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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
24 import logging
25 import logging
25 import urlparse
26 import urlparse
26
27
27 from pylons import url
28 from pylons import url
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.httpexceptions import HTTPFound
29 from pyramid.view import view_config
30 from pyramid.view import view_config
30 from recaptcha.client.captcha import submit
31 from recaptcha.client.captcha import submit
31
32
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.events import UserRegistered
34 from rhodecode.events import UserRegistered
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
36 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
37 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
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 _
46
48
47
49
48 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
49
51
50 CaptchaData = collections.namedtuple(
52 CaptchaData = collections.namedtuple(
51 'CaptchaData', 'active, private_key, public_key')
53 'CaptchaData', 'active, private_key, public_key')
52
54
53
55
54 def _store_user_in_session(session, username, remember=False):
56 def _store_user_in_session(session, username, remember=False):
55 user = User.get_by_username(username, case_insensitive=True)
57 user = User.get_by_username(username, case_insensitive=True)
56 auth_user = AuthUser(user.user_id)
58 auth_user = AuthUser(user.user_id)
57 auth_user.set_authenticated()
59 auth_user.set_authenticated()
58 cs = auth_user.get_cookie_store()
60 cs = auth_user.get_cookie_store()
59 session['rhodecode_user'] = cs
61 session['rhodecode_user'] = cs
60 user.update_lastlogin()
62 user.update_lastlogin()
61 Session().commit()
63 Session().commit()
62
64
63 # If they want to be remembered, update the cookie
65 # If they want to be remembered, update the cookie
64 if remember:
66 if remember:
65 _year = (datetime.datetime.now() +
67 _year = (datetime.datetime.now() +
66 datetime.timedelta(seconds=60 * 60 * 24 * 365))
68 datetime.timedelta(seconds=60 * 60 * 24 * 365))
67 session._set_cookie_expires(_year)
69 session._set_cookie_expires(_year)
68
70
69 session.save()
71 session.save()
70
72
71 safe_cs = cs.copy()
73 safe_cs = cs.copy()
72 safe_cs['password'] = '****'
74 safe_cs['password'] = '****'
73 log.info('user %s is now authenticated and stored in '
75 log.info('user %s is now authenticated and stored in '
74 'session, session attrs %s', username, safe_cs)
76 'session, session attrs %s', username, safe_cs)
75
77
76 # dumps session attrs back to cookie
78 # dumps session attrs back to cookie
77 session._update_cookie_out()
79 session._update_cookie_out()
78 # we set new cookie
80 # we set new cookie
79 headers = None
81 headers = None
80 if session.request['set_cookie']:
82 if session.request['set_cookie']:
81 # send set-cookie headers back to response to update cookie
83 # send set-cookie headers back to response to update cookie
82 headers = [('Set-Cookie', session.request['cookie_out'])]
84 headers = [('Set-Cookie', session.request['cookie_out'])]
83 return headers
85 return headers
84
86
85
87
86 def get_came_from(request):
88 def get_came_from(request):
87 came_from = safe_str(request.GET.get('came_from', ''))
89 came_from = safe_str(request.GET.get('came_from', ''))
88 parsed = urlparse.urlparse(came_from)
90 parsed = urlparse.urlparse(came_from)
89 allowed_schemes = ['http', 'https']
91 allowed_schemes = ['http', 'https']
90 if parsed.scheme and parsed.scheme not in allowed_schemes:
92 if parsed.scheme and parsed.scheme not in allowed_schemes:
91 log.error('Suspicious URL scheme detected %s for url %s' %
93 log.error('Suspicious URL scheme detected %s for url %s' %
92 (parsed.scheme, parsed))
94 (parsed.scheme, parsed))
93 came_from = url('home')
95 came_from = url('home')
94 elif parsed.netloc and request.host != parsed.netloc:
96 elif parsed.netloc and request.host != parsed.netloc:
95 log.error('Suspicious NETLOC detected %s for url %s server url '
97 log.error('Suspicious NETLOC detected %s for url %s server url '
96 'is: %s' % (parsed.netloc, parsed, request.host))
98 'is: %s' % (parsed.netloc, parsed, request.host))
97 came_from = url('home')
99 came_from = url('home')
98 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
100 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
99 log.error('Header injection detected `%s` for url %s server url ' %
101 log.error('Header injection detected `%s` for url %s server url ' %
100 (parsed.path, parsed))
102 (parsed.path, parsed))
101 came_from = url('home')
103 came_from = url('home')
102
104
103 return came_from or url('home')
105 return came_from or url('home')
104
106
105
107
106 class LoginView(object):
108 class LoginView(object):
107
109
108 def __init__(self, context, request):
110 def __init__(self, context, request):
109 self.request = request
111 self.request = request
110 self.context = context
112 self.context = context
111 self.session = request.session
113 self.session = request.session
112 self._rhodecode_user = request.user
114 self._rhodecode_user = request.user
113
115
114 def _get_template_context(self):
116 def _get_template_context(self):
115 return {
117 return {
116 'came_from': get_came_from(self.request),
118 'came_from': get_came_from(self.request),
117 'defaults': {},
119 'defaults': {},
118 'errors': {},
120 'errors': {},
119 }
121 }
120
122
121 def _get_captcha_data(self):
123 def _get_captcha_data(self):
122 settings = SettingsModel().get_all_settings()
124 settings = SettingsModel().get_all_settings()
123 private_key = settings.get('rhodecode_captcha_private_key')
125 private_key = settings.get('rhodecode_captcha_private_key')
124 public_key = settings.get('rhodecode_captcha_public_key')
126 public_key = settings.get('rhodecode_captcha_public_key')
125 active = bool(private_key)
127 active = bool(private_key)
126 return CaptchaData(
128 return CaptchaData(
127 active=active, private_key=private_key, public_key=public_key)
129 active=active, private_key=private_key, public_key=public_key)
128
130
129 @view_config(
131 @view_config(
130 route_name='login', request_method='GET',
132 route_name='login', request_method='GET',
131 renderer='rhodecode:templates/login.mako')
133 renderer='rhodecode:templates/login.mako')
132 def login(self):
134 def login(self):
133 came_from = get_came_from(self.request)
135 came_from = get_came_from(self.request)
134 user = self.request.user
136 user = self.request.user
135
137
136 # redirect if already logged in
138 # redirect if already logged in
137 if user.is_authenticated and not user.is_default and user.ip_allowed:
139 if user.is_authenticated and not user.is_default and user.ip_allowed:
138 raise HTTPFound(came_from)
140 raise HTTPFound(came_from)
139
141
140 # check if we use headers plugin, and try to login using it.
142 # check if we use headers plugin, and try to login using it.
141 try:
143 try:
142 log.debug('Running PRE-AUTH for headers based authentication')
144 log.debug('Running PRE-AUTH for headers based authentication')
143 auth_info = authenticate(
145 auth_info = authenticate(
144 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
146 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
145 if auth_info:
147 if auth_info:
146 headers = _store_user_in_session(
148 headers = _store_user_in_session(
147 self.session, auth_info.get('username'))
149 self.session, auth_info.get('username'))
148 raise HTTPFound(came_from, headers=headers)
150 raise HTTPFound(came_from, headers=headers)
149 except UserCreationError as e:
151 except UserCreationError as e:
150 log.error(e)
152 log.error(e)
151 self.session.flash(e, queue='error')
153 self.session.flash(e, queue='error')
152
154
153 return self._get_template_context()
155 return self._get_template_context()
154
156
155 @view_config(
157 @view_config(
156 route_name='login', request_method='POST',
158 route_name='login', request_method='POST',
157 renderer='rhodecode:templates/login.mako')
159 renderer='rhodecode:templates/login.mako')
158 def login_post(self):
160 def login_post(self):
159 came_from = get_came_from(self.request)
161 came_from = get_came_from(self.request)
160
162
161 login_form = LoginForm()()
163 login_form = LoginForm()()
162
164
163 try:
165 try:
164 self.session.invalidate()
166 self.session.invalidate()
165 form_result = login_form.to_python(self.request.params)
167 form_result = login_form.to_python(self.request.params)
166 # form checks for username/password, now we're authenticated
168 # form checks for username/password, now we're authenticated
167 headers = _store_user_in_session(
169 headers = _store_user_in_session(
168 self.session,
170 self.session,
169 username=form_result['username'],
171 username=form_result['username'],
170 remember=form_result['remember'])
172 remember=form_result['remember'])
171 log.debug('Redirecting to "%s" after login.', came_from)
173 log.debug('Redirecting to "%s" after login.', came_from)
172 raise HTTPFound(came_from, headers=headers)
174 raise HTTPFound(came_from, headers=headers)
173 except formencode.Invalid as errors:
175 except formencode.Invalid as errors:
174 defaults = errors.value
176 defaults = errors.value
175 # remove password from filling in form again
177 # remove password from filling in form again
176 defaults.pop('password', None)
178 defaults.pop('password', None)
177 render_ctx = self._get_template_context()
179 render_ctx = self._get_template_context()
178 render_ctx.update({
180 render_ctx.update({
179 'errors': errors.error_dict,
181 'errors': errors.error_dict,
180 'defaults': defaults,
182 'defaults': defaults,
181 })
183 })
182 return render_ctx
184 return render_ctx
183
185
184 except UserCreationError as e:
186 except UserCreationError as e:
185 # headers auth or other auth functions that create users on
187 # headers auth or other auth functions that create users on
186 # the fly can throw this exception signaling that there's issue
188 # the fly can throw this exception signaling that there's issue
187 # with user creation, explanation should be provided in
189 # with user creation, explanation should be provided in
188 # Exception itself
190 # Exception itself
189 self.session.flash(e, queue='error')
191 self.session.flash(e, queue='error')
190 return self._get_template_context()
192 return self._get_template_context()
191
193
192 @CSRFRequired()
194 @CSRFRequired()
193 @view_config(route_name='logout', request_method='POST')
195 @view_config(route_name='logout', request_method='POST')
194 def logout(self):
196 def logout(self):
195 user = self.request.user
197 user = self.request.user
196 log.info('Deleting session for user: `%s`', user)
198 log.info('Deleting session for user: `%s`', user)
197 self.session.delete()
199 self.session.delete()
198 return HTTPFound(url('home'))
200 return HTTPFound(url('home'))
199
201
200 @HasPermissionAnyDecorator(
202 @HasPermissionAnyDecorator(
201 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
203 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
202 @view_config(
204 @view_config(
203 route_name='register', request_method='GET',
205 route_name='register', request_method='GET',
204 renderer='rhodecode:templates/register.mako',)
206 renderer='rhodecode:templates/register.mako',)
205 def register(self, defaults=None, errors=None):
207 def register(self, defaults=None, errors=None):
206 defaults = defaults or {}
208 defaults = defaults or {}
207 errors = errors or {}
209 errors = errors or {}
208
210
209 settings = SettingsModel().get_all_settings()
211 settings = SettingsModel().get_all_settings()
210 register_message = settings.get('rhodecode_register_message') or ''
212 register_message = settings.get('rhodecode_register_message') or ''
211 captcha = self._get_captcha_data()
213 captcha = self._get_captcha_data()
212 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
214 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
213 .AuthUser.permissions['global']
215 .AuthUser.permissions['global']
214
216
215 render_ctx = self._get_template_context()
217 render_ctx = self._get_template_context()
216 render_ctx.update({
218 render_ctx.update({
217 'defaults': defaults,
219 'defaults': defaults,
218 'errors': errors,
220 'errors': errors,
219 'auto_active': auto_active,
221 'auto_active': auto_active,
220 'captcha_active': captcha.active,
222 'captcha_active': captcha.active,
221 'captcha_public_key': captcha.public_key,
223 'captcha_public_key': captcha.public_key,
222 'register_message': register_message,
224 'register_message': register_message,
223 })
225 })
224 return render_ctx
226 return render_ctx
225
227
226 @HasPermissionAnyDecorator(
228 @HasPermissionAnyDecorator(
227 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
229 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
228 @view_config(
230 @view_config(
229 route_name='register', request_method='POST',
231 route_name='register', request_method='POST',
230 renderer='rhodecode:templates/register.mako')
232 renderer='rhodecode:templates/register.mako')
231 def register_post(self):
233 def register_post(self):
232 captcha = self._get_captcha_data()
234 captcha = self._get_captcha_data()
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
235 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 .AuthUser.permissions['global']
236 .AuthUser.permissions['global']
235
237
236 register_form = RegisterForm()()
238 register_form = RegisterForm()()
237 try:
239 try:
238 form_result = register_form.to_python(self.request.params)
240 form_result = register_form.to_python(self.request.params)
239 form_result['active'] = auto_active
241 form_result['active'] = auto_active
240
242
241 if captcha.active:
243 if captcha.active:
242 response = submit(
244 response = submit(
243 self.request.params.get('recaptcha_challenge_field'),
245 self.request.params.get('recaptcha_challenge_field'),
244 self.request.params.get('recaptcha_response_field'),
246 self.request.params.get('recaptcha_response_field'),
245 private_key=captcha.private_key,
247 private_key=captcha.private_key,
246 remoteip=get_ip_addr(self.request.environ))
248 remoteip=get_ip_addr(self.request.environ))
247 if not response.is_valid:
249 if not response.is_valid:
248 _value = form_result
250 _value = form_result
249 _msg = _('Bad captcha')
251 _msg = _('Bad captcha')
250 error_dict = {'recaptcha_field': _msg}
252 error_dict = {'recaptcha_field': _msg}
251 raise formencode.Invalid(_msg, _value, None,
253 raise formencode.Invalid(_msg, _value, None,
252 error_dict=error_dict)
254 error_dict=error_dict)
253
255
254 new_user = UserModel().create_registration(form_result)
256 new_user = UserModel().create_registration(form_result)
255 event = UserRegistered(user=new_user, session=self.session)
257 event = UserRegistered(user=new_user, session=self.session)
256 self.request.registry.notify(event)
258 self.request.registry.notify(event)
257 self.session.flash(
259 self.session.flash(
258 _('You have successfully registered with RhodeCode'),
260 _('You have successfully registered with RhodeCode'),
259 queue='success')
261 queue='success')
260 Session().commit()
262 Session().commit()
261
263
262 redirect_ro = self.request.route_path('login')
264 redirect_ro = self.request.route_path('login')
263 raise HTTPFound(redirect_ro)
265 raise HTTPFound(redirect_ro)
264
266
265 except formencode.Invalid as errors:
267 except formencode.Invalid as errors:
266 errors.value.pop('password', None)
268 errors.value.pop('password', None)
267 errors.value.pop('password_confirmation', None)
269 errors.value.pop('password_confirmation', None)
268 return self.register(
270 return self.register(
269 defaults=errors.value, errors=errors.error_dict)
271 defaults=errors.value, errors=errors.error_dict)
270
272
271 except UserCreationError as e:
273 except UserCreationError as e:
272 # container auth or other auth functions that create users on
274 # container auth or other auth functions that create users on
273 # the fly can throw this exception signaling that there's issue
275 # the fly can throw this exception signaling that there's issue
274 # with user creation, explanation should be provided in
276 # with user creation, explanation should be provided in
275 # Exception itself
277 # Exception itself
276 self.session.flash(e, queue='error')
278 self.session.flash(e, queue='error')
277 return self.register()
279 return self.register()
278
280
279 @view_config(
281 @view_config(
280 route_name='reset_password', request_method=('GET', 'POST'),
282 route_name='reset_password', request_method=('GET', 'POST'),
281 renderer='rhodecode:templates/password_reset.mako')
283 renderer='rhodecode:templates/password_reset.mako')
282 def password_reset(self):
284 def password_reset(self):
283 captcha = self._get_captcha_data()
285 captcha = self._get_captcha_data()
284
286
285 render_ctx = {
287 render_ctx = {
286 'captcha_active': captcha.active,
288 'captcha_active': captcha.active,
287 'captcha_public_key': captcha.public_key,
289 'captcha_public_key': captcha.public_key,
288 'defaults': {},
290 'defaults': {},
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'),
306 self.request.params.get('recaptcha_response_field'),
315 self.request.params.get('recaptcha_response_field'),
307 private_key=captcha.private_key,
316 private_key=captcha.private_key,
308 remoteip=get_ip_addr(self.request.environ))
317 remoteip=get_ip_addr(self.request.environ))
309 if not response.is_valid:
318 if not response.is_valid:
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(_msg, _value, None,
322 raise formencode.Invalid(
314 error_dict=error_dict)
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': user.api_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 'reset_password_confirmation',
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'),
353 queue='success')
385 queue='success')
354 except Exception as e:
386 except Exception as e:
355 log.error(e)
387 log.error(e)
356 return HTTPFound(self.request.route_path('reset_password'))
388 return HTTPFound(self.request.route_path('reset_password'))
357
389
358 return HTTPFound(self.request.route_path('login'))
390 return HTTPFound(self.request.route_path('login'))
@@ -1,87 +1,97 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 """
21 """
22 authentication tokens model for RhodeCode
22 authentication tokens model for RhodeCode
23 """
23 """
24
24
25 import time
25 import time
26 import logging
26 import logging
27 import traceback
27 import traceback
28 from sqlalchemy import or_
28 from sqlalchemy import or_
29
29
30 from rhodecode.model import BaseModel
30 from rhodecode.model import BaseModel
31 from rhodecode.model.db import UserApiKeys
31 from rhodecode.model.db import UserApiKeys
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AuthTokenModel(BaseModel):
37 class AuthTokenModel(BaseModel):
38 cls = UserApiKeys
38 cls = UserApiKeys
39
39
40 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
40 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
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 seconds
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
48
48
49 user = self._get_user(user)
49 user = self._get_user(user)
50
50
51 new_auth_token = UserApiKeys()
51 new_auth_token = UserApiKeys()
52 new_auth_token.api_key = generate_auth_token(user.username)
52 new_auth_token.api_key = generate_auth_token(user.username)
53 new_auth_token.user_id = user.user_id
53 new_auth_token.user_id = user.user_id
54 new_auth_token.description = description
54 new_auth_token.description = description
55 new_auth_token.role = role
55 new_auth_token.role = role
56 new_auth_token.expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
56 new_auth_token.expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
57 Session().add(new_auth_token)
57 Session().add(new_auth_token)
58
58
59 return new_auth_token
59 return new_auth_token
60
60
61 def delete(self, api_key, user=None):
61 def delete(self, api_key, user=None):
62 """
62 """
63 Deletes given api_key, if user is set it also filters the object for
63 Deletes given api_key, if user is set it also filters the object for
64 deletion by given user.
64 deletion by given user.
65 """
65 """
66 api_key = UserApiKeys.query().filter(UserApiKeys.api_key == api_key)
66 api_key = UserApiKeys.query().filter(UserApiKeys.api_key == api_key)
67
67
68 if user:
68 if user:
69 user = self._get_user(user)
69 user = self._get_user(user)
70 api_key = api_key.filter(UserApiKeys.user_id == user.user_id)
70 api_key = api_key.filter(UserApiKeys.user_id == user.user_id)
71
71
72 api_key = api_key.scalar()
72 api_key = api_key.scalar()
73 try:
73 try:
74 Session().delete(api_key)
74 Session().delete(api_key)
75 except Exception:
75 except Exception:
76 log.error(traceback.format_exc())
76 log.error(traceback.format_exc())
77 raise
77 raise
78
78
79 def get_auth_tokens(self, user, show_expired=True):
79 def get_auth_tokens(self, user, show_expired=True):
80 user = self._get_user(user)
80 user = self._get_user(user)
81 user_auth_tokens = UserApiKeys.query()\
81 user_auth_tokens = UserApiKeys.query()\
82 .filter(UserApiKeys.user_id == user.user_id)
82 .filter(UserApiKeys.user_id == user.user_id)
83 if not show_expired:
83 if not show_expired:
84 user_auth_tokens = user_auth_tokens\
84 user_auth_tokens = user_auth_tokens\
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
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,845 +1,852 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 """
21 """
22 users model for RhodeCode
22 users model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27
27
28 import datetime
28 import datetime
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30
30
31 import ipaddress
31 import ipaddress
32 from sqlalchemy.exc import DatabaseError
32 from sqlalchemy.exc import DatabaseError
33 from sqlalchemy.sql.expression import true, false
33 from sqlalchemy.sql.expression import true, false
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.lib.utils2 import (
36 from rhodecode.lib.utils2 import (
37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 AttributeDict, str2bool)
38 AttributeDict, str2bool)
39 from rhodecode.lib.caching_query import FromCache
39 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 User, UserToPerm, UserEmailMap, UserIpMap)
43 User, UserToPerm, UserEmailMap, UserIpMap)
44 from rhodecode.lib.exceptions import (
44 from rhodecode.lib.exceptions import (
45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.repo_group import RepoGroupModel
48 from rhodecode.model.repo_group import RepoGroupModel
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class UserModel(BaseModel):
54 class UserModel(BaseModel):
55 cls = User
55 cls = User
56
56
57 def get(self, user_id, cache=False):
57 def get(self, user_id, cache=False):
58 user = self.sa.query(User)
58 user = self.sa.query(User)
59 if cache:
59 if cache:
60 user = user.options(FromCache("sql_cache_short",
60 user = user.options(FromCache("sql_cache_short",
61 "get_user_%s" % user_id))
61 "get_user_%s" % user_id))
62 return user.get(user_id)
62 return user.get(user_id)
63
63
64 def get_user(self, user):
64 def get_user(self, user):
65 return self._get_user(user)
65 return self._get_user(user)
66
66
67 def get_by_username(self, username, cache=False, case_insensitive=False):
67 def get_by_username(self, username, cache=False, case_insensitive=False):
68
68
69 if case_insensitive:
69 if case_insensitive:
70 user = self.sa.query(User).filter(User.username.ilike(username))
70 user = self.sa.query(User).filter(User.username.ilike(username))
71 else:
71 else:
72 user = self.sa.query(User)\
72 user = self.sa.query(User)\
73 .filter(User.username == username)
73 .filter(User.username == username)
74 if cache:
74 if cache:
75 user = user.options(FromCache("sql_cache_short",
75 user = user.options(FromCache("sql_cache_short",
76 "get_user_%s" % username))
76 "get_user_%s" % username))
77 return user.scalar()
77 return user.scalar()
78
78
79 def get_by_email(self, email, cache=False, case_insensitive=False):
79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 return User.get_by_email(email, case_insensitive, cache)
80 return User.get_by_email(email, case_insensitive, cache)
81
81
82 def get_by_auth_token(self, auth_token, cache=False):
82 def get_by_auth_token(self, auth_token, cache=False):
83 return User.get_by_auth_token(auth_token, cache)
83 return User.get_by_auth_token(auth_token, cache)
84
84
85 def get_active_user_count(self, cache=False):
85 def get_active_user_count(self, cache=False):
86 return User.query().filter(
86 return User.query().filter(
87 User.active == True).filter(
87 User.active == True).filter(
88 User.username != User.DEFAULT_USER).count()
88 User.username != User.DEFAULT_USER).count()
89
89
90 def create(self, form_data, cur_user=None):
90 def create(self, form_data, cur_user=None):
91 if not cur_user:
91 if not cur_user:
92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93
93
94 user_data = {
94 user_data = {
95 'username': form_data['username'],
95 'username': form_data['username'],
96 'password': form_data['password'],
96 'password': form_data['password'],
97 'email': form_data['email'],
97 'email': form_data['email'],
98 'firstname': form_data['firstname'],
98 'firstname': form_data['firstname'],
99 'lastname': form_data['lastname'],
99 'lastname': form_data['lastname'],
100 'active': form_data['active'],
100 'active': form_data['active'],
101 'extern_type': form_data['extern_type'],
101 'extern_type': form_data['extern_type'],
102 'extern_name': form_data['extern_name'],
102 'extern_name': form_data['extern_name'],
103 'admin': False,
103 'admin': False,
104 'cur_user': cur_user
104 'cur_user': cur_user
105 }
105 }
106
106
107 if 'create_repo_group' in form_data:
107 if 'create_repo_group' in form_data:
108 user_data['create_repo_group'] = str2bool(
108 user_data['create_repo_group'] = str2bool(
109 form_data.get('create_repo_group'))
109 form_data.get('create_repo_group'))
110
110
111 try:
111 try:
112 if form_data.get('password_change'):
112 if form_data.get('password_change'):
113 user_data['force_password_change'] = True
113 user_data['force_password_change'] = True
114 return UserModel().create_or_update(**user_data)
114 return UserModel().create_or_update(**user_data)
115 except Exception:
115 except Exception:
116 log.error(traceback.format_exc())
116 log.error(traceback.format_exc())
117 raise
117 raise
118
118
119 def update_user(self, user, skip_attrs=None, **kwargs):
119 def update_user(self, user, skip_attrs=None, **kwargs):
120 from rhodecode.lib.auth import get_crypt_password
120 from rhodecode.lib.auth import get_crypt_password
121
121
122 user = self._get_user(user)
122 user = self._get_user(user)
123 if user.username == User.DEFAULT_USER:
123 if user.username == User.DEFAULT_USER:
124 raise DefaultUserException(
124 raise DefaultUserException(
125 _("You can't Edit this user since it's"
125 _("You can't Edit this user since it's"
126 " crucial for entire application"))
126 " crucial for entire application"))
127
127
128 # first store only defaults
128 # first store only defaults
129 user_attrs = {
129 user_attrs = {
130 'updating_user_id': user.user_id,
130 'updating_user_id': user.user_id,
131 'username': user.username,
131 'username': user.username,
132 'password': user.password,
132 'password': user.password,
133 'email': user.email,
133 'email': user.email,
134 'firstname': user.name,
134 'firstname': user.name,
135 'lastname': user.lastname,
135 'lastname': user.lastname,
136 'active': user.active,
136 'active': user.active,
137 'admin': user.admin,
137 'admin': user.admin,
138 'extern_name': user.extern_name,
138 'extern_name': user.extern_name,
139 'extern_type': user.extern_type,
139 'extern_type': user.extern_type,
140 'language': user.user_data.get('language')
140 'language': user.user_data.get('language')
141 }
141 }
142
142
143 # in case there's new_password, that comes from form, use it to
143 # in case there's new_password, that comes from form, use it to
144 # store password
144 # store password
145 if kwargs.get('new_password'):
145 if kwargs.get('new_password'):
146 kwargs['password'] = kwargs['new_password']
146 kwargs['password'] = kwargs['new_password']
147
147
148 # cleanups, my_account password change form
148 # cleanups, my_account password change form
149 kwargs.pop('current_password', None)
149 kwargs.pop('current_password', None)
150 kwargs.pop('new_password', None)
150 kwargs.pop('new_password', None)
151
151
152 # cleanups, user edit password change form
152 # cleanups, user edit password change form
153 kwargs.pop('password_confirmation', None)
153 kwargs.pop('password_confirmation', None)
154 kwargs.pop('password_change', None)
154 kwargs.pop('password_change', None)
155
155
156 # create repo group on user creation
156 # create repo group on user creation
157 kwargs.pop('create_repo_group', None)
157 kwargs.pop('create_repo_group', None)
158
158
159 # legacy forms send name, which is the firstname
159 # legacy forms send name, which is the firstname
160 firstname = kwargs.pop('name', None)
160 firstname = kwargs.pop('name', None)
161 if firstname:
161 if firstname:
162 kwargs['firstname'] = firstname
162 kwargs['firstname'] = firstname
163
163
164 for k, v in kwargs.items():
164 for k, v in kwargs.items():
165 # skip if we don't want to update this
165 # skip if we don't want to update this
166 if skip_attrs and k in skip_attrs:
166 if skip_attrs and k in skip_attrs:
167 continue
167 continue
168
168
169 user_attrs[k] = v
169 user_attrs[k] = v
170
170
171 try:
171 try:
172 return self.create_or_update(**user_attrs)
172 return self.create_or_update(**user_attrs)
173 except Exception:
173 except Exception:
174 log.error(traceback.format_exc())
174 log.error(traceback.format_exc())
175 raise
175 raise
176
176
177 def create_or_update(
177 def create_or_update(
178 self, username, password, email, firstname='', lastname='',
178 self, username, password, email, firstname='', lastname='',
179 active=True, admin=False, extern_type=None, extern_name=None,
179 active=True, admin=False, extern_type=None, extern_name=None,
180 cur_user=None, plugin=None, force_password_change=False,
180 cur_user=None, plugin=None, force_password_change=False,
181 allow_to_create_user=True, create_repo_group=None,
181 allow_to_create_user=True, create_repo_group=None,
182 updating_user_id=None, language=None, strict_creation_check=True):
182 updating_user_id=None, language=None, strict_creation_check=True):
183 """
183 """
184 Creates a new instance if not found, or updates current one
184 Creates a new instance if not found, or updates current one
185
185
186 :param username:
186 :param username:
187 :param password:
187 :param password:
188 :param email:
188 :param email:
189 :param firstname:
189 :param firstname:
190 :param lastname:
190 :param lastname:
191 :param active:
191 :param active:
192 :param admin:
192 :param admin:
193 :param extern_type:
193 :param extern_type:
194 :param extern_name:
194 :param extern_name:
195 :param cur_user:
195 :param cur_user:
196 :param plugin: optional plugin this method was called from
196 :param plugin: optional plugin this method was called from
197 :param force_password_change: toggles new or existing user flag
197 :param force_password_change: toggles new or existing user flag
198 for password change
198 for password change
199 :param allow_to_create_user: Defines if the method can actually create
199 :param allow_to_create_user: Defines if the method can actually create
200 new users
200 new users
201 :param create_repo_group: Defines if the method should also
201 :param create_repo_group: Defines if the method should also
202 create an repo group with user name, and owner
202 create an repo group with user name, and owner
203 :param updating_user_id: if we set it up this is the user we want to
203 :param updating_user_id: if we set it up this is the user we want to
204 update this allows to editing username.
204 update this allows to editing username.
205 :param language: language of user from interface.
205 :param language: language of user from interface.
206
206
207 :returns: new User object with injected `is_new_user` attribute.
207 :returns: new User object with injected `is_new_user` attribute.
208 """
208 """
209 if not cur_user:
209 if not cur_user:
210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211
211
212 from rhodecode.lib.auth import (
212 from rhodecode.lib.auth import (
213 get_crypt_password, check_password, generate_auth_token)
213 get_crypt_password, check_password, generate_auth_token)
214 from rhodecode.lib.hooks_base import (
214 from rhodecode.lib.hooks_base import (
215 log_create_user, check_allowed_create_user)
215 log_create_user, check_allowed_create_user)
216
216
217 def _password_change(new_user, password):
217 def _password_change(new_user, password):
218 # empty password
218 # empty password
219 if not new_user.password:
219 if not new_user.password:
220 return False
220 return False
221
221
222 # password check is only needed for RhodeCode internal auth calls
222 # password check is only needed for RhodeCode internal auth calls
223 # in case it's a plugin we don't care
223 # in case it's a plugin we don't care
224 if not plugin:
224 if not plugin:
225
225
226 # first check if we gave crypted password back, and if it
226 # first check if we gave crypted password back, and if it
227 # matches it's not password change
227 # matches it's not password change
228 if new_user.password == password:
228 if new_user.password == password:
229 return False
229 return False
230
230
231 password_match = check_password(password, new_user.password)
231 password_match = check_password(password, new_user.password)
232 if not password_match:
232 if not password_match:
233 return True
233 return True
234
234
235 return False
235 return False
236
236
237 # read settings on default personal repo group creation
237 # read settings on default personal repo group creation
238 if create_repo_group is None:
238 if create_repo_group is None:
239 default_create_repo_group = RepoGroupModel()\
239 default_create_repo_group = RepoGroupModel()\
240 .get_default_create_personal_repo_group()
240 .get_default_create_personal_repo_group()
241 create_repo_group = default_create_repo_group
241 create_repo_group = default_create_repo_group
242
242
243 user_data = {
243 user_data = {
244 'username': username,
244 'username': username,
245 'password': password,
245 'password': password,
246 'email': email,
246 'email': email,
247 'firstname': firstname,
247 'firstname': firstname,
248 'lastname': lastname,
248 'lastname': lastname,
249 'active': active,
249 'active': active,
250 'admin': admin
250 'admin': admin
251 }
251 }
252
252
253 if updating_user_id:
253 if updating_user_id:
254 log.debug('Checking for existing account in RhodeCode '
254 log.debug('Checking for existing account in RhodeCode '
255 'database with user_id `%s` ' % (updating_user_id,))
255 'database with user_id `%s` ' % (updating_user_id,))
256 user = User.get(updating_user_id)
256 user = User.get(updating_user_id)
257 else:
257 else:
258 log.debug('Checking for existing account in RhodeCode '
258 log.debug('Checking for existing account in RhodeCode '
259 'database with username `%s` ' % (username,))
259 'database with username `%s` ' % (username,))
260 user = User.get_by_username(username, case_insensitive=True)
260 user = User.get_by_username(username, case_insensitive=True)
261
261
262 if user is None:
262 if user is None:
263 # we check internal flag if this method is actually allowed to
263 # we check internal flag if this method is actually allowed to
264 # create new user
264 # create new user
265 if not allow_to_create_user:
265 if not allow_to_create_user:
266 msg = ('Method wants to create new user, but it is not '
266 msg = ('Method wants to create new user, but it is not '
267 'allowed to do so')
267 'allowed to do so')
268 log.warning(msg)
268 log.warning(msg)
269 raise NotAllowedToCreateUserError(msg)
269 raise NotAllowedToCreateUserError(msg)
270
270
271 log.debug('Creating new user %s', username)
271 log.debug('Creating new user %s', username)
272
272
273 # only if we create user that is active
273 # only if we create user that is active
274 new_active_user = active
274 new_active_user = active
275 if new_active_user and strict_creation_check:
275 if new_active_user and strict_creation_check:
276 # raises UserCreationError if it's not allowed for any reason to
276 # raises UserCreationError if it's not allowed for any reason to
277 # create new active user, this also executes pre-create hooks
277 # create new active user, this also executes pre-create hooks
278 check_allowed_create_user(user_data, cur_user, strict_check=True)
278 check_allowed_create_user(user_data, cur_user, strict_check=True)
279 events.trigger(events.UserPreCreate(user_data))
279 events.trigger(events.UserPreCreate(user_data))
280 new_user = User()
280 new_user = User()
281 edit = False
281 edit = False
282 else:
282 else:
283 log.debug('updating user %s', username)
283 log.debug('updating user %s', username)
284 events.trigger(events.UserPreUpdate(user, user_data))
284 events.trigger(events.UserPreUpdate(user, user_data))
285 new_user = user
285 new_user = user
286 edit = True
286 edit = True
287
287
288 # we're not allowed to edit default user
288 # we're not allowed to edit default user
289 if user.username == User.DEFAULT_USER:
289 if user.username == User.DEFAULT_USER:
290 raise DefaultUserException(
290 raise DefaultUserException(
291 _("You can't edit this user (`%(username)s`) since it's "
291 _("You can't edit this user (`%(username)s`) since it's "
292 "crucial for entire application") % {'username': user.username})
292 "crucial for entire application") % {'username': user.username})
293
293
294 # inject special attribute that will tell us if User is new or old
294 # inject special attribute that will tell us if User is new or old
295 new_user.is_new_user = not edit
295 new_user.is_new_user = not edit
296 # for users that didn's specify auth type, we use RhodeCode built in
296 # for users that didn's specify auth type, we use RhodeCode built in
297 from rhodecode.authentication.plugins import auth_rhodecode
297 from rhodecode.authentication.plugins import auth_rhodecode
298 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
298 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
299 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
299 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
300
300
301 try:
301 try:
302 new_user.username = username
302 new_user.username = username
303 new_user.admin = admin
303 new_user.admin = admin
304 new_user.email = email
304 new_user.email = email
305 new_user.active = active
305 new_user.active = active
306 new_user.extern_name = safe_unicode(extern_name)
306 new_user.extern_name = safe_unicode(extern_name)
307 new_user.extern_type = safe_unicode(extern_type)
307 new_user.extern_type = safe_unicode(extern_type)
308 new_user.name = firstname
308 new_user.name = firstname
309 new_user.lastname = lastname
309 new_user.lastname = lastname
310
310
311 if not edit:
311 if not edit:
312 new_user.api_key = generate_auth_token(username)
312 new_user.api_key = generate_auth_token(username)
313
313
314 # set password only if creating an user or password is changed
314 # set password only if creating an user or password is changed
315 if not edit or _password_change(new_user, password):
315 if not edit or _password_change(new_user, password):
316 reason = 'new password' if edit else 'new user'
316 reason = 'new password' if edit else 'new user'
317 log.debug('Updating password reason=>%s', reason)
317 log.debug('Updating password reason=>%s', reason)
318 new_user.password = get_crypt_password(password) if password else None
318 new_user.password = get_crypt_password(password) if password else None
319
319
320 if force_password_change:
320 if force_password_change:
321 new_user.update_userdata(force_password_change=True)
321 new_user.update_userdata(force_password_change=True)
322 if language:
322 if language:
323 new_user.update_userdata(language=language)
323 new_user.update_userdata(language=language)
324 new_user.update_userdata(notification_status=True)
324 new_user.update_userdata(notification_status=True)
325
325
326 self.sa.add(new_user)
326 self.sa.add(new_user)
327
327
328 if not edit and create_repo_group:
328 if not edit and create_repo_group:
329 RepoGroupModel().create_personal_repo_group(
329 RepoGroupModel().create_personal_repo_group(
330 new_user, commit_early=False)
330 new_user, commit_early=False)
331
331
332 if not edit:
332 if not edit:
333 # add the RSS token
333 # add the RSS token
334 AuthTokenModel().create(username,
334 AuthTokenModel().create(username,
335 description='Generated feed token',
335 description='Generated feed token',
336 role=AuthTokenModel.cls.ROLE_FEED)
336 role=AuthTokenModel.cls.ROLE_FEED)
337 log_create_user(created_by=cur_user, **new_user.get_dict())
337 log_create_user(created_by=cur_user, **new_user.get_dict())
338 events.trigger(events.UserPostCreate(user_data))
338 events.trigger(events.UserPostCreate(user_data))
339 return new_user
339 return new_user
340 except (DatabaseError,):
340 except (DatabaseError,):
341 log.error(traceback.format_exc())
341 log.error(traceback.format_exc())
342 raise
342 raise
343
343
344 def create_registration(self, form_data):
344 def create_registration(self, form_data):
345 from rhodecode.model.notification import NotificationModel
345 from rhodecode.model.notification import NotificationModel
346 from rhodecode.model.notification import EmailNotificationModel
346 from rhodecode.model.notification import EmailNotificationModel
347
347
348 try:
348 try:
349 form_data['admin'] = False
349 form_data['admin'] = False
350 form_data['extern_name'] = 'rhodecode'
350 form_data['extern_name'] = 'rhodecode'
351 form_data['extern_type'] = 'rhodecode'
351 form_data['extern_type'] = 'rhodecode'
352 new_user = self.create(form_data)
352 new_user = self.create(form_data)
353
353
354 self.sa.add(new_user)
354 self.sa.add(new_user)
355 self.sa.flush()
355 self.sa.flush()
356
356
357 user_data = new_user.get_dict()
357 user_data = new_user.get_dict()
358 kwargs = {
358 kwargs = {
359 # use SQLALCHEMY safe dump of user data
359 # use SQLALCHEMY safe dump of user data
360 'user': AttributeDict(user_data),
360 'user': AttributeDict(user_data),
361 'date': datetime.datetime.now()
361 'date': datetime.datetime.now()
362 }
362 }
363 notification_type = EmailNotificationModel.TYPE_REGISTRATION
363 notification_type = EmailNotificationModel.TYPE_REGISTRATION
364 # pre-generate the subject for notification itself
364 # pre-generate the subject for notification itself
365 (subject,
365 (subject,
366 _h, _e, # we don't care about those
366 _h, _e, # we don't care about those
367 body_plaintext) = EmailNotificationModel().render_email(
367 body_plaintext) = EmailNotificationModel().render_email(
368 notification_type, **kwargs)
368 notification_type, **kwargs)
369
369
370 # create notification objects, and emails
370 # create notification objects, and emails
371 NotificationModel().create(
371 NotificationModel().create(
372 created_by=new_user,
372 created_by=new_user,
373 notification_subject=subject,
373 notification_subject=subject,
374 notification_body=body_plaintext,
374 notification_body=body_plaintext,
375 notification_type=notification_type,
375 notification_type=notification_type,
376 recipients=None, # all admins
376 recipients=None, # all admins
377 email_kwargs=kwargs,
377 email_kwargs=kwargs,
378 )
378 )
379
379
380 return new_user
380 return new_user
381 except Exception:
381 except Exception:
382 log.error(traceback.format_exc())
382 log.error(traceback.format_exc())
383 raise
383 raise
384
384
385 def _handle_user_repos(self, username, repositories, handle_mode=None):
385 def _handle_user_repos(self, username, repositories, handle_mode=None):
386 _superadmin = self.cls.get_first_super_admin()
386 _superadmin = self.cls.get_first_super_admin()
387 left_overs = True
387 left_overs = True
388
388
389 from rhodecode.model.repo import RepoModel
389 from rhodecode.model.repo import RepoModel
390
390
391 if handle_mode == 'detach':
391 if handle_mode == 'detach':
392 for obj in repositories:
392 for obj in repositories:
393 obj.user = _superadmin
393 obj.user = _superadmin
394 # set description we know why we super admin now owns
394 # set description we know why we super admin now owns
395 # additional repositories that were orphaned !
395 # additional repositories that were orphaned !
396 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
396 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
397 self.sa.add(obj)
397 self.sa.add(obj)
398 left_overs = False
398 left_overs = False
399 elif handle_mode == 'delete':
399 elif handle_mode == 'delete':
400 for obj in repositories:
400 for obj in repositories:
401 RepoModel().delete(obj, forks='detach')
401 RepoModel().delete(obj, forks='detach')
402 left_overs = False
402 left_overs = False
403
403
404 # if nothing is done we have left overs left
404 # if nothing is done we have left overs left
405 return left_overs
405 return left_overs
406
406
407 def _handle_user_repo_groups(self, username, repository_groups,
407 def _handle_user_repo_groups(self, username, repository_groups,
408 handle_mode=None):
408 handle_mode=None):
409 _superadmin = self.cls.get_first_super_admin()
409 _superadmin = self.cls.get_first_super_admin()
410 left_overs = True
410 left_overs = True
411
411
412 from rhodecode.model.repo_group import RepoGroupModel
412 from rhodecode.model.repo_group import RepoGroupModel
413
413
414 if handle_mode == 'detach':
414 if handle_mode == 'detach':
415 for r in repository_groups:
415 for r in repository_groups:
416 r.user = _superadmin
416 r.user = _superadmin
417 # set description we know why we super admin now owns
417 # set description we know why we super admin now owns
418 # additional repositories that were orphaned !
418 # additional repositories that were orphaned !
419 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
419 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
420 self.sa.add(r)
420 self.sa.add(r)
421 left_overs = False
421 left_overs = False
422 elif handle_mode == 'delete':
422 elif handle_mode == 'delete':
423 for r in repository_groups:
423 for r in repository_groups:
424 RepoGroupModel().delete(r)
424 RepoGroupModel().delete(r)
425 left_overs = False
425 left_overs = False
426
426
427 # if nothing is done we have left overs left
427 # if nothing is done we have left overs left
428 return left_overs
428 return left_overs
429
429
430 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
430 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
431 _superadmin = self.cls.get_first_super_admin()
431 _superadmin = self.cls.get_first_super_admin()
432 left_overs = True
432 left_overs = True
433
433
434 from rhodecode.model.user_group import UserGroupModel
434 from rhodecode.model.user_group import UserGroupModel
435
435
436 if handle_mode == 'detach':
436 if handle_mode == 'detach':
437 for r in user_groups:
437 for r in user_groups:
438 for user_user_group_to_perm in r.user_user_group_to_perm:
438 for user_user_group_to_perm in r.user_user_group_to_perm:
439 if user_user_group_to_perm.user.username == username:
439 if user_user_group_to_perm.user.username == username:
440 user_user_group_to_perm.user = _superadmin
440 user_user_group_to_perm.user = _superadmin
441 r.user = _superadmin
441 r.user = _superadmin
442 # set description we know why we super admin now owns
442 # set description we know why we super admin now owns
443 # additional repositories that were orphaned !
443 # additional repositories that were orphaned !
444 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
444 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
445 self.sa.add(r)
445 self.sa.add(r)
446 left_overs = False
446 left_overs = False
447 elif handle_mode == 'delete':
447 elif handle_mode == 'delete':
448 for r in user_groups:
448 for r in user_groups:
449 UserGroupModel().delete(r)
449 UserGroupModel().delete(r)
450 left_overs = False
450 left_overs = False
451
451
452 # if nothing is done we have left overs left
452 # if nothing is done we have left overs left
453 return left_overs
453 return left_overs
454
454
455 def delete(self, user, cur_user=None, handle_repos=None,
455 def delete(self, user, cur_user=None, handle_repos=None,
456 handle_repo_groups=None, handle_user_groups=None):
456 handle_repo_groups=None, handle_user_groups=None):
457 if not cur_user:
457 if not cur_user:
458 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
458 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
459 user = self._get_user(user)
459 user = self._get_user(user)
460
460
461 try:
461 try:
462 if user.username == User.DEFAULT_USER:
462 if user.username == User.DEFAULT_USER:
463 raise DefaultUserException(
463 raise DefaultUserException(
464 _(u"You can't remove this user since it's"
464 _(u"You can't remove this user since it's"
465 u" crucial for entire application"))
465 u" crucial for entire application"))
466
466
467 left_overs = self._handle_user_repos(
467 left_overs = self._handle_user_repos(
468 user.username, user.repositories, handle_repos)
468 user.username, user.repositories, handle_repos)
469 if left_overs and user.repositories:
469 if left_overs and user.repositories:
470 repos = [x.repo_name for x in user.repositories]
470 repos = [x.repo_name for x in user.repositories]
471 raise UserOwnsReposException(
471 raise UserOwnsReposException(
472 _(u'user "%s" still owns %s repositories and cannot be '
472 _(u'user "%s" still owns %s repositories and cannot be '
473 u'removed. Switch owners or remove those repositories:%s')
473 u'removed. Switch owners or remove those repositories:%s')
474 % (user.username, len(repos), ', '.join(repos)))
474 % (user.username, len(repos), ', '.join(repos)))
475
475
476 left_overs = self._handle_user_repo_groups(
476 left_overs = self._handle_user_repo_groups(
477 user.username, user.repository_groups, handle_repo_groups)
477 user.username, user.repository_groups, handle_repo_groups)
478 if left_overs and user.repository_groups:
478 if left_overs and user.repository_groups:
479 repo_groups = [x.group_name for x in user.repository_groups]
479 repo_groups = [x.group_name for x in user.repository_groups]
480 raise UserOwnsRepoGroupsException(
480 raise UserOwnsRepoGroupsException(
481 _(u'user "%s" still owns %s repository groups and cannot be '
481 _(u'user "%s" still owns %s repository groups and cannot be '
482 u'removed. Switch owners or remove those repository groups:%s')
482 u'removed. Switch owners or remove those repository groups:%s')
483 % (user.username, len(repo_groups), ', '.join(repo_groups)))
483 % (user.username, len(repo_groups), ', '.join(repo_groups)))
484
484
485 left_overs = self._handle_user_user_groups(
485 left_overs = self._handle_user_user_groups(
486 user.username, user.user_groups, handle_user_groups)
486 user.username, user.user_groups, handle_user_groups)
487 if left_overs and user.user_groups:
487 if left_overs and user.user_groups:
488 user_groups = [x.users_group_name for x in user.user_groups]
488 user_groups = [x.users_group_name for x in user.user_groups]
489 raise UserOwnsUserGroupsException(
489 raise UserOwnsUserGroupsException(
490 _(u'user "%s" still owns %s user groups and cannot be '
490 _(u'user "%s" still owns %s user groups and cannot be '
491 u'removed. Switch owners or remove those user groups:%s')
491 u'removed. Switch owners or remove those user groups:%s')
492 % (user.username, len(user_groups), ', '.join(user_groups)))
492 % (user.username, len(user_groups), ', '.join(user_groups)))
493
493
494 # we might change the user data with detach/delete, make sure
494 # we might change the user data with detach/delete, make sure
495 # the object is marked as expired before actually deleting !
495 # the object is marked as expired before actually deleting !
496 self.sa.expire(user)
496 self.sa.expire(user)
497 self.sa.delete(user)
497 self.sa.delete(user)
498 from rhodecode.lib.hooks_base import log_delete_user
498 from rhodecode.lib.hooks_base import log_delete_user
499 log_delete_user(deleted_by=cur_user, **user.get_dict())
499 log_delete_user(deleted_by=cur_user, **user.get_dict())
500 except Exception:
500 except Exception:
501 log.error(traceback.format_exc())
501 log.error(traceback.format_exc())
502 raise
502 raise
503
503
504 def reset_password_link(self, data, pwd_reset_url):
504 def reset_password_link(self, data, pwd_reset_url):
505 from rhodecode.lib.celerylib import tasks, run_task
505 from rhodecode.lib.celerylib import tasks, run_task
506 from rhodecode.model.notification import EmailNotificationModel
506 from rhodecode.model.notification import EmailNotificationModel
507 user_email = data['email']
507 user_email = data['email']
508 try:
508 try:
509 user = User.get_by_email(user_email)
509 user = User.get_by_email(user_email)
510 if user:
510 if user:
511 log.debug('password reset user found %s', user)
511 log.debug('password reset user found %s', user)
512
512
513 email_kwargs = {
513 email_kwargs = {
514 'password_reset_url': pwd_reset_url,
514 'password_reset_url': pwd_reset_url,
515 'user': user,
515 'user': user,
516 'email': user_email,
516 'email': user_email,
517 'date': datetime.datetime.now()
517 'date': datetime.datetime.now()
518 }
518 }
519
519
520 (subject, headers, email_body,
520 (subject, headers, email_body,
521 email_body_plaintext) = EmailNotificationModel().render_email(
521 email_body_plaintext) = EmailNotificationModel().render_email(
522 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
522 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
523
523
524 recipients = [user_email]
524 recipients = [user_email]
525
525
526 action_logger_generic(
526 action_logger_generic(
527 'sending password reset email to user: {}'.format(
527 'sending password reset email to user: {}'.format(
528 user), namespace='security.password_reset')
528 user), namespace='security.password_reset')
529
529
530 run_task(tasks.send_email, recipients, subject,
530 run_task(tasks.send_email, recipients, subject,
531 email_body_plaintext, email_body)
531 email_body_plaintext, email_body)
532
532
533 else:
533 else:
534 log.debug("password reset email %s not found", user_email)
534 log.debug("password reset email %s not found", user_email)
535 except Exception:
535 except Exception:
536 log.error(traceback.format_exc())
536 log.error(traceback.format_exc())
537 return False
537 return False
538
538
539 return True
539 return True
540
540
541 def reset_password(self, data, pwd_reset_url):
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
545 user_email = data['email']
545 user_email = data['email']
546 pre_db = True
546 pre_db = True
547 try:
547 try:
548 user = User.get_by_email(user_email)
548 user = User.get_by_email(user_email)
549 new_passwd = auth.PasswordGenerator().gen_password(
549 new_passwd = auth.PasswordGenerator().gen_password(
550 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
550 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
551 if user:
551 if user:
552 user.password = auth.get_crypt_password(new_passwd)
552 user.password = auth.get_crypt_password(new_passwd)
553 # also force this user to reset his password !
553 # also force this user to reset his password !
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
562 pre_db = False
569 pre_db = False
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()
570 }
576 }
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, **email_kwargs)
580 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
581 **email_kwargs)
575
582
576 recipients = [user_email]
583 recipients = [user_email]
577
584
578 action_logger_generic(
585 action_logger_generic(
579 'sent new password to user: {} with email: {}'.format(
586 'sent new password to user: {} with email: {}'.format(
580 user, user_email), namespace='security.password_reset')
587 user, user_email), namespace='security.password_reset')
581
588
582 run_task(tasks.send_email, recipients, subject,
589 run_task(tasks.send_email, recipients, subject,
583 email_body_plaintext, email_body)
590 email_body_plaintext, email_body)
584
591
585 except Exception:
592 except Exception:
586 log.error('Failed to update user password')
593 log.error('Failed to update user password')
587 log.error(traceback.format_exc())
594 log.error(traceback.format_exc())
588 if pre_db:
595 if pre_db:
589 # we rollback only if local db stuff fails. If it goes into
596 # we rollback only if local db stuff fails. If it goes into
590 # run_task, we're pass rollback state this wouldn't work then
597 # run_task, we're pass rollback state this wouldn't work then
591 Session().rollback()
598 Session().rollback()
592
599
593 return True
600 return True
594
601
595 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
602 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
596 """
603 """
597 Fetches auth_user by user_id,or api_key if present.
604 Fetches auth_user by user_id,or api_key if present.
598 Fills auth_user attributes with those taken from database.
605 Fills auth_user attributes with those taken from database.
599 Additionally set's is_authenitated if lookup fails
606 Additionally set's is_authenitated if lookup fails
600 present in database
607 present in database
601
608
602 :param auth_user: instance of user to set attributes
609 :param auth_user: instance of user to set attributes
603 :param user_id: user id to fetch by
610 :param user_id: user id to fetch by
604 :param api_key: api key to fetch by
611 :param api_key: api key to fetch by
605 :param username: username to fetch by
612 :param username: username to fetch by
606 """
613 """
607 if user_id is None and api_key is None and username is None:
614 if user_id is None and api_key is None and username is None:
608 raise Exception('You need to pass user_id, api_key or username')
615 raise Exception('You need to pass user_id, api_key or username')
609
616
610 log.debug(
617 log.debug(
611 'doing fill data based on: user_id:%s api_key:%s username:%s',
618 'doing fill data based on: user_id:%s api_key:%s username:%s',
612 user_id, api_key, username)
619 user_id, api_key, username)
613 try:
620 try:
614 dbuser = None
621 dbuser = None
615 if user_id:
622 if user_id:
616 dbuser = self.get(user_id)
623 dbuser = self.get(user_id)
617 elif api_key:
624 elif api_key:
618 dbuser = self.get_by_auth_token(api_key)
625 dbuser = self.get_by_auth_token(api_key)
619 elif username:
626 elif username:
620 dbuser = self.get_by_username(username)
627 dbuser = self.get_by_username(username)
621
628
622 if not dbuser:
629 if not dbuser:
623 log.warning(
630 log.warning(
624 'Unable to lookup user by id:%s api_key:%s username:%s',
631 'Unable to lookup user by id:%s api_key:%s username:%s',
625 user_id, api_key, username)
632 user_id, api_key, username)
626 return False
633 return False
627 if not dbuser.active:
634 if not dbuser.active:
628 log.debug('User `%s` is inactive, skipping fill data', username)
635 log.debug('User `%s` is inactive, skipping fill data', username)
629 return False
636 return False
630
637
631 log.debug('filling user:%s data', dbuser)
638 log.debug('filling user:%s data', dbuser)
632
639
633 # TODO: johbo: Think about this and find a clean solution
640 # TODO: johbo: Think about this and find a clean solution
634 user_data = dbuser.get_dict()
641 user_data = dbuser.get_dict()
635 user_data.update(dbuser.get_api_data(include_secrets=True))
642 user_data.update(dbuser.get_api_data(include_secrets=True))
636
643
637 for k, v in user_data.iteritems():
644 for k, v in user_data.iteritems():
638 # properties of auth user we dont update
645 # properties of auth user we dont update
639 if k not in ['auth_tokens', 'permissions']:
646 if k not in ['auth_tokens', 'permissions']:
640 setattr(auth_user, k, v)
647 setattr(auth_user, k, v)
641
648
642 # few extras
649 # few extras
643 setattr(auth_user, 'feed_token', dbuser.feed_token)
650 setattr(auth_user, 'feed_token', dbuser.feed_token)
644 except Exception:
651 except Exception:
645 log.error(traceback.format_exc())
652 log.error(traceback.format_exc())
646 auth_user.is_authenticated = False
653 auth_user.is_authenticated = False
647 return False
654 return False
648
655
649 return True
656 return True
650
657
651 def has_perm(self, user, perm):
658 def has_perm(self, user, perm):
652 perm = self._get_perm(perm)
659 perm = self._get_perm(perm)
653 user = self._get_user(user)
660 user = self._get_user(user)
654
661
655 return UserToPerm.query().filter(UserToPerm.user == user)\
662 return UserToPerm.query().filter(UserToPerm.user == user)\
656 .filter(UserToPerm.permission == perm).scalar() is not None
663 .filter(UserToPerm.permission == perm).scalar() is not None
657
664
658 def grant_perm(self, user, perm):
665 def grant_perm(self, user, perm):
659 """
666 """
660 Grant user global permissions
667 Grant user global permissions
661
668
662 :param user:
669 :param user:
663 :param perm:
670 :param perm:
664 """
671 """
665 user = self._get_user(user)
672 user = self._get_user(user)
666 perm = self._get_perm(perm)
673 perm = self._get_perm(perm)
667 # if this permission is already granted skip it
674 # if this permission is already granted skip it
668 _perm = UserToPerm.query()\
675 _perm = UserToPerm.query()\
669 .filter(UserToPerm.user == user)\
676 .filter(UserToPerm.user == user)\
670 .filter(UserToPerm.permission == perm)\
677 .filter(UserToPerm.permission == perm)\
671 .scalar()
678 .scalar()
672 if _perm:
679 if _perm:
673 return
680 return
674 new = UserToPerm()
681 new = UserToPerm()
675 new.user = user
682 new.user = user
676 new.permission = perm
683 new.permission = perm
677 self.sa.add(new)
684 self.sa.add(new)
678 return new
685 return new
679
686
680 def revoke_perm(self, user, perm):
687 def revoke_perm(self, user, perm):
681 """
688 """
682 Revoke users global permissions
689 Revoke users global permissions
683
690
684 :param user:
691 :param user:
685 :param perm:
692 :param perm:
686 """
693 """
687 user = self._get_user(user)
694 user = self._get_user(user)
688 perm = self._get_perm(perm)
695 perm = self._get_perm(perm)
689
696
690 obj = UserToPerm.query()\
697 obj = UserToPerm.query()\
691 .filter(UserToPerm.user == user)\
698 .filter(UserToPerm.user == user)\
692 .filter(UserToPerm.permission == perm)\
699 .filter(UserToPerm.permission == perm)\
693 .scalar()
700 .scalar()
694 if obj:
701 if obj:
695 self.sa.delete(obj)
702 self.sa.delete(obj)
696
703
697 def add_extra_email(self, user, email):
704 def add_extra_email(self, user, email):
698 """
705 """
699 Adds email address to UserEmailMap
706 Adds email address to UserEmailMap
700
707
701 :param user:
708 :param user:
702 :param email:
709 :param email:
703 """
710 """
704 from rhodecode.model import forms
711 from rhodecode.model import forms
705 form = forms.UserExtraEmailForm()()
712 form = forms.UserExtraEmailForm()()
706 data = form.to_python({'email': email})
713 data = form.to_python({'email': email})
707 user = self._get_user(user)
714 user = self._get_user(user)
708
715
709 obj = UserEmailMap()
716 obj = UserEmailMap()
710 obj.user = user
717 obj.user = user
711 obj.email = data['email']
718 obj.email = data['email']
712 self.sa.add(obj)
719 self.sa.add(obj)
713 return obj
720 return obj
714
721
715 def delete_extra_email(self, user, email_id):
722 def delete_extra_email(self, user, email_id):
716 """
723 """
717 Removes email address from UserEmailMap
724 Removes email address from UserEmailMap
718
725
719 :param user:
726 :param user:
720 :param email_id:
727 :param email_id:
721 """
728 """
722 user = self._get_user(user)
729 user = self._get_user(user)
723 obj = UserEmailMap.query().get(email_id)
730 obj = UserEmailMap.query().get(email_id)
724 if obj:
731 if obj:
725 self.sa.delete(obj)
732 self.sa.delete(obj)
726
733
727 def parse_ip_range(self, ip_range):
734 def parse_ip_range(self, ip_range):
728 ip_list = []
735 ip_list = []
729 def make_unique(value):
736 def make_unique(value):
730 seen = []
737 seen = []
731 return [c for c in value if not (c in seen or seen.append(c))]
738 return [c for c in value if not (c in seen or seen.append(c))]
732
739
733 # firsts split by commas
740 # firsts split by commas
734 for ip_range in ip_range.split(','):
741 for ip_range in ip_range.split(','):
735 if not ip_range:
742 if not ip_range:
736 continue
743 continue
737 ip_range = ip_range.strip()
744 ip_range = ip_range.strip()
738 if '-' in ip_range:
745 if '-' in ip_range:
739 start_ip, end_ip = ip_range.split('-', 1)
746 start_ip, end_ip = ip_range.split('-', 1)
740 start_ip = ipaddress.ip_address(start_ip.strip())
747 start_ip = ipaddress.ip_address(start_ip.strip())
741 end_ip = ipaddress.ip_address(end_ip.strip())
748 end_ip = ipaddress.ip_address(end_ip.strip())
742 parsed_ip_range = []
749 parsed_ip_range = []
743
750
744 for index in xrange(int(start_ip), int(end_ip) + 1):
751 for index in xrange(int(start_ip), int(end_ip) + 1):
745 new_ip = ipaddress.ip_address(index)
752 new_ip = ipaddress.ip_address(index)
746 parsed_ip_range.append(str(new_ip))
753 parsed_ip_range.append(str(new_ip))
747 ip_list.extend(parsed_ip_range)
754 ip_list.extend(parsed_ip_range)
748 else:
755 else:
749 ip_list.append(ip_range)
756 ip_list.append(ip_range)
750
757
751 return make_unique(ip_list)
758 return make_unique(ip_list)
752
759
753 def add_extra_ip(self, user, ip, description=None):
760 def add_extra_ip(self, user, ip, description=None):
754 """
761 """
755 Adds ip address to UserIpMap
762 Adds ip address to UserIpMap
756
763
757 :param user:
764 :param user:
758 :param ip:
765 :param ip:
759 """
766 """
760 from rhodecode.model import forms
767 from rhodecode.model import forms
761 form = forms.UserExtraIpForm()()
768 form = forms.UserExtraIpForm()()
762 data = form.to_python({'ip': ip})
769 data = form.to_python({'ip': ip})
763 user = self._get_user(user)
770 user = self._get_user(user)
764
771
765 obj = UserIpMap()
772 obj = UserIpMap()
766 obj.user = user
773 obj.user = user
767 obj.ip_addr = data['ip']
774 obj.ip_addr = data['ip']
768 obj.description = description
775 obj.description = description
769 self.sa.add(obj)
776 self.sa.add(obj)
770 return obj
777 return obj
771
778
772 def delete_extra_ip(self, user, ip_id):
779 def delete_extra_ip(self, user, ip_id):
773 """
780 """
774 Removes ip address from UserIpMap
781 Removes ip address from UserIpMap
775
782
776 :param user:
783 :param user:
777 :param ip_id:
784 :param ip_id:
778 """
785 """
779 user = self._get_user(user)
786 user = self._get_user(user)
780 obj = UserIpMap.query().get(ip_id)
787 obj = UserIpMap.query().get(ip_id)
781 if obj:
788 if obj:
782 self.sa.delete(obj)
789 self.sa.delete(obj)
783
790
784 def get_accounts_in_creation_order(self, current_user=None):
791 def get_accounts_in_creation_order(self, current_user=None):
785 """
792 """
786 Get accounts in order of creation for deactivation for license limits
793 Get accounts in order of creation for deactivation for license limits
787
794
788 pick currently logged in user, and append to the list in position 0
795 pick currently logged in user, and append to the list in position 0
789 pick all super-admins in order of creation date and add it to the list
796 pick all super-admins in order of creation date and add it to the list
790 pick all other accounts in order of creation and add it to the list.
797 pick all other accounts in order of creation and add it to the list.
791
798
792 Based on that list, the last accounts can be disabled as they are
799 Based on that list, the last accounts can be disabled as they are
793 created at the end and don't include any of the super admins as well
800 created at the end and don't include any of the super admins as well
794 as the current user.
801 as the current user.
795
802
796 :param current_user: optionally current user running this operation
803 :param current_user: optionally current user running this operation
797 """
804 """
798
805
799 if not current_user:
806 if not current_user:
800 current_user = get_current_rhodecode_user()
807 current_user = get_current_rhodecode_user()
801 active_super_admins = [
808 active_super_admins = [
802 x.user_id for x in User.query()
809 x.user_id for x in User.query()
803 .filter(User.user_id != current_user.user_id)
810 .filter(User.user_id != current_user.user_id)
804 .filter(User.active == true())
811 .filter(User.active == true())
805 .filter(User.admin == true())
812 .filter(User.admin == true())
806 .order_by(User.created_on.asc())]
813 .order_by(User.created_on.asc())]
807
814
808 active_regular_users = [
815 active_regular_users = [
809 x.user_id for x in User.query()
816 x.user_id for x in User.query()
810 .filter(User.user_id != current_user.user_id)
817 .filter(User.user_id != current_user.user_id)
811 .filter(User.active == true())
818 .filter(User.active == true())
812 .filter(User.admin == false())
819 .filter(User.admin == false())
813 .order_by(User.created_on.asc())]
820 .order_by(User.created_on.asc())]
814
821
815 list_of_accounts = [current_user.user_id]
822 list_of_accounts = [current_user.user_id]
816 list_of_accounts += active_super_admins
823 list_of_accounts += active_super_admins
817 list_of_accounts += active_regular_users
824 list_of_accounts += active_regular_users
818
825
819 return list_of_accounts
826 return list_of_accounts
820
827
821 def deactivate_last_users(self, expected_users):
828 def deactivate_last_users(self, expected_users):
822 """
829 """
823 Deactivate accounts that are over the license limits.
830 Deactivate accounts that are over the license limits.
824 Algorithm of which accounts to disabled is based on the formula:
831 Algorithm of which accounts to disabled is based on the formula:
825
832
826 Get current user, then super admins in creation order, then regular
833 Get current user, then super admins in creation order, then regular
827 active users in creation order.
834 active users in creation order.
828
835
829 Using that list we mark all accounts from the end of it as inactive.
836 Using that list we mark all accounts from the end of it as inactive.
830 This way we block only latest created accounts.
837 This way we block only latest created accounts.
831
838
832 :param expected_users: list of users in special order, we deactivate
839 :param expected_users: list of users in special order, we deactivate
833 the end N ammoun of users from that list
840 the end N ammoun of users from that list
834 """
841 """
835
842
836 list_of_accounts = self.get_accounts_in_creation_order()
843 list_of_accounts = self.get_accounts_in_creation_order()
837
844
838 for acc_id in list_of_accounts[expected_users + 1:]:
845 for acc_id in list_of_accounts[expected_users + 1:]:
839 user = User.get(acc_id)
846 user = User.get(acc_id)
840 log.info('Deactivating account %s for license unlock', user)
847 log.info('Deactivating account %s for license unlock', user)
841 user.active = False
848 user.active = False
842 Session().add(user)
849 Session().add(user)
843 Session().commit()
850 Session().commit()
844
851
845 return
852 return
@@ -1,31 +1,33 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3
3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim">
5 RhodeCode Password reset
5 RhodeCode Password reset
6 </%def>
6 </%def>
7
7
8 ## plain text version of the email. Empty by default
8 ## plain text version of the email. Empty by default
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 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
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 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
22 ## BODY GOES BELOW
23 ## BODY GOES BELOW
23 <p>
24 <p>
24 Hello ${user.username},
25 Hello ${user.username},
25 </p><p>
26 </p><p>
26 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
27 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
27 <br/>
28 <br/>
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>
@@ -1,30 +1,29 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3
3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim">
5 Your new RhodeCode password
5 Your new RhodeCode password
6 </%def>
6 </%def>
7
7
8 ## plain text version of the email. Empty by default
8 ## plain text version of the email. Empty by default
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>
21
20
22 ## BODY GOES BELOW
21 ## BODY GOES BELOW
23 <p>
22 <p>
24 Hello ${user.username},
23 Hello ${user.username},
25 </p><p>
24 </p><p>
26 Below is your new access password for RhodeCode.
25 Below is your new access password for RhodeCode.
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: <input value='${new_password}'/></p>
29 <p>password: <pre>${new_password}</pre>
@@ -1,591 +1,510 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 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, generate_auth_token
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()
40
38
41 # Hardcode URLs because we don't have a request object to use
39 # Hardcode URLs because we don't have a request object to use
42 # pyramids URL generation methods.
40 # pyramids URL generation methods.
43 index_url = '/'
41 index_url = '/'
44 login_url = ADMIN_PREFIX + '/login'
42 login_url = ADMIN_PREFIX + '/login'
45 logut_url = ADMIN_PREFIX + '/logout'
43 logut_url = ADMIN_PREFIX + '/logout'
46 register_url = ADMIN_PREFIX + '/register'
44 register_url = ADMIN_PREFIX + '/register'
47 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
45 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
48 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
46 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
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
56 def teardown_class(cls):
54 def teardown_class(cls):
57 fixture.destroy_users(cls.destroy_users)
55 fixture.destroy_users(cls.destroy_users)
58
56
59 def teardown_method(self, method):
57 def teardown_method(self, method):
60 for n in Notification.query().all():
58 for n in Notification.query().all():
61 Session().delete(n)
59 Session().delete(n)
62
60
63 Session().commit()
61 Session().commit()
64 assert Notification.query().all() == []
62 assert Notification.query().all() == []
65
63
66 def test_index(self):
64 def test_index(self):
67 response = self.app.get(login_url)
65 response = self.app.get(login_url)
68 assert response.status == '200 OK'
66 assert response.status == '200 OK'
69 # Test response...
67 # Test response...
70
68
71 def test_login_admin_ok(self):
69 def test_login_admin_ok(self):
72 response = self.app.post(login_url,
70 response = self.app.post(login_url,
73 {'username': 'test_admin',
71 {'username': 'test_admin',
74 'password': 'test12'})
72 'password': 'test12'})
75 assert response.status == '302 Found'
73 assert response.status == '302 Found'
76 session = get_session_from_response(response)
74 session = get_session_from_response(response)
77 username = session['rhodecode_user'].get('username')
75 username = session['rhodecode_user'].get('username')
78 assert username == 'test_admin'
76 assert username == 'test_admin'
79 response = response.follow()
77 response = response.follow()
80 response.mustcontain('/%s' % HG_REPO)
78 response.mustcontain('/%s' % HG_REPO)
81
79
82 def test_login_regular_ok(self):
80 def test_login_regular_ok(self):
83 response = self.app.post(login_url,
81 response = self.app.post(login_url,
84 {'username': 'test_regular',
82 {'username': 'test_regular',
85 'password': 'test12'})
83 'password': 'test12'})
86
84
87 assert response.status == '302 Found'
85 assert response.status == '302 Found'
88 session = get_session_from_response(response)
86 session = get_session_from_response(response)
89 username = session['rhodecode_user'].get('username')
87 username = session['rhodecode_user'].get('username')
90 assert username == 'test_regular'
88 assert username == 'test_regular'
91 response = response.follow()
89 response = response.follow()
92 response.mustcontain('/%s' % HG_REPO)
90 response.mustcontain('/%s' % HG_REPO)
93
91
94 def test_login_ok_came_from(self):
92 def test_login_ok_came_from(self):
95 test_came_from = '/_admin/users?branch=stable'
93 test_came_from = '/_admin/users?branch=stable'
96 _url = '{}?came_from={}'.format(login_url, test_came_from)
94 _url = '{}?came_from={}'.format(login_url, test_came_from)
97 response = self.app.post(
95 response = self.app.post(
98 _url, {'username': 'test_admin', 'password': 'test12'})
96 _url, {'username': 'test_admin', 'password': 'test12'})
99 assert response.status == '302 Found'
97 assert response.status == '302 Found'
100 assert 'branch=stable' in response.location
98 assert 'branch=stable' in response.location
101 response = response.follow()
99 response = response.follow()
102
100
103 assert response.status == '200 OK'
101 assert response.status == '200 OK'
104 response.mustcontain('Users administration')
102 response.mustcontain('Users administration')
105
103
106 def test_redirect_to_login_with_get_args(self):
104 def test_redirect_to_login_with_get_args(self):
107 with fixture.anon_access(False):
105 with fixture.anon_access(False):
108 kwargs = {'branch': 'stable'}
106 kwargs = {'branch': 'stable'}
109 response = self.app.get(
107 response = self.app.get(
110 url('summary_home', repo_name=HG_REPO, **kwargs))
108 url('summary_home', repo_name=HG_REPO, **kwargs))
111 assert response.status == '302 Found'
109 assert response.status == '302 Found'
112 response_query = urlparse.parse_qsl(response.location)
110 response_query = urlparse.parse_qsl(response.location)
113 assert 'branch=stable' in response_query[0][1]
111 assert 'branch=stable' in response_query[0][1]
114
112
115 def test_login_form_with_get_args(self):
113 def test_login_form_with_get_args(self):
116 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
114 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
117 response = self.app.get(_url)
115 response = self.app.get(_url)
118 assert 'branch%3Dstable' in response.form.action
116 assert 'branch%3Dstable' in response.form.action
119
117
120 @pytest.mark.parametrize("url_came_from", [
118 @pytest.mark.parametrize("url_came_from", [
121 'data:text/html,<script>window.alert("xss")</script>',
119 'data:text/html,<script>window.alert("xss")</script>',
122 'mailto:test@rhodecode.org',
120 'mailto:test@rhodecode.org',
123 'file:///etc/passwd',
121 'file:///etc/passwd',
124 'ftp://some.ftp.server',
122 'ftp://some.ftp.server',
125 'http://other.domain',
123 'http://other.domain',
126 '/\r\nX-Forwarded-Host: http://example.org',
124 '/\r\nX-Forwarded-Host: http://example.org',
127 ])
125 ])
128 def test_login_bad_came_froms(self, url_came_from):
126 def test_login_bad_came_froms(self, url_came_from):
129 _url = '{}?came_from={}'.format(login_url, url_came_from)
127 _url = '{}?came_from={}'.format(login_url, url_came_from)
130 response = self.app.post(
128 response = self.app.post(
131 _url,
129 _url,
132 {'username': 'test_admin', 'password': 'test12'})
130 {'username': 'test_admin', 'password': 'test12'})
133 assert response.status == '302 Found'
131 assert response.status == '302 Found'
134 response = response.follow()
132 response = response.follow()
135 assert response.status == '200 OK'
133 assert response.status == '200 OK'
136 assert response.request.path == '/'
134 assert response.request.path == '/'
137
135
138 def test_login_short_password(self):
136 def test_login_short_password(self):
139 response = self.app.post(login_url,
137 response = self.app.post(login_url,
140 {'username': 'test_admin',
138 {'username': 'test_admin',
141 'password': 'as'})
139 'password': 'as'})
142 assert response.status == '200 OK'
140 assert response.status == '200 OK'
143
141
144 response.mustcontain('Enter 3 characters or more')
142 response.mustcontain('Enter 3 characters or more')
145
143
146 def test_login_wrong_non_ascii_password(self, user_regular):
144 def test_login_wrong_non_ascii_password(self, user_regular):
147 response = self.app.post(
145 response = self.app.post(
148 login_url,
146 login_url,
149 {'username': user_regular.username,
147 {'username': user_regular.username,
150 'password': u'invalid-non-asci\xe4'.encode('utf8')})
148 'password': u'invalid-non-asci\xe4'.encode('utf8')})
151
149
152 response.mustcontain('invalid user name')
150 response.mustcontain('invalid user name')
153 response.mustcontain('invalid password')
151 response.mustcontain('invalid password')
154
152
155 def test_login_with_non_ascii_password(self, user_util):
153 def test_login_with_non_ascii_password(self, user_util):
156 password = u'valid-non-ascii\xe4'
154 password = u'valid-non-ascii\xe4'
157 user = user_util.create_user(password=password)
155 user = user_util.create_user(password=password)
158 response = self.app.post(
156 response = self.app.post(
159 login_url,
157 login_url,
160 {'username': user.username,
158 {'username': user.username,
161 'password': password.encode('utf-8')})
159 'password': password.encode('utf-8')})
162 assert response.status_code == 302
160 assert response.status_code == 302
163
161
164 def test_login_wrong_username_password(self):
162 def test_login_wrong_username_password(self):
165 response = self.app.post(login_url,
163 response = self.app.post(login_url,
166 {'username': 'error',
164 {'username': 'error',
167 'password': 'test12'})
165 'password': 'test12'})
168
166
169 response.mustcontain('invalid user name')
167 response.mustcontain('invalid user name')
170 response.mustcontain('invalid password')
168 response.mustcontain('invalid password')
171
169
172 def test_login_admin_ok_password_migration(self, real_crypto_backend):
170 def test_login_admin_ok_password_migration(self, real_crypto_backend):
173 from rhodecode.lib import auth
171 from rhodecode.lib import auth
174
172
175 # create new user, with sha256 password
173 # create new user, with sha256 password
176 temp_user = 'test_admin_sha256'
174 temp_user = 'test_admin_sha256'
177 user = fixture.create_user(temp_user)
175 user = fixture.create_user(temp_user)
178 user.password = auth._RhodeCodeCryptoSha256().hash_create(
176 user.password = auth._RhodeCodeCryptoSha256().hash_create(
179 b'test123')
177 b'test123')
180 Session().add(user)
178 Session().add(user)
181 Session().commit()
179 Session().commit()
182 self.destroy_users.add(temp_user)
180 self.destroy_users.add(temp_user)
183 response = self.app.post(login_url,
181 response = self.app.post(login_url,
184 {'username': temp_user,
182 {'username': temp_user,
185 'password': 'test123'})
183 'password': 'test123'})
186
184
187 assert response.status == '302 Found'
185 assert response.status == '302 Found'
188 session = get_session_from_response(response)
186 session = get_session_from_response(response)
189 username = session['rhodecode_user'].get('username')
187 username = session['rhodecode_user'].get('username')
190 assert username == temp_user
188 assert username == temp_user
191 response = response.follow()
189 response = response.follow()
192 response.mustcontain('/%s' % HG_REPO)
190 response.mustcontain('/%s' % HG_REPO)
193
191
194 # new password should be bcrypted, after log-in and transfer
192 # new password should be bcrypted, after log-in and transfer
195 user = User.get_by_username(temp_user)
193 user = User.get_by_username(temp_user)
196 assert user.password.startswith('$')
194 assert user.password.startswith('$')
197
195
198 # REGISTRATIONS
196 # REGISTRATIONS
199 def test_register(self):
197 def test_register(self):
200 response = self.app.get(register_url)
198 response = self.app.get(register_url)
201 response.mustcontain('Create an Account')
199 response.mustcontain('Create an Account')
202
200
203 def test_register_err_same_username(self):
201 def test_register_err_same_username(self):
204 uname = 'test_admin'
202 uname = 'test_admin'
205 response = self.app.post(
203 response = self.app.post(
206 register_url,
204 register_url,
207 {
205 {
208 'username': uname,
206 'username': uname,
209 'password': 'test12',
207 'password': 'test12',
210 'password_confirmation': 'test12',
208 'password_confirmation': 'test12',
211 'email': 'goodmail@domain.com',
209 'email': 'goodmail@domain.com',
212 'firstname': 'test',
210 'firstname': 'test',
213 'lastname': 'test'
211 'lastname': 'test'
214 }
212 }
215 )
213 )
216
214
217 assertr = AssertResponse(response)
215 assertr = AssertResponse(response)
218 msg = validators.ValidUsername()._messages['username_exists']
216 msg = validators.ValidUsername()._messages['username_exists']
219 msg = msg % {'username': uname}
217 msg = msg % {'username': uname}
220 assertr.element_contains('#username+.error-message', msg)
218 assertr.element_contains('#username+.error-message', msg)
221
219
222 def test_register_err_same_email(self):
220 def test_register_err_same_email(self):
223 response = self.app.post(
221 response = self.app.post(
224 register_url,
222 register_url,
225 {
223 {
226 'username': 'test_admin_0',
224 'username': 'test_admin_0',
227 'password': 'test12',
225 'password': 'test12',
228 'password_confirmation': 'test12',
226 'password_confirmation': 'test12',
229 'email': 'test_admin@mail.com',
227 'email': 'test_admin@mail.com',
230 'firstname': 'test',
228 'firstname': 'test',
231 'lastname': 'test'
229 'lastname': 'test'
232 }
230 }
233 )
231 )
234
232
235 assertr = AssertResponse(response)
233 assertr = AssertResponse(response)
236 msg = validators.UniqSystemEmail()()._messages['email_taken']
234 msg = validators.UniqSystemEmail()()._messages['email_taken']
237 assertr.element_contains('#email+.error-message', msg)
235 assertr.element_contains('#email+.error-message', msg)
238
236
239 def test_register_err_same_email_case_sensitive(self):
237 def test_register_err_same_email_case_sensitive(self):
240 response = self.app.post(
238 response = self.app.post(
241 register_url,
239 register_url,
242 {
240 {
243 'username': 'test_admin_1',
241 'username': 'test_admin_1',
244 'password': 'test12',
242 'password': 'test12',
245 'password_confirmation': 'test12',
243 'password_confirmation': 'test12',
246 'email': 'TesT_Admin@mail.COM',
244 'email': 'TesT_Admin@mail.COM',
247 'firstname': 'test',
245 'firstname': 'test',
248 'lastname': 'test'
246 'lastname': 'test'
249 }
247 }
250 )
248 )
251 assertr = AssertResponse(response)
249 assertr = AssertResponse(response)
252 msg = validators.UniqSystemEmail()()._messages['email_taken']
250 msg = validators.UniqSystemEmail()()._messages['email_taken']
253 assertr.element_contains('#email+.error-message', msg)
251 assertr.element_contains('#email+.error-message', msg)
254
252
255 def test_register_err_wrong_data(self):
253 def test_register_err_wrong_data(self):
256 response = self.app.post(
254 response = self.app.post(
257 register_url,
255 register_url,
258 {
256 {
259 'username': 'xs',
257 'username': 'xs',
260 'password': 'test',
258 'password': 'test',
261 'password_confirmation': 'test',
259 'password_confirmation': 'test',
262 'email': 'goodmailm',
260 'email': 'goodmailm',
263 'firstname': 'test',
261 'firstname': 'test',
264 'lastname': 'test'
262 'lastname': 'test'
265 }
263 }
266 )
264 )
267 assert response.status == '200 OK'
265 assert response.status == '200 OK'
268 response.mustcontain('An email address must contain a single @')
266 response.mustcontain('An email address must contain a single @')
269 response.mustcontain('Enter a value 6 characters long or more')
267 response.mustcontain('Enter a value 6 characters long or more')
270
268
271 def test_register_err_username(self):
269 def test_register_err_username(self):
272 response = self.app.post(
270 response = self.app.post(
273 register_url,
271 register_url,
274 {
272 {
275 'username': 'error user',
273 'username': 'error user',
276 'password': 'test12',
274 'password': 'test12',
277 'password_confirmation': 'test12',
275 'password_confirmation': 'test12',
278 'email': 'goodmailm',
276 'email': 'goodmailm',
279 'firstname': 'test',
277 'firstname': 'test',
280 'lastname': 'test'
278 'lastname': 'test'
281 }
279 }
282 )
280 )
283
281
284 response.mustcontain('An email address must contain a single @')
282 response.mustcontain('An email address must contain a single @')
285 response.mustcontain(
283 response.mustcontain(
286 'Username may only contain '
284 'Username may only contain '
287 'alphanumeric characters underscores, '
285 'alphanumeric characters underscores, '
288 'periods or dashes and must begin with '
286 'periods or dashes and must begin with '
289 'alphanumeric character')
287 'alphanumeric character')
290
288
291 def test_register_err_case_sensitive(self):
289 def test_register_err_case_sensitive(self):
292 usr = 'Test_Admin'
290 usr = 'Test_Admin'
293 response = self.app.post(
291 response = self.app.post(
294 register_url,
292 register_url,
295 {
293 {
296 'username': usr,
294 'username': usr,
297 'password': 'test12',
295 'password': 'test12',
298 'password_confirmation': 'test12',
296 'password_confirmation': 'test12',
299 'email': 'goodmailm',
297 'email': 'goodmailm',
300 'firstname': 'test',
298 'firstname': 'test',
301 'lastname': 'test'
299 'lastname': 'test'
302 }
300 }
303 )
301 )
304
302
305 assertr = AssertResponse(response)
303 assertr = AssertResponse(response)
306 msg = validators.ValidUsername()._messages['username_exists']
304 msg = validators.ValidUsername()._messages['username_exists']
307 msg = msg % {'username': usr}
305 msg = msg % {'username': usr}
308 assertr.element_contains('#username+.error-message', msg)
306 assertr.element_contains('#username+.error-message', msg)
309
307
310 def test_register_special_chars(self):
308 def test_register_special_chars(self):
311 response = self.app.post(
309 response = self.app.post(
312 register_url,
310 register_url,
313 {
311 {
314 'username': 'xxxaxn',
312 'username': 'xxxaxn',
315 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
313 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
316 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
314 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
317 'email': 'goodmailm@test.plx',
315 'email': 'goodmailm@test.plx',
318 'firstname': 'test',
316 'firstname': 'test',
319 'lastname': 'test'
317 'lastname': 'test'
320 }
318 }
321 )
319 )
322
320
323 msg = validators.ValidPassword()._messages['invalid_password']
321 msg = validators.ValidPassword()._messages['invalid_password']
324 response.mustcontain(msg)
322 response.mustcontain(msg)
325
323
326 def test_register_password_mismatch(self):
324 def test_register_password_mismatch(self):
327 response = self.app.post(
325 response = self.app.post(
328 register_url,
326 register_url,
329 {
327 {
330 'username': 'xs',
328 'username': 'xs',
331 'password': '123qwe',
329 'password': '123qwe',
332 'password_confirmation': 'qwe123',
330 'password_confirmation': 'qwe123',
333 'email': 'goodmailm@test.plxa',
331 'email': 'goodmailm@test.plxa',
334 'firstname': 'test',
332 'firstname': 'test',
335 'lastname': 'test'
333 'lastname': 'test'
336 }
334 }
337 )
335 )
338 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
336 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
339 response.mustcontain(msg)
337 response.mustcontain(msg)
340
338
341 def test_register_ok(self):
339 def test_register_ok(self):
342 username = 'test_regular4'
340 username = 'test_regular4'
343 password = 'qweqwe'
341 password = 'qweqwe'
344 email = 'marcin@test.com'
342 email = 'marcin@test.com'
345 name = 'testname'
343 name = 'testname'
346 lastname = 'testlastname'
344 lastname = 'testlastname'
347
345
348 response = self.app.post(
346 response = self.app.post(
349 register_url,
347 register_url,
350 {
348 {
351 'username': username,
349 'username': username,
352 'password': password,
350 'password': password,
353 'password_confirmation': password,
351 'password_confirmation': password,
354 'email': email,
352 'email': email,
355 'firstname': name,
353 'firstname': name,
356 'lastname': lastname,
354 'lastname': lastname,
357 'admin': True
355 'admin': True
358 }
356 }
359 ) # This should be overriden
357 ) # This should be overriden
360 assert response.status == '302 Found'
358 assert response.status == '302 Found'
361 assert_session_flash(
359 assert_session_flash(
362 response, 'You have successfully registered with RhodeCode')
360 response, 'You have successfully registered with RhodeCode')
363
361
364 ret = Session().query(User).filter(
362 ret = Session().query(User).filter(
365 User.username == 'test_regular4').one()
363 User.username == 'test_regular4').one()
366 assert ret.username == username
364 assert ret.username == username
367 assert check_password(password, ret.password)
365 assert check_password(password, ret.password)
368 assert ret.email == email
366 assert ret.email == email
369 assert ret.name == name
367 assert ret.name == name
370 assert ret.lastname == lastname
368 assert ret.lastname == lastname
371 assert ret.api_key is not None
369 assert ret.api_key is not None
372 assert not ret.admin
370 assert not ret.admin
373
371
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)
428
414
429 assert_session_flash(
415 assert_session_flash(
430 response,
416 response,
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 response = response.follow()
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 []}
438 return config
424 return config
439
425
440 @pytest.mark.parametrize("test_name, auth_token", [
426 @pytest.mark.parametrize("test_name, auth_token", [
441 ('none', None),
427 ('none', None),
442 ('empty_string', ''),
428 ('empty_string', ''),
443 ('fake_number', '123456'),
429 ('fake_number', '123456'),
444 ('proper_auth_token', None)
430 ('proper_auth_token', None)
445 ])
431 ])
446 def test_access_not_whitelisted_page_via_auth_token(
432 def test_access_not_whitelisted_page_via_auth_token(
447 self, test_name, auth_token, user_admin):
433 self, test_name, auth_token, user_admin):
448
434
449 whitelist = self._get_api_whitelist([])
435 whitelist = self._get_api_whitelist([])
450 with mock.patch.dict('rhodecode.CONFIG', whitelist):
436 with mock.patch.dict('rhodecode.CONFIG', whitelist):
451 assert [] == whitelist['api_access_controllers_whitelist']
437 assert [] == whitelist['api_access_controllers_whitelist']
452 if test_name == 'proper_auth_token':
438 if test_name == 'proper_auth_token':
453 # use builtin if api_key is None
439 # use builtin if api_key is None
454 auth_token = user_admin.api_key
440 auth_token = user_admin.api_key
455
441
456 with fixture.anon_access(False):
442 with fixture.anon_access(False):
457 self.app.get(url(controller='changeset',
443 self.app.get(url(controller='changeset',
458 action='changeset_raw',
444 action='changeset_raw',
459 repo_name=HG_REPO, revision='tip',
445 repo_name=HG_REPO, revision='tip',
460 api_key=auth_token),
446 api_key=auth_token),
461 status=302)
447 status=302)
462
448
463 @pytest.mark.parametrize("test_name, auth_token, code", [
449 @pytest.mark.parametrize("test_name, auth_token, code", [
464 ('none', None, 302),
450 ('none', None, 302),
465 ('empty_string', '', 302),
451 ('empty_string', '', 302),
466 ('fake_number', '123456', 302),
452 ('fake_number', '123456', 302),
467 ('proper_auth_token', None, 200)
453 ('proper_auth_token', None, 200)
468 ])
454 ])
469 def test_access_whitelisted_page_via_auth_token(
455 def test_access_whitelisted_page_via_auth_token(
470 self, test_name, auth_token, code, user_admin):
456 self, test_name, auth_token, code, user_admin):
471
457
472 whitelist_entry = ['ChangesetController:changeset_raw']
458 whitelist_entry = ['ChangesetController:changeset_raw']
473 whitelist = self._get_api_whitelist(whitelist_entry)
459 whitelist = self._get_api_whitelist(whitelist_entry)
474
460
475 with mock.patch.dict('rhodecode.CONFIG', whitelist):
461 with mock.patch.dict('rhodecode.CONFIG', whitelist):
476 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
462 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
477
463
478 if test_name == 'proper_auth_token':
464 if test_name == 'proper_auth_token':
479 auth_token = user_admin.api_key
465 auth_token = user_admin.api_key
480
466
481 with fixture.anon_access(False):
467 with fixture.anon_access(False):
482 self.app.get(url(controller='changeset',
468 self.app.get(url(controller='changeset',
483 action='changeset_raw',
469 action='changeset_raw',
484 repo_name=HG_REPO, revision='tip',
470 repo_name=HG_REPO, revision='tip',
485 api_key=auth_token),
471 api_key=auth_token),
486 status=code)
472 status=code)
487
473
488 def test_access_page_via_extra_auth_token(self):
474 def test_access_page_via_extra_auth_token(self):
489 whitelist = self._get_api_whitelist(
475 whitelist = self._get_api_whitelist(
490 ['ChangesetController:changeset_raw'])
476 ['ChangesetController:changeset_raw'])
491 with mock.patch.dict('rhodecode.CONFIG', whitelist):
477 with mock.patch.dict('rhodecode.CONFIG', whitelist):
492 assert ['ChangesetController:changeset_raw'] == \
478 assert ['ChangesetController:changeset_raw'] == \
493 whitelist['api_access_controllers_whitelist']
479 whitelist['api_access_controllers_whitelist']
494
480
495 new_auth_token = AuthTokenModel().create(
481 new_auth_token = AuthTokenModel().create(
496 TEST_USER_ADMIN_LOGIN, 'test')
482 TEST_USER_ADMIN_LOGIN, 'test')
497 Session().commit()
483 Session().commit()
498 with fixture.anon_access(False):
484 with fixture.anon_access(False):
499 self.app.get(url(controller='changeset',
485 self.app.get(url(controller='changeset',
500 action='changeset_raw',
486 action='changeset_raw',
501 repo_name=HG_REPO, revision='tip',
487 repo_name=HG_REPO, revision='tip',
502 api_key=new_auth_token.api_key),
488 api_key=new_auth_token.api_key),
503 status=200)
489 status=200)
504
490
505 def test_access_page_via_expired_auth_token(self):
491 def test_access_page_via_expired_auth_token(self):
506 whitelist = self._get_api_whitelist(
492 whitelist = self._get_api_whitelist(
507 ['ChangesetController:changeset_raw'])
493 ['ChangesetController:changeset_raw'])
508 with mock.patch.dict('rhodecode.CONFIG', whitelist):
494 with mock.patch.dict('rhodecode.CONFIG', whitelist):
509 assert ['ChangesetController:changeset_raw'] == \
495 assert ['ChangesetController:changeset_raw'] == \
510 whitelist['api_access_controllers_whitelist']
496 whitelist['api_access_controllers_whitelist']
511
497
512 new_auth_token = AuthTokenModel().create(
498 new_auth_token = AuthTokenModel().create(
513 TEST_USER_ADMIN_LOGIN, 'test')
499 TEST_USER_ADMIN_LOGIN, 'test')
514 Session().commit()
500 Session().commit()
515 # patch the api key and make it expired
501 # patch the api key and make it expired
516 new_auth_token.expires = 0
502 new_auth_token.expires = 0
517 Session().add(new_auth_token)
503 Session().add(new_auth_token)
518 Session().commit()
504 Session().commit()
519 with fixture.anon_access(False):
505 with fixture.anon_access(False):
520 self.app.get(url(controller='changeset',
506 self.app.get(url(controller='changeset',
521 action='changeset_raw',
507 action='changeset_raw',
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