##// END OF EJS Templates
registration: updated flash with more information after user registered.
marcink -
r4058:0e4c6b60 default
parent child Browse files
Show More
@@ -1,579 +1,579 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.tests import (
27 27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 28 no_newline_id_generator)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.lib.auth import check_password
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 33 from rhodecode.model.db import User, Notification, UserApiKeys
34 34 from rhodecode.model.meta import Session
35 35
36 36 fixture = Fixture()
37 37
38 38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 39
40 40
41 41 def route_path(name, params=None, **kwargs):
42 42 import urllib
43 43 from rhodecode.apps._base import ADMIN_PREFIX
44 44
45 45 base_url = {
46 46 'login': ADMIN_PREFIX + '/login',
47 47 'logout': ADMIN_PREFIX + '/logout',
48 48 'register': ADMIN_PREFIX + '/register',
49 49 'reset_password':
50 50 ADMIN_PREFIX + '/password_reset',
51 51 'reset_password_confirmation':
52 52 ADMIN_PREFIX + '/password_reset_confirmation',
53 53
54 54 'admin_permissions_application':
55 55 ADMIN_PREFIX + '/permissions/application',
56 56 'admin_permissions_application_update':
57 57 ADMIN_PREFIX + '/permissions/application/update',
58 58
59 59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 60
61 61 }[name].format(**kwargs)
62 62
63 63 if params:
64 64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 65 return base_url
66 66
67 67
68 68 @pytest.mark.usefixtures('app')
69 69 class TestLoginController(object):
70 70 destroy_users = set()
71 71
72 72 @classmethod
73 73 def teardown_class(cls):
74 74 fixture.destroy_users(cls.destroy_users)
75 75
76 76 def teardown_method(self, method):
77 77 for n in Notification.query().all():
78 78 Session().delete(n)
79 79
80 80 Session().commit()
81 81 assert Notification.query().all() == []
82 82
83 83 def test_index(self):
84 84 response = self.app.get(route_path('login'))
85 85 assert response.status == '200 OK'
86 86 # Test response...
87 87
88 88 def test_login_admin_ok(self):
89 89 response = self.app.post(route_path('login'),
90 90 {'username': 'test_admin',
91 91 'password': 'test12'}, status=302)
92 92 response = response.follow()
93 93 session = response.get_session_from_response()
94 94 username = session['rhodecode_user'].get('username')
95 95 assert username == 'test_admin'
96 96 response.mustcontain('/%s' % HG_REPO)
97 97
98 98 def test_login_regular_ok(self):
99 99 response = self.app.post(route_path('login'),
100 100 {'username': 'test_regular',
101 101 'password': 'test12'}, status=302)
102 102
103 103 response = response.follow()
104 104 session = response.get_session_from_response()
105 105 username = session['rhodecode_user'].get('username')
106 106 assert username == 'test_regular'
107 107
108 108 response.mustcontain('/%s' % HG_REPO)
109 109
110 110 def test_login_regular_forbidden_when_super_admin_restriction(self):
111 111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
112 112 with fixture.auth_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 113 response = self.app.post(route_path('login'),
114 114 {'username': 'test_regular',
115 115 'password': 'test12'})
116 116
117 117 response.mustcontain('invalid user name')
118 118 response.mustcontain('invalid password')
119 119
120 120 def test_login_regular_forbidden_when_scope_restriction(self):
121 121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 122 with fixture.scope_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
123 123 response = self.app.post(route_path('login'),
124 124 {'username': 'test_regular',
125 125 'password': 'test12'})
126 126
127 127 response.mustcontain('invalid user name')
128 128 response.mustcontain('invalid password')
129 129
130 130 def test_login_ok_came_from(self):
131 131 test_came_from = '/_admin/users?branch=stable'
132 132 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
133 133 response = self.app.post(
134 134 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
135 135
136 136 assert 'branch=stable' in response.location
137 137 response = response.follow()
138 138
139 139 assert response.status == '200 OK'
140 140 response.mustcontain('Users administration')
141 141
142 142 def test_redirect_to_login_with_get_args(self):
143 143 with fixture.anon_access(False):
144 144 kwargs = {'branch': 'stable'}
145 145 response = self.app.get(
146 146 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
147 147 status=302)
148 148
149 149 response_query = urlparse.parse_qsl(response.location)
150 150 assert 'branch=stable' in response_query[0][1]
151 151
152 152 def test_login_form_with_get_args(self):
153 153 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
154 154 response = self.app.get(_url)
155 155 assert 'branch%3Dstable' in response.form.action
156 156
157 157 @pytest.mark.parametrize("url_came_from", [
158 158 'data:text/html,<script>window.alert("xss")</script>',
159 159 'mailto:test@rhodecode.org',
160 160 'file:///etc/passwd',
161 161 'ftp://some.ftp.server',
162 162 'http://other.domain',
163 163 '/\r\nX-Forwarded-Host: http://example.org',
164 164 ], ids=no_newline_id_generator)
165 165 def test_login_bad_came_froms(self, url_came_from):
166 166 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 167 response = self.app.post(
168 168 _url,
169 169 {'username': 'test_admin', 'password': 'test12'})
170 170 assert response.status == '302 Found'
171 171 response = response.follow()
172 172 assert response.status == '200 OK'
173 173 assert response.request.path == '/'
174 174
175 175 def test_login_short_password(self):
176 176 response = self.app.post(route_path('login'),
177 177 {'username': 'test_admin',
178 178 'password': 'as'})
179 179 assert response.status == '200 OK'
180 180
181 181 response.mustcontain('Enter 3 characters or more')
182 182
183 183 def test_login_wrong_non_ascii_password(self, user_regular):
184 184 response = self.app.post(
185 185 route_path('login'),
186 186 {'username': user_regular.username,
187 187 'password': u'invalid-non-asci\xe4'.encode('utf8')})
188 188
189 189 response.mustcontain('invalid user name')
190 190 response.mustcontain('invalid password')
191 191
192 192 def test_login_with_non_ascii_password(self, user_util):
193 193 password = u'valid-non-ascii\xe4'
194 194 user = user_util.create_user(password=password)
195 195 response = self.app.post(
196 196 route_path('login'),
197 197 {'username': user.username,
198 198 'password': password.encode('utf-8')})
199 199 assert response.status_code == 302
200 200
201 201 def test_login_wrong_username_password(self):
202 202 response = self.app.post(route_path('login'),
203 203 {'username': 'error',
204 204 'password': 'test12'})
205 205
206 206 response.mustcontain('invalid user name')
207 207 response.mustcontain('invalid password')
208 208
209 209 def test_login_admin_ok_password_migration(self, real_crypto_backend):
210 210 from rhodecode.lib import auth
211 211
212 212 # create new user, with sha256 password
213 213 temp_user = 'test_admin_sha256'
214 214 user = fixture.create_user(temp_user)
215 215 user.password = auth._RhodeCodeCryptoSha256().hash_create(
216 216 b'test123')
217 217 Session().add(user)
218 218 Session().commit()
219 219 self.destroy_users.add(temp_user)
220 220 response = self.app.post(route_path('login'),
221 221 {'username': temp_user,
222 222 'password': 'test123'}, status=302)
223 223
224 224 response = response.follow()
225 225 session = response.get_session_from_response()
226 226 username = session['rhodecode_user'].get('username')
227 227 assert username == temp_user
228 228 response.mustcontain('/%s' % HG_REPO)
229 229
230 230 # new password should be bcrypted, after log-in and transfer
231 231 user = User.get_by_username(temp_user)
232 232 assert user.password.startswith('$')
233 233
234 234 # REGISTRATIONS
235 235 def test_register(self):
236 236 response = self.app.get(route_path('register'))
237 237 response.mustcontain('Create an Account')
238 238
239 239 def test_register_err_same_username(self):
240 240 uname = 'test_admin'
241 241 response = self.app.post(
242 242 route_path('register'),
243 243 {
244 244 'username': uname,
245 245 'password': 'test12',
246 246 'password_confirmation': 'test12',
247 247 'email': 'goodmail@domain.com',
248 248 'firstname': 'test',
249 249 'lastname': 'test'
250 250 }
251 251 )
252 252
253 253 assertr = response.assert_response()
254 254 msg = 'Username "%(username)s" already exists'
255 255 msg = msg % {'username': uname}
256 256 assertr.element_contains('#username+.error-message', msg)
257 257
258 258 def test_register_err_same_email(self):
259 259 response = self.app.post(
260 260 route_path('register'),
261 261 {
262 262 'username': 'test_admin_0',
263 263 'password': 'test12',
264 264 'password_confirmation': 'test12',
265 265 'email': 'test_admin@mail.com',
266 266 'firstname': 'test',
267 267 'lastname': 'test'
268 268 }
269 269 )
270 270
271 271 assertr = response.assert_response()
272 272 msg = u'This e-mail address is already taken'
273 273 assertr.element_contains('#email+.error-message', msg)
274 274
275 275 def test_register_err_same_email_case_sensitive(self):
276 276 response = self.app.post(
277 277 route_path('register'),
278 278 {
279 279 'username': 'test_admin_1',
280 280 'password': 'test12',
281 281 'password_confirmation': 'test12',
282 282 'email': 'TesT_Admin@mail.COM',
283 283 'firstname': 'test',
284 284 'lastname': 'test'
285 285 }
286 286 )
287 287 assertr = response.assert_response()
288 288 msg = u'This e-mail address is already taken'
289 289 assertr.element_contains('#email+.error-message', msg)
290 290
291 291 def test_register_err_wrong_data(self):
292 292 response = self.app.post(
293 293 route_path('register'),
294 294 {
295 295 'username': 'xs',
296 296 'password': 'test',
297 297 'password_confirmation': 'test',
298 298 'email': 'goodmailm',
299 299 'firstname': 'test',
300 300 'lastname': 'test'
301 301 }
302 302 )
303 303 assert response.status == '200 OK'
304 304 response.mustcontain('An email address must contain a single @')
305 305 response.mustcontain('Enter a value 6 characters long or more')
306 306
307 307 def test_register_err_username(self):
308 308 response = self.app.post(
309 309 route_path('register'),
310 310 {
311 311 'username': 'error user',
312 312 'password': 'test12',
313 313 'password_confirmation': 'test12',
314 314 'email': 'goodmailm',
315 315 'firstname': 'test',
316 316 'lastname': 'test'
317 317 }
318 318 )
319 319
320 320 response.mustcontain('An email address must contain a single @')
321 321 response.mustcontain(
322 322 'Username may only contain '
323 323 'alphanumeric characters underscores, '
324 324 'periods or dashes and must begin with '
325 325 'alphanumeric character')
326 326
327 327 def test_register_err_case_sensitive(self):
328 328 usr = 'Test_Admin'
329 329 response = self.app.post(
330 330 route_path('register'),
331 331 {
332 332 'username': usr,
333 333 'password': 'test12',
334 334 'password_confirmation': 'test12',
335 335 'email': 'goodmailm',
336 336 'firstname': 'test',
337 337 'lastname': 'test'
338 338 }
339 339 )
340 340
341 341 assertr = response.assert_response()
342 342 msg = u'Username "%(username)s" already exists'
343 343 msg = msg % {'username': usr}
344 344 assertr.element_contains('#username+.error-message', msg)
345 345
346 346 def test_register_special_chars(self):
347 347 response = self.app.post(
348 348 route_path('register'),
349 349 {
350 350 'username': 'xxxaxn',
351 351 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
352 352 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
353 353 'email': 'goodmailm@test.plx',
354 354 'firstname': 'test',
355 355 'lastname': 'test'
356 356 }
357 357 )
358 358
359 359 msg = u'Invalid characters (non-ascii) in password'
360 360 response.mustcontain(msg)
361 361
362 362 def test_register_password_mismatch(self):
363 363 response = self.app.post(
364 364 route_path('register'),
365 365 {
366 366 'username': 'xs',
367 367 'password': '123qwe',
368 368 'password_confirmation': 'qwe123',
369 369 'email': 'goodmailm@test.plxa',
370 370 'firstname': 'test',
371 371 'lastname': 'test'
372 372 }
373 373 )
374 374 msg = u'Passwords do not match'
375 375 response.mustcontain(msg)
376 376
377 377 def test_register_ok(self):
378 378 username = 'test_regular4'
379 379 password = 'qweqwe'
380 380 email = 'marcin@test.com'
381 381 name = 'testname'
382 382 lastname = 'testlastname'
383 383
384 384 # this initializes a session
385 385 response = self.app.get(route_path('register'))
386 386 response.mustcontain('Create an Account')
387 387
388 388
389 389 response = self.app.post(
390 390 route_path('register'),
391 391 {
392 392 'username': username,
393 393 'password': password,
394 394 'password_confirmation': password,
395 395 'email': email,
396 396 'firstname': name,
397 397 'lastname': lastname,
398 398 'admin': True
399 399 },
400 400 status=302
401 401 ) # This should be overridden
402 402
403 403 assert_session_flash(
404 response, 'You have successfully registered with RhodeCode')
404 response, 'You have successfully registered with RhodeCode. You can log-in now.')
405 405
406 406 ret = Session().query(User).filter(
407 407 User.username == 'test_regular4').one()
408 408 assert ret.username == username
409 409 assert check_password(password, ret.password)
410 410 assert ret.email == email
411 411 assert ret.name == name
412 412 assert ret.lastname == lastname
413 413 assert ret.auth_tokens is not None
414 414 assert not ret.admin
415 415
416 416 def test_forgot_password_wrong_mail(self):
417 417 bad_email = 'marcin@wrongmail.org'
418 418 # this initializes a session
419 419 self.app.get(route_path('reset_password'))
420 420
421 421 response = self.app.post(
422 422 route_path('reset_password'), {'email': bad_email, }
423 423 )
424 424 assert_session_flash(response,
425 425 'If such email exists, a password reset link was sent to it.')
426 426
427 427 def test_forgot_password(self, user_util):
428 428 # this initializes a session
429 429 self.app.get(route_path('reset_password'))
430 430
431 431 user = user_util.create_user()
432 432 user_id = user.user_id
433 433 email = user.email
434 434
435 435 response = self.app.post(route_path('reset_password'), {'email': email, })
436 436
437 437 assert_session_flash(response,
438 438 'If such email exists, a password reset link was sent to it.')
439 439
440 440 # BAD KEY
441 441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
442 442 response = self.app.get(confirm_url, status=302)
443 443 assert response.location.endswith(route_path('reset_password'))
444 444 assert_session_flash(response, 'Given reset token is invalid')
445 445
446 446 response.follow() # cleanup flash
447 447
448 448 # GOOD KEY
449 449 key = UserApiKeys.query()\
450 450 .filter(UserApiKeys.user_id == user_id)\
451 451 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
452 452 .first()
453 453
454 454 assert key
455 455
456 456 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
457 457 response = self.app.get(confirm_url)
458 458 assert response.status == '302 Found'
459 459 assert response.location.endswith(route_path('login'))
460 460
461 461 assert_session_flash(
462 462 response,
463 463 'Your password reset was successful, '
464 464 'a new password has been sent to your email')
465 465
466 466 response.follow()
467 467
468 468 def _get_api_whitelist(self, values=None):
469 469 config = {'api_access_controllers_whitelist': values or []}
470 470 return config
471 471
472 472 @pytest.mark.parametrize("test_name, auth_token", [
473 473 ('none', None),
474 474 ('empty_string', ''),
475 475 ('fake_number', '123456'),
476 476 ('proper_auth_token', None)
477 477 ])
478 478 def test_access_not_whitelisted_page_via_auth_token(
479 479 self, test_name, auth_token, user_admin):
480 480
481 481 whitelist = self._get_api_whitelist([])
482 482 with mock.patch.dict('rhodecode.CONFIG', whitelist):
483 483 assert [] == whitelist['api_access_controllers_whitelist']
484 484 if test_name == 'proper_auth_token':
485 485 # use builtin if api_key is None
486 486 auth_token = user_admin.api_key
487 487
488 488 with fixture.anon_access(False):
489 489 self.app.get(
490 490 route_path('repo_commit_raw',
491 491 repo_name=HG_REPO, commit_id='tip',
492 492 params=dict(api_key=auth_token)),
493 493 status=302)
494 494
495 495 @pytest.mark.parametrize("test_name, auth_token, code", [
496 496 ('none', None, 302),
497 497 ('empty_string', '', 302),
498 498 ('fake_number', '123456', 302),
499 499 ('proper_auth_token', None, 200)
500 500 ])
501 501 def test_access_whitelisted_page_via_auth_token(
502 502 self, test_name, auth_token, code, user_admin):
503 503
504 504 whitelist = self._get_api_whitelist(whitelist_view)
505 505
506 506 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 507 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508 508
509 509 if test_name == 'proper_auth_token':
510 510 auth_token = user_admin.api_key
511 511 assert auth_token
512 512
513 513 with fixture.anon_access(False):
514 514 self.app.get(
515 515 route_path('repo_commit_raw',
516 516 repo_name=HG_REPO, commit_id='tip',
517 517 params=dict(api_key=auth_token)),
518 518 status=code)
519 519
520 520 @pytest.mark.parametrize("test_name, auth_token, code", [
521 521 ('proper_auth_token', None, 200),
522 522 ('wrong_auth_token', '123456', 302),
523 523 ])
524 524 def test_access_whitelisted_page_via_auth_token_bound_to_token(
525 525 self, test_name, auth_token, code, user_admin):
526 526
527 527 expected_token = auth_token
528 528 if test_name == 'proper_auth_token':
529 529 auth_token = user_admin.api_key
530 530 expected_token = auth_token
531 531 assert auth_token
532 532
533 533 whitelist = self._get_api_whitelist([
534 534 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
535 535
536 536 with mock.patch.dict('rhodecode.CONFIG', whitelist):
537 537
538 538 with fixture.anon_access(False):
539 539 self.app.get(
540 540 route_path('repo_commit_raw',
541 541 repo_name=HG_REPO, commit_id='tip',
542 542 params=dict(api_key=auth_token)),
543 543 status=code)
544 544
545 545 def test_access_page_via_extra_auth_token(self):
546 546 whitelist = self._get_api_whitelist(whitelist_view)
547 547 with mock.patch.dict('rhodecode.CONFIG', whitelist):
548 548 assert whitelist_view == \
549 549 whitelist['api_access_controllers_whitelist']
550 550
551 551 new_auth_token = AuthTokenModel().create(
552 552 TEST_USER_ADMIN_LOGIN, 'test')
553 553 Session().commit()
554 554 with fixture.anon_access(False):
555 555 self.app.get(
556 556 route_path('repo_commit_raw',
557 557 repo_name=HG_REPO, commit_id='tip',
558 558 params=dict(api_key=new_auth_token.api_key)),
559 559 status=200)
560 560
561 561 def test_access_page_via_expired_auth_token(self):
562 562 whitelist = self._get_api_whitelist(whitelist_view)
563 563 with mock.patch.dict('rhodecode.CONFIG', whitelist):
564 564 assert whitelist_view == \
565 565 whitelist['api_access_controllers_whitelist']
566 566
567 567 new_auth_token = AuthTokenModel().create(
568 568 TEST_USER_ADMIN_LOGIN, 'test')
569 569 Session().commit()
570 570 # patch the api key and make it expired
571 571 new_auth_token.expires = 0
572 572 Session().add(new_auth_token)
573 573 Session().commit()
574 574 with fixture.anon_access(False):
575 575 self.app.get(
576 576 route_path('repo_commit_raw',
577 577 repo_name=HG_REPO, commit_id='tip',
578 578 params=dict(api_key=new_auth_token.api_key)),
579 579 status=302)
@@ -1,486 +1,489 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import logging
27 27 import urlparse
28 28 import requests
29 29
30 30 from pyramid.httpexceptions import HTTPFound
31 31 from pyramid.view import view_config
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import UserRegistered, trigger
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.auth import (
40 40 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
41 41 from rhodecode.lib.base import get_ip_addr
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.model.db import User, UserApiKeys
45 45 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.settings import SettingsModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.translation import _
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55 CaptchaData = collections.namedtuple(
56 56 'CaptchaData', 'active, private_key, public_key')
57 57
58 58
59 59 def store_user_in_session(session, username, remember=False):
60 60 user = User.get_by_username(username, case_insensitive=True)
61 61 auth_user = AuthUser(user.user_id)
62 62 auth_user.set_authenticated()
63 63 cs = auth_user.get_cookie_store()
64 64 session['rhodecode_user'] = cs
65 65 user.update_lastlogin()
66 66 Session().commit()
67 67
68 68 # If they want to be remembered, update the cookie
69 69 if remember:
70 70 _year = (datetime.datetime.now() +
71 71 datetime.timedelta(seconds=60 * 60 * 24 * 365))
72 72 session._set_cookie_expires(_year)
73 73
74 74 session.save()
75 75
76 76 safe_cs = cs.copy()
77 77 safe_cs['password'] = '****'
78 78 log.info('user %s is now authenticated and stored in '
79 79 'session, session attrs %s', username, safe_cs)
80 80
81 81 # dumps session attrs back to cookie
82 82 session._update_cookie_out()
83 83 # we set new cookie
84 84 headers = None
85 85 if session.request['set_cookie']:
86 86 # send set-cookie headers back to response to update cookie
87 87 headers = [('Set-Cookie', session.request['cookie_out'])]
88 88 return headers
89 89
90 90
91 91 def get_came_from(request):
92 92 came_from = safe_str(request.GET.get('came_from', ''))
93 93 parsed = urlparse.urlparse(came_from)
94 94 allowed_schemes = ['http', 'https']
95 95 default_came_from = h.route_path('home')
96 96 if parsed.scheme and parsed.scheme not in allowed_schemes:
97 97 log.error('Suspicious URL scheme detected %s for url %s',
98 98 parsed.scheme, parsed)
99 99 came_from = default_came_from
100 100 elif parsed.netloc and request.host != parsed.netloc:
101 101 log.error('Suspicious NETLOC detected %s for url %s server url '
102 102 'is: %s', parsed.netloc, parsed, request.host)
103 103 came_from = default_came_from
104 104 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
105 105 log.error('Header injection detected `%s` for url %s server url ',
106 106 parsed.path, parsed)
107 107 came_from = default_came_from
108 108
109 109 return came_from or default_came_from
110 110
111 111
112 112 class LoginView(BaseAppView):
113 113
114 114 def load_default_context(self):
115 115 c = self._get_local_tmpl_context()
116 116 c.came_from = get_came_from(self.request)
117 117
118 118 return c
119 119
120 120 def _get_captcha_data(self):
121 121 settings = SettingsModel().get_all_settings()
122 122 private_key = settings.get('rhodecode_captcha_private_key')
123 123 public_key = settings.get('rhodecode_captcha_public_key')
124 124 active = bool(private_key)
125 125 return CaptchaData(
126 126 active=active, private_key=private_key, public_key=public_key)
127 127
128 128 def validate_captcha(self, private_key):
129 129
130 130 captcha_rs = self.request.POST.get('g-recaptcha-response')
131 131 url = "https://www.google.com/recaptcha/api/siteverify"
132 132 params = {
133 133 'secret': private_key,
134 134 'response': captcha_rs,
135 135 'remoteip': get_ip_addr(self.request.environ)
136 136 }
137 137 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
138 138 verify_rs = verify_rs.json()
139 139 captcha_status = verify_rs.get('success', False)
140 140 captcha_errors = verify_rs.get('error-codes', [])
141 141 if not isinstance(captcha_errors, list):
142 142 captcha_errors = [captcha_errors]
143 143 captcha_errors = ', '.join(captcha_errors)
144 144 captcha_message = ''
145 145 if captcha_status is False:
146 146 captcha_message = "Bad captcha. Errors: {}".format(
147 147 captcha_errors)
148 148
149 149 return captcha_status, captcha_message
150 150
151 151 @view_config(
152 152 route_name='login', request_method='GET',
153 153 renderer='rhodecode:templates/login.mako')
154 154 def login(self):
155 155 c = self.load_default_context()
156 156 auth_user = self._rhodecode_user
157 157
158 158 # redirect if already logged in
159 159 if (auth_user.is_authenticated and
160 160 not auth_user.is_default and auth_user.ip_allowed):
161 161 raise HTTPFound(c.came_from)
162 162
163 163 # check if we use headers plugin, and try to login using it.
164 164 try:
165 165 log.debug('Running PRE-AUTH for headers based authentication')
166 166 auth_info = authenticate(
167 167 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
168 168 if auth_info:
169 169 headers = store_user_in_session(
170 170 self.session, auth_info.get('username'))
171 171 raise HTTPFound(c.came_from, headers=headers)
172 172 except UserCreationError as e:
173 173 log.error(e)
174 174 h.flash(e, category='error')
175 175
176 176 return self._get_template_context(c)
177 177
178 178 @view_config(
179 179 route_name='login', request_method='POST',
180 180 renderer='rhodecode:templates/login.mako')
181 181 def login_post(self):
182 182 c = self.load_default_context()
183 183
184 184 login_form = LoginForm(self.request.translate)()
185 185
186 186 try:
187 187 self.session.invalidate()
188 188 form_result = login_form.to_python(self.request.POST)
189 189 # form checks for username/password, now we're authenticated
190 190 headers = store_user_in_session(
191 191 self.session,
192 192 username=form_result['username'],
193 193 remember=form_result['remember'])
194 194 log.debug('Redirecting to "%s" after login.', c.came_from)
195 195
196 196 audit_user = audit_logger.UserWrap(
197 197 username=self.request.POST.get('username'),
198 198 ip_addr=self.request.remote_addr)
199 199 action_data = {'user_agent': self.request.user_agent}
200 200 audit_logger.store_web(
201 201 'user.login.success', action_data=action_data,
202 202 user=audit_user, commit=True)
203 203
204 204 raise HTTPFound(c.came_from, headers=headers)
205 205 except formencode.Invalid as errors:
206 206 defaults = errors.value
207 207 # remove password from filling in form again
208 208 defaults.pop('password', None)
209 209 render_ctx = {
210 210 'errors': errors.error_dict,
211 211 'defaults': defaults,
212 212 }
213 213
214 214 audit_user = audit_logger.UserWrap(
215 215 username=self.request.POST.get('username'),
216 216 ip_addr=self.request.remote_addr)
217 217 action_data = {'user_agent': self.request.user_agent}
218 218 audit_logger.store_web(
219 219 'user.login.failure', action_data=action_data,
220 220 user=audit_user, commit=True)
221 221 return self._get_template_context(c, **render_ctx)
222 222
223 223 except UserCreationError as e:
224 224 # headers auth or other auth functions that create users on
225 225 # the fly can throw this exception signaling that there's issue
226 226 # with user creation, explanation should be provided in
227 227 # Exception itself
228 228 h.flash(e, category='error')
229 229 return self._get_template_context(c)
230 230
231 231 @CSRFRequired()
232 232 @view_config(route_name='logout', request_method='POST')
233 233 def logout(self):
234 234 auth_user = self._rhodecode_user
235 235 log.info('Deleting session for user: `%s`', auth_user)
236 236
237 237 action_data = {'user_agent': self.request.user_agent}
238 238 audit_logger.store_web(
239 239 'user.logout', action_data=action_data,
240 240 user=auth_user, commit=True)
241 241 self.session.delete()
242 242 return HTTPFound(h.route_path('home'))
243 243
244 244 @HasPermissionAnyDecorator(
245 245 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
246 246 @view_config(
247 247 route_name='register', request_method='GET',
248 248 renderer='rhodecode:templates/register.mako',)
249 249 def register(self, defaults=None, errors=None):
250 250 c = self.load_default_context()
251 251 defaults = defaults or {}
252 252 errors = errors or {}
253 253
254 254 settings = SettingsModel().get_all_settings()
255 255 register_message = settings.get('rhodecode_register_message') or ''
256 256 captcha = self._get_captcha_data()
257 257 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
258 258 .AuthUser().permissions['global']
259 259
260 260 render_ctx = self._get_template_context(c)
261 261 render_ctx.update({
262 262 'defaults': defaults,
263 263 'errors': errors,
264 264 'auto_active': auto_active,
265 265 'captcha_active': captcha.active,
266 266 'captcha_public_key': captcha.public_key,
267 267 'register_message': register_message,
268 268 })
269 269 return render_ctx
270 270
271 271 @HasPermissionAnyDecorator(
272 272 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
273 273 @view_config(
274 274 route_name='register', request_method='POST',
275 275 renderer='rhodecode:templates/register.mako')
276 276 def register_post(self):
277 277 from rhodecode.authentication.plugins import auth_rhodecode
278 278
279 279 self.load_default_context()
280 280 captcha = self._get_captcha_data()
281 281 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
282 282 .AuthUser().permissions['global']
283 283
284 284 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
285 285 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
286 286
287 287 register_form = RegisterForm(self.request.translate)()
288 288 try:
289 289
290 290 form_result = register_form.to_python(self.request.POST)
291 291 form_result['active'] = auto_active
292 292 external_identity = self.request.POST.get('external_identity')
293 293
294 294 if external_identity:
295 295 extern_name = external_identity
296 296 extern_type = external_identity
297 297
298 298 if captcha.active:
299 299 captcha_status, captcha_message = self.validate_captcha(
300 300 captcha.private_key)
301 301
302 302 if not captcha_status:
303 303 _value = form_result
304 304 _msg = _('Bad captcha')
305 305 error_dict = {'recaptcha_field': captcha_message}
306 306 raise formencode.Invalid(
307 307 _msg, _value, None, error_dict=error_dict)
308 308
309 309 new_user = UserModel().create_registration(
310 310 form_result, extern_name=extern_name, extern_type=extern_type)
311 311
312 312 action_data = {'data': new_user.get_api_data(),
313 313 'user_agent': self.request.user_agent}
314 314
315
316
317 315 if external_identity:
318 316 action_data['external_identity'] = external_identity
319 317
320 318 audit_user = audit_logger.UserWrap(
321 319 username=new_user.username,
322 320 user_id=new_user.user_id,
323 321 ip_addr=self.request.remote_addr)
324 322
325 323 audit_logger.store_web(
326 324 'user.register', action_data=action_data,
327 325 user=audit_user)
328 326
329 327 event = UserRegistered(user=new_user, session=self.session)
330 328 trigger(event)
331 329 h.flash(
332 _('You have successfully registered with RhodeCode'),
330 _('You have successfully registered with RhodeCode. You can log-in now.'),
333 331 category='success')
332 if external_identity:
333 h.flash(
334 _('Please use the {identity} button to log-in').format(
335 identity=external_identity),
336 category='success')
334 337 Session().commit()
335 338
336 339 redirect_ro = self.request.route_path('login')
337 340 raise HTTPFound(redirect_ro)
338 341
339 342 except formencode.Invalid as errors:
340 343 errors.value.pop('password', None)
341 344 errors.value.pop('password_confirmation', None)
342 345 return self.register(
343 346 defaults=errors.value, errors=errors.error_dict)
344 347
345 348 except UserCreationError as e:
346 349 # container auth or other auth functions that create users on
347 350 # the fly can throw this exception signaling that there's issue
348 351 # with user creation, explanation should be provided in
349 352 # Exception itself
350 353 h.flash(e, category='error')
351 354 return self.register()
352 355
353 356 @view_config(
354 357 route_name='reset_password', request_method=('GET', 'POST'),
355 358 renderer='rhodecode:templates/password_reset.mako')
356 359 def password_reset(self):
357 360 c = self.load_default_context()
358 361 captcha = self._get_captcha_data()
359 362
360 363 template_context = {
361 364 'captcha_active': captcha.active,
362 365 'captcha_public_key': captcha.public_key,
363 366 'defaults': {},
364 367 'errors': {},
365 368 }
366 369
367 370 # always send implicit message to prevent from discovery of
368 371 # matching emails
369 372 msg = _('If such email exists, a password reset link was sent to it.')
370 373
371 374 def default_response():
372 375 log.debug('faking response on invalid password reset')
373 376 # make this take 2s, to prevent brute forcing.
374 377 time.sleep(2)
375 378 h.flash(msg, category='success')
376 379 return HTTPFound(self.request.route_path('reset_password'))
377 380
378 381 if self.request.POST:
379 382 if h.HasPermissionAny('hg.password_reset.disabled')():
380 383 _email = self.request.POST.get('email', '')
381 384 log.error('Failed attempt to reset password for `%s`.', _email)
382 385 h.flash(_('Password reset has been disabled.'), category='error')
383 386 return HTTPFound(self.request.route_path('reset_password'))
384 387
385 388 password_reset_form = PasswordResetForm(self.request.translate)()
386 389 description = u'Generated token for password reset from {}'.format(
387 390 datetime.datetime.now().isoformat())
388 391
389 392 try:
390 393 form_result = password_reset_form.to_python(
391 394 self.request.POST)
392 395 user_email = form_result['email']
393 396
394 397 if captcha.active:
395 398 captcha_status, captcha_message = self.validate_captcha(
396 399 captcha.private_key)
397 400
398 401 if not captcha_status:
399 402 _value = form_result
400 403 _msg = _('Bad captcha')
401 404 error_dict = {'recaptcha_field': captcha_message}
402 405 raise formencode.Invalid(
403 406 _msg, _value, None, error_dict=error_dict)
404 407
405 408 # Generate reset URL and send mail.
406 409 user = User.get_by_email(user_email)
407 410
408 411 # only allow rhodecode based users to reset their password
409 412 # external auth shouldn't allow password reset
410 413 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
411 414 log.warning('User %s with external type `%s` tried a password reset. '
412 415 'This try was rejected', user, user.extern_type)
413 416 return default_response()
414 417
415 418 # generate password reset token that expires in 10 minutes
416 419 reset_token = UserModel().add_auth_token(
417 420 user=user, lifetime_minutes=10,
418 421 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
419 422 description=description)
420 423 Session().commit()
421 424
422 425 log.debug('Successfully created password recovery token')
423 426 password_reset_url = self.request.route_url(
424 427 'reset_password_confirmation',
425 428 _query={'key': reset_token.api_key})
426 429 UserModel().reset_password_link(
427 430 form_result, password_reset_url)
428 431
429 432 action_data = {'email': user_email,
430 433 'user_agent': self.request.user_agent}
431 434 audit_logger.store_web(
432 435 'user.password.reset_request', action_data=action_data,
433 436 user=self._rhodecode_user, commit=True)
434 437
435 438 return default_response()
436 439
437 440 except formencode.Invalid as errors:
438 441 template_context.update({
439 442 'defaults': errors.value,
440 443 'errors': errors.error_dict,
441 444 })
442 445 if not self.request.POST.get('email'):
443 446 # case of empty email, we want to report that
444 447 return self._get_template_context(c, **template_context)
445 448
446 449 if 'recaptcha_field' in errors.error_dict:
447 450 # case of failed captcha
448 451 return self._get_template_context(c, **template_context)
449 452
450 453 return default_response()
451 454
452 455 return self._get_template_context(c, **template_context)
453 456
454 457 @view_config(route_name='reset_password_confirmation',
455 458 request_method='GET')
456 459 def password_reset_confirmation(self):
457 460 self.load_default_context()
458 461 if self.request.GET and self.request.GET.get('key'):
459 462 # make this take 2s, to prevent brute forcing.
460 463 time.sleep(2)
461 464
462 465 token = AuthTokenModel().get_auth_token(
463 466 self.request.GET.get('key'))
464 467
465 468 # verify token is the correct role
466 469 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
467 470 log.debug('Got token with role:%s expected is %s',
468 471 getattr(token, 'role', 'EMPTY_TOKEN'),
469 472 UserApiKeys.ROLE_PASSWORD_RESET)
470 473 h.flash(
471 474 _('Given reset token is invalid'), category='error')
472 475 return HTTPFound(self.request.route_path('reset_password'))
473 476
474 477 try:
475 478 owner = token.user
476 479 data = {'email': owner.email, 'token': token.api_key}
477 480 UserModel().reset_password(data)
478 481 h.flash(
479 482 _('Your password reset was successful, '
480 483 'a new password has been sent to your email'),
481 484 category='success')
482 485 except Exception as e:
483 486 log.error(e)
484 487 return HTTPFound(self.request.route_path('reset_password'))
485 488
486 489 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now