##// END OF EJS Templates
fix(tests): fixed 2fa tests and password reset broken by accident
super-admin -
r5369:cab08940 default
parent child Browse files
Show More
@@ -1,593 +1,593 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import urllib.parse
20 20
21 21 import mock
22 22 import pytest
23 23
24 24
25 25 from rhodecode.lib.auth import check_password
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.auth_token import AuthTokenModel
28 28 from rhodecode.model.db import User, Notification, UserApiKeys
29 29 from rhodecode.model.meta import Session
30 30
31 31 from rhodecode.tests import (
32 32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
33 33 no_newline_id_generator)
34 34 from rhodecode.tests.fixture import Fixture
35 35 from rhodecode.tests.routes import route_path
36 36
37 37 fixture = Fixture()
38 38
39 39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
40 40
41 41
42 42 @pytest.mark.usefixtures('app')
43 43 class TestLoginController(object):
44 44 destroy_users = set()
45 45
46 46 @classmethod
47 47 def teardown_class(cls):
48 48 fixture.destroy_users(cls.destroy_users)
49 49
50 50 def teardown_method(self, method):
51 51 for n in Notification.query().all():
52 52 Session().delete(n)
53 53
54 54 Session().commit()
55 55 assert Notification.query().all() == []
56 56
57 57 def test_index(self):
58 58 response = self.app.get(route_path('login'))
59 59 assert response.status == '200 OK'
60 60 # Test response...
61 61
62 62 def test_login_admin_ok(self):
63 63 response = self.app.post(route_path('login'),
64 64 {'username': 'test_admin',
65 65 'password': 'test12'}, status=302)
66 66 response = response.follow()
67 67 session = response.get_session_from_response()
68 68 username = session['rhodecode_user'].get('username')
69 69 assert username == 'test_admin'
70 70 response.mustcontain('logout')
71 71
72 72 def test_login_regular_ok(self):
73 73 response = self.app.post(route_path('login'),
74 74 {'username': 'test_regular',
75 75 'password': 'test12'}, status=302)
76 76
77 77 response = response.follow()
78 78 session = response.get_session_from_response()
79 79 username = session['rhodecode_user'].get('username')
80 80 assert username == 'test_regular'
81 81 response.mustcontain('logout')
82 82
83 83 def test_login_with_primary_email(self):
84 84 user_email = 'test_regular@mail.com'
85 85 response = self.app.post(route_path('login'),
86 86 {'username': user_email,
87 87 'password': 'test12'}, status=302)
88 88 response = response.follow()
89 89 session = response.get_session_from_response()
90 90 user = session['rhodecode_user']
91 91 assert user['username'] == user_email.split('@')[0]
92 92 assert user['is_authenticated']
93 93 response.mustcontain('logout')
94 94
95 95 def test_login_regular_forbidden_when_super_admin_restriction(self):
96 96 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
97 97 with fixture.auth_restriction(self.app._pyramid_registry,
98 98 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
99 99 response = self.app.post(route_path('login'),
100 100 {'username': 'test_regular',
101 101 'password': 'test12'})
102 102
103 103 response.mustcontain('invalid user name')
104 104 response.mustcontain('invalid password')
105 105
106 106 def test_login_regular_forbidden_when_scope_restriction(self):
107 107 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
108 108 with fixture.scope_restriction(self.app._pyramid_registry,
109 109 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
110 110 response = self.app.post(route_path('login'),
111 111 {'username': 'test_regular',
112 112 'password': 'test12'})
113 113
114 114 response.mustcontain('invalid user name')
115 115 response.mustcontain('invalid password')
116 116
117 117 def test_login_ok_came_from(self):
118 118 test_came_from = '/_admin/users?branch=stable'
119 119 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
120 120 response = self.app.post(
121 121 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
122 122
123 123 assert 'branch=stable' in response.location
124 124 response = response.follow()
125 125
126 126 assert response.status == '200 OK'
127 127 response.mustcontain('Users administration')
128 128
129 129 def test_redirect_to_login_with_get_args(self):
130 130 with fixture.anon_access(False):
131 131 kwargs = {'branch': 'stable'}
132 132 response = self.app.get(
133 133 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
134 134 status=302)
135 135
136 136 response_query = urllib.parse.parse_qsl(response.location)
137 137 assert 'branch=stable' in response_query[0][1]
138 138
139 139 def test_login_form_with_get_args(self):
140 140 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
141 141 response = self.app.get(_url)
142 142 assert 'branch%3Dstable' in response.form.action
143 143
144 144 @pytest.mark.parametrize("url_came_from", [
145 145 'data:text/html,<script>window.alert("xss")</script>',
146 146 'mailto:test@rhodecode.org',
147 147 'file:///etc/passwd',
148 148 'ftp://some.ftp.server',
149 149 'http://other.domain',
150 150 ], ids=no_newline_id_generator)
151 151 def test_login_bad_came_froms(self, url_came_from):
152 152 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
153 153 response = self.app.post(
154 154 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
155 155 assert response.status == '302 Found'
156 156 response = response.follow()
157 157 assert response.status == '200 OK'
158 158 assert response.request.path == '/'
159 159
160 160 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
161 161 @pytest.mark.parametrize("url_came_from", [
162 162 '/\r\nX-Forwarded-Host: \rhttp://example.org',
163 163 ], ids=no_newline_id_generator)
164 164 def test_login_bad_came_froms_404(self, url_came_from):
165 165 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
166 166 response = self.app.post(
167 167 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
168 168
169 169 response = response.follow()
170 170 assert response.status == '404 Not Found'
171 171
172 172 def test_login_short_password(self):
173 173 response = self.app.post(route_path('login'),
174 174 {'username': 'test_admin',
175 175 'password': 'as'})
176 176 assert response.status == '200 OK'
177 177
178 178 response.mustcontain('Enter 3 characters or more')
179 179
180 180 def test_login_wrong_non_ascii_password(self, user_regular):
181 181 response = self.app.post(
182 182 route_path('login'),
183 183 {'username': user_regular.username,
184 184 'password': 'invalid-non-asci\xe4'.encode('utf8')})
185 185
186 186 response.mustcontain('invalid user name')
187 187 response.mustcontain('invalid password')
188 188
189 189 def test_login_with_non_ascii_password(self, user_util):
190 190 password = u'valid-non-ascii\xe4'
191 191 user = user_util.create_user(password=password)
192 192 response = self.app.post(
193 193 route_path('login'),
194 194 {'username': user.username,
195 195 'password': password})
196 196 assert response.status_code == 302
197 197
198 198 def test_login_wrong_username_password(self):
199 199 response = self.app.post(route_path('login'),
200 200 {'username': 'error',
201 201 'password': 'test12'})
202 202
203 203 response.mustcontain('invalid user name')
204 204 response.mustcontain('invalid password')
205 205
206 206 def test_login_admin_ok_password_migration(self, real_crypto_backend):
207 207 from rhodecode.lib import auth
208 208
209 209 # create new user, with sha256 password
210 210 temp_user = 'test_admin_sha256'
211 211 user = fixture.create_user(temp_user)
212 212 user.password = auth._RhodeCodeCryptoSha256().hash_create(
213 213 b'test123')
214 214 Session().add(user)
215 215 Session().commit()
216 216 self.destroy_users.add(temp_user)
217 217 response = self.app.post(route_path('login'),
218 218 {'username': temp_user,
219 219 'password': 'test123'}, status=302)
220 220
221 221 response = response.follow()
222 222 session = response.get_session_from_response()
223 223 username = session['rhodecode_user'].get('username')
224 224 assert username == temp_user
225 225 response.mustcontain('logout')
226 226
227 227 # new password should be bcrypted, after log-in and transfer
228 228 user = User.get_by_username(temp_user)
229 229 assert user.password.startswith('$')
230 230
231 231 # REGISTRATIONS
232 232 def test_register(self):
233 233 response = self.app.get(route_path('register'))
234 234 response.mustcontain('Create an Account')
235 235
236 236 def test_register_err_same_username(self):
237 237 uname = 'test_admin'
238 238 response = self.app.post(
239 239 route_path('register'),
240 240 {
241 241 'username': uname,
242 242 'password': 'test12',
243 243 'password_confirmation': 'test12',
244 244 'email': 'goodmail@domain.com',
245 245 'firstname': 'test',
246 246 'lastname': 'test'
247 247 }
248 248 )
249 249
250 250 assertr = response.assert_response()
251 251 msg = 'Username "%(username)s" already exists'
252 252 msg = msg % {'username': uname}
253 253 assertr.element_contains('#username+.error-message', msg)
254 254
255 255 def test_register_err_same_email(self):
256 256 response = self.app.post(
257 257 route_path('register'),
258 258 {
259 259 'username': 'test_admin_0',
260 260 'password': 'test12',
261 261 'password_confirmation': 'test12',
262 262 'email': 'test_admin@mail.com',
263 263 'firstname': 'test',
264 264 'lastname': 'test'
265 265 }
266 266 )
267 267
268 268 assertr = response.assert_response()
269 269 msg = u'This e-mail address is already taken'
270 270 assertr.element_contains('#email+.error-message', msg)
271 271
272 272 def test_register_err_same_email_case_sensitive(self):
273 273 response = self.app.post(
274 274 route_path('register'),
275 275 {
276 276 'username': 'test_admin_1',
277 277 'password': 'test12',
278 278 'password_confirmation': 'test12',
279 279 'email': 'TesT_Admin@mail.COM',
280 280 'firstname': 'test',
281 281 'lastname': 'test'
282 282 }
283 283 )
284 284 assertr = response.assert_response()
285 285 msg = u'This e-mail address is already taken'
286 286 assertr.element_contains('#email+.error-message', msg)
287 287
288 288 def test_register_err_wrong_data(self):
289 289 response = self.app.post(
290 290 route_path('register'),
291 291 {
292 292 'username': 'xs',
293 293 'password': 'test',
294 294 'password_confirmation': 'test',
295 295 'email': 'goodmailm',
296 296 'firstname': 'test',
297 297 'lastname': 'test'
298 298 }
299 299 )
300 300 assert response.status == '200 OK'
301 301 response.mustcontain('An email address must contain a single @')
302 302 response.mustcontain('Enter a value 6 characters long or more')
303 303
304 304 def test_register_err_username(self):
305 305 response = self.app.post(
306 306 route_path('register'),
307 307 {
308 308 'username': 'error user',
309 309 'password': 'test12',
310 310 'password_confirmation': 'test12',
311 311 'email': 'goodmailm',
312 312 'firstname': 'test',
313 313 'lastname': 'test'
314 314 }
315 315 )
316 316
317 317 response.mustcontain('An email address must contain a single @')
318 318 response.mustcontain(
319 319 'Username may only contain '
320 320 'alphanumeric characters underscores, '
321 321 'periods or dashes and must begin with '
322 322 'alphanumeric character')
323 323
324 324 def test_register_err_case_sensitive(self):
325 325 usr = 'Test_Admin'
326 326 response = self.app.post(
327 327 route_path('register'),
328 328 {
329 329 'username': usr,
330 330 'password': 'test12',
331 331 'password_confirmation': 'test12',
332 332 'email': 'goodmailm',
333 333 'firstname': 'test',
334 334 'lastname': 'test'
335 335 }
336 336 )
337 337
338 338 assertr = response.assert_response()
339 339 msg = u'Username "%(username)s" already exists'
340 340 msg = msg % {'username': usr}
341 341 assertr.element_contains('#username+.error-message', msg)
342 342
343 343 def test_register_special_chars(self):
344 344 response = self.app.post(
345 345 route_path('register'),
346 346 {
347 347 'username': 'xxxaxn',
348 348 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
349 349 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
350 350 'email': 'goodmailm@test.plx',
351 351 'firstname': 'test',
352 352 'lastname': 'test'
353 353 }
354 354 )
355 355
356 356 msg = u'Invalid characters (non-ascii) in password'
357 357 response.mustcontain(msg)
358 358
359 359 def test_register_password_mismatch(self):
360 360 response = self.app.post(
361 361 route_path('register'),
362 362 {
363 363 'username': 'xs',
364 364 'password': '123qwe',
365 365 'password_confirmation': 'qwe123',
366 366 'email': 'goodmailm@test.plxa',
367 367 'firstname': 'test',
368 368 'lastname': 'test'
369 369 }
370 370 )
371 371 msg = u'Passwords do not match'
372 372 response.mustcontain(msg)
373 373
374 374 def test_register_ok(self):
375 375 username = 'test_regular4'
376 376 password = 'qweqwe'
377 377 email = 'marcin@test.com'
378 378 name = 'testname'
379 379 lastname = 'testlastname'
380 380
381 381 # this initializes a session
382 382 response = self.app.get(route_path('register'))
383 383 response.mustcontain('Create an Account')
384 384
385 385
386 386 response = self.app.post(
387 387 route_path('register'),
388 388 {
389 389 'username': username,
390 390 'password': password,
391 391 'password_confirmation': password,
392 392 'email': email,
393 393 'firstname': name,
394 394 'lastname': lastname,
395 395 'admin': True
396 396 },
397 397 status=302
398 398 ) # This should be overridden
399 399
400 400 assert_session_flash(
401 401 response, 'You have successfully registered with RhodeCode. You can log-in now.')
402 402
403 403 ret = Session().query(User).filter(
404 404 User.username == 'test_regular4').one()
405 405 assert ret.username == username
406 406 assert check_password(password, ret.password)
407 407 assert ret.email == email
408 408 assert ret.name == name
409 409 assert ret.lastname == lastname
410 410 assert ret.auth_tokens is not None
411 411 assert not ret.admin
412 412
413 413 def test_forgot_password_wrong_mail(self):
414 414 bad_email = 'marcin@wrongmail.org'
415 415 # this initializes a session
416 416 self.app.get(route_path('reset_password'))
417 417
418 418 response = self.app.post(
419 419 route_path('reset_password'), {'email': bad_email, }
420 420 )
421 421 assert_session_flash(response,
422 422 'If such email exists, a password reset link was sent to it.')
423 423
424 424 def test_forgot_password(self, user_util):
425 425 # this initializes a session
426 426 self.app.get(route_path('reset_password'))
427 427
428 428 user = user_util.create_user()
429 429 user_id = user.user_id
430 430 email = user.email
431 431
432 432 response = self.app.post(route_path('reset_password'), {'email': email, })
433 433
434 434 assert_session_flash(response,
435 435 'If such email exists, a password reset link was sent to it.')
436 436
437 437 # BAD KEY
438 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
438 confirm_url = route_path('reset_password_confirmation', params={'key': 'badkey'})
439 439 response = self.app.get(confirm_url, status=302)
440 440 assert response.location.endswith(route_path('reset_password'))
441 441 assert_session_flash(response, 'Given reset token is invalid')
442 442
443 443 response.follow() # cleanup flash
444 444
445 445 # GOOD KEY
446 446 key = UserApiKeys.query()\
447 447 .filter(UserApiKeys.user_id == user_id)\
448 448 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
449 449 .first()
450 450
451 451 assert key
452 452
453 453 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
454 454 response = self.app.get(confirm_url)
455 455 assert response.status == '302 Found'
456 456 assert response.location.endswith(route_path('login'))
457 457
458 458 assert_session_flash(
459 459 response,
460 460 'Your password reset was successful, '
461 461 'a new password has been sent to your email')
462 462
463 463 response.follow()
464 464
465 465 def _get_api_whitelist(self, values=None):
466 466 config = {'api_access_controllers_whitelist': values or []}
467 467 return config
468 468
469 469 @pytest.mark.parametrize("test_name, auth_token", [
470 470 ('none', None),
471 471 ('empty_string', ''),
472 472 ('fake_number', '123456'),
473 473 ('proper_auth_token', None)
474 474 ])
475 475 def test_access_not_whitelisted_page_via_auth_token(
476 476 self, test_name, auth_token, user_admin):
477 477
478 478 whitelist = self._get_api_whitelist([])
479 479 with mock.patch.dict('rhodecode.CONFIG', whitelist):
480 480 assert [] == whitelist['api_access_controllers_whitelist']
481 481 if test_name == 'proper_auth_token':
482 482 # use builtin if api_key is None
483 483 auth_token = user_admin.api_key
484 484
485 485 with fixture.anon_access(False):
486 486 # webtest uses linter to check if response is bytes,
487 487 # and we use memoryview here as a wrapper, quick turn-off
488 488 self.app.lint = False
489 489
490 490 self.app.get(
491 491 route_path('repo_commit_raw',
492 492 repo_name=HG_REPO, commit_id='tip',
493 493 params=dict(api_key=auth_token)),
494 494 status=302)
495 495
496 496 @pytest.mark.parametrize("test_name, auth_token, code", [
497 497 ('none', None, 302),
498 498 ('empty_string', '', 302),
499 499 ('fake_number', '123456', 302),
500 500 ('proper_auth_token', None, 200)
501 501 ])
502 502 def test_access_whitelisted_page_via_auth_token(
503 503 self, test_name, auth_token, code, user_admin):
504 504
505 505 whitelist = self._get_api_whitelist(whitelist_view)
506 506
507 507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
508 508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
509 509
510 510 if test_name == 'proper_auth_token':
511 511 auth_token = user_admin.api_key
512 512 assert auth_token
513 513
514 514 with fixture.anon_access(False):
515 515 # webtest uses linter to check if response is bytes,
516 516 # and we use memoryview here as a wrapper, quick turn-off
517 517 self.app.lint = False
518 518 self.app.get(
519 519 route_path('repo_commit_raw',
520 520 repo_name=HG_REPO, commit_id='tip',
521 521 params=dict(api_key=auth_token)),
522 522 status=code)
523 523
524 524 @pytest.mark.parametrize("test_name, auth_token, code", [
525 525 ('proper_auth_token', None, 200),
526 526 ('wrong_auth_token', '123456', 302),
527 527 ])
528 528 def test_access_whitelisted_page_via_auth_token_bound_to_token(
529 529 self, test_name, auth_token, code, user_admin):
530 530
531 531 expected_token = auth_token
532 532 if test_name == 'proper_auth_token':
533 533 auth_token = user_admin.api_key
534 534 expected_token = auth_token
535 535 assert auth_token
536 536
537 537 whitelist = self._get_api_whitelist([
538 538 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
539 539
540 540 with mock.patch.dict('rhodecode.CONFIG', whitelist):
541 541
542 542 with fixture.anon_access(False):
543 543 # webtest uses linter to check if response is bytes,
544 544 # and we use memoryview here as a wrapper, quick turn-off
545 545 self.app.lint = False
546 546
547 547 self.app.get(
548 548 route_path('repo_commit_raw',
549 549 repo_name=HG_REPO, commit_id='tip',
550 550 params=dict(api_key=auth_token)),
551 551 status=code)
552 552
553 553 def test_access_page_via_extra_auth_token(self):
554 554 whitelist = self._get_api_whitelist(whitelist_view)
555 555 with mock.patch.dict('rhodecode.CONFIG', whitelist):
556 556 assert whitelist_view == \
557 557 whitelist['api_access_controllers_whitelist']
558 558
559 559 new_auth_token = AuthTokenModel().create(
560 560 TEST_USER_ADMIN_LOGIN, 'test')
561 561 Session().commit()
562 562 with fixture.anon_access(False):
563 563 # webtest uses linter to check if response is bytes,
564 564 # and we use memoryview here as a wrapper, quick turn-off
565 565 self.app.lint = False
566 566 self.app.get(
567 567 route_path('repo_commit_raw',
568 568 repo_name=HG_REPO, commit_id='tip',
569 569 params=dict(api_key=new_auth_token.api_key)),
570 570 status=200)
571 571
572 572 def test_access_page_via_expired_auth_token(self):
573 573 whitelist = self._get_api_whitelist(whitelist_view)
574 574 with mock.patch.dict('rhodecode.CONFIG', whitelist):
575 575 assert whitelist_view == \
576 576 whitelist['api_access_controllers_whitelist']
577 577
578 578 new_auth_token = AuthTokenModel().create(
579 579 TEST_USER_ADMIN_LOGIN, 'test')
580 580 Session().commit()
581 581 # patch the api key and make it expired
582 582 new_auth_token.expires = 0
583 583 Session().add(new_auth_token)
584 584 Session().commit()
585 585 with fixture.anon_access(False):
586 586 # webtest uses linter to check if response is bytes,
587 587 # and we use memoryview here as a wrapper, quick turn-off
588 588 self.app.lint = False
589 589 self.app.get(
590 590 route_path('repo_commit_raw',
591 591 repo_name=HG_REPO, commit_id='tip',
592 592 params=dict(api_key=new_auth_token.api_key)),
593 593 status=302)
@@ -1,554 +1,552 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import time
20 20 import json
21 21 import pyotp
22 22 import qrcode
23 23 import collections
24 24 import datetime
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import logging
28 28 import urllib.parse
29 29 import requests
30 30 from io import BytesIO
31 31 from base64 import b64encode
32 32
33 33 from pyramid.renderers import render
34 34 from pyramid.response import Response
35 35 from pyramid.httpexceptions import HTTPFound
36 36
37 37
38 38 from rhodecode.apps._base import BaseAppView
39 39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
40 40 from rhodecode.authentication.plugins import auth_rhodecode
41 41 from rhodecode.events import UserRegistered, trigger
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import audit_logger
44 44 from rhodecode.lib.auth import (
45 45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
46 46 from rhodecode.lib.base import get_ip_addr
47 47 from rhodecode.lib.exceptions import UserCreationError
48 48 from rhodecode.lib.utils2 import safe_str
49 49 from rhodecode.model.db import User, UserApiKeys
50 50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.auth_token import AuthTokenModel
53 53 from rhodecode.model.settings import SettingsModel
54 54 from rhodecode.model.user import UserModel
55 55 from rhodecode.translation import _
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60 CaptchaData = collections.namedtuple(
61 61 'CaptchaData', 'active, private_key, public_key')
62 62
63 63
64 64 def store_user_in_session(session, user_identifier, remember=False):
65 65 user = User.get_by_username_or_primary_email(user_identifier)
66 66 auth_user = AuthUser(user.user_id)
67 67 auth_user.set_authenticated()
68 68 cs = auth_user.get_cookie_store()
69 69 session['rhodecode_user'] = cs
70 70 user.update_lastlogin()
71 71 Session().commit()
72 72
73 73 # If they want to be remembered, update the cookie
74 74 if remember:
75 75 _year = (datetime.datetime.now() +
76 76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
77 77 session._set_cookie_expires(_year)
78 78
79 79 session.save()
80 80
81 81 safe_cs = cs.copy()
82 82 safe_cs['password'] = '****'
83 83 log.info('user %s is now authenticated and stored in '
84 84 'session, session attrs %s', user_identifier, safe_cs)
85 85
86 86 # dumps session attrs back to cookie
87 87 session._update_cookie_out()
88 88 # we set new cookie
89 89 headers = None
90 90 if session.request['set_cookie']:
91 91 # send set-cookie headers back to response to update cookie
92 92 headers = [('Set-Cookie', session.request['cookie_out'])]
93 93 return headers
94 94
95 95
96 96 def get_came_from(request):
97 97 came_from = safe_str(request.GET.get('came_from', ''))
98 98 parsed = urllib.parse.urlparse(came_from)
99 99
100 100 allowed_schemes = ['http', 'https']
101 101 default_came_from = h.route_path('home')
102 102 if parsed.scheme and parsed.scheme not in allowed_schemes:
103 103 log.error('Suspicious URL scheme detected %s for url %s',
104 104 parsed.scheme, parsed)
105 105 came_from = default_came_from
106 106 elif parsed.netloc and request.host != parsed.netloc:
107 107 log.error('Suspicious NETLOC detected %s for url %s server url '
108 108 'is: %s', parsed.netloc, parsed, request.host)
109 109 came_from = default_came_from
110 110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
111 111 log.error('Header injection detected `%s` for url %s server url ',
112 112 parsed.path, parsed)
113 113 came_from = default_came_from
114 114
115 115 return came_from or default_came_from
116 116
117 117
118 118 class LoginView(BaseAppView):
119 119
120 120 def load_default_context(self):
121 121 c = self._get_local_tmpl_context()
122 122 c.came_from = get_came_from(self.request)
123 123 return c
124 124
125 125 def _get_captcha_data(self):
126 126 settings = SettingsModel().get_all_settings()
127 127 private_key = settings.get('rhodecode_captcha_private_key')
128 128 public_key = settings.get('rhodecode_captcha_public_key')
129 129 active = bool(private_key)
130 130 return CaptchaData(
131 131 active=active, private_key=private_key, public_key=public_key)
132 132
133 133 def validate_captcha(self, private_key):
134 134
135 135 captcha_rs = self.request.POST.get('g-recaptcha-response')
136 136 url = "https://www.google.com/recaptcha/api/siteverify"
137 137 params = {
138 138 'secret': private_key,
139 139 'response': captcha_rs,
140 140 'remoteip': get_ip_addr(self.request.environ)
141 141 }
142 142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
143 143 verify_rs = verify_rs.json()
144 144 captcha_status = verify_rs.get('success', False)
145 145 captcha_errors = verify_rs.get('error-codes', [])
146 146 if not isinstance(captcha_errors, list):
147 147 captcha_errors = [captcha_errors]
148 148 captcha_errors = ', '.join(captcha_errors)
149 149 captcha_message = ''
150 150 if captcha_status is False:
151 151 captcha_message = "Bad captcha. Errors: {}".format(
152 152 captcha_errors)
153 153
154 154 return captcha_status, captcha_message
155 155
156 156 def login(self):
157 157 c = self.load_default_context()
158 158 auth_user = self._rhodecode_user
159 159
160 160 # redirect if already logged in
161 161 if (auth_user.is_authenticated and
162 162 not auth_user.is_default and auth_user.ip_allowed):
163 163 raise HTTPFound(c.came_from)
164 164
165 165 # check if we use headers plugin, and try to login using it.
166 166 try:
167 167 log.debug('Running PRE-AUTH for headers based authentication')
168 168 auth_info = authenticate(
169 169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
170 170 if auth_info:
171 171 headers = store_user_in_session(
172 172 self.session, auth_info.get('username'))
173 173 raise HTTPFound(c.came_from, headers=headers)
174 174 except UserCreationError as e:
175 175 log.error(e)
176 176 h.flash(e, category='error')
177 177
178 178 return self._get_template_context(c)
179 179
180 180 def login_post(self):
181 181 c = self.load_default_context()
182 182
183 183 login_form = LoginForm(self.request.translate)()
184 184
185 185 try:
186 186 self.session.invalidate()
187 187 form_result = login_form.to_python(self.request.POST)
188 188 # form checks for username/password, now we're authenticated
189 189 username = form_result['username']
190 190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
191 191 user.has_check_2fa_flag = True
192 192
193 193 headers = store_user_in_session(
194 194 self.session,
195 195 user_identifier=username,
196 196 remember=form_result['remember'])
197 197 log.debug('Redirecting to "%s" after login.', c.came_from)
198 198
199 199 audit_user = audit_logger.UserWrap(
200 200 username=self.request.POST.get('username'),
201 201 ip_addr=self.request.remote_addr)
202 202 action_data = {'user_agent': self.request.user_agent}
203 203 audit_logger.store_web(
204 204 'user.login.success', action_data=action_data,
205 205 user=audit_user, commit=True)
206 206
207 207 raise HTTPFound(c.came_from, headers=headers)
208 208 except formencode.Invalid as errors:
209 209 defaults = errors.value
210 210 # remove password from filling in form again
211 211 defaults.pop('password', None)
212 212 render_ctx = {
213 213 'errors': errors.error_dict,
214 214 'defaults': defaults,
215 215 }
216 216
217 217 audit_user = audit_logger.UserWrap(
218 218 username=self.request.POST.get('username'),
219 219 ip_addr=self.request.remote_addr)
220 220 action_data = {'user_agent': self.request.user_agent}
221 221 audit_logger.store_web(
222 222 'user.login.failure', action_data=action_data,
223 223 user=audit_user, commit=True)
224 224 return self._get_template_context(c, **render_ctx)
225 225
226 226 except UserCreationError as e:
227 227 # headers auth or other auth functions that create users on
228 228 # the fly can throw this exception signaling that there's issue
229 229 # with user creation, explanation should be provided in
230 230 # Exception itself
231 231 h.flash(e, category='error')
232 232 return self._get_template_context(c)
233 233
234 234 @CSRFRequired()
235 235 def logout(self):
236 236 auth_user = self._rhodecode_user
237 237 log.info('Deleting session for user: `%s`', auth_user)
238 238
239 239 action_data = {'user_agent': self.request.user_agent}
240 240 audit_logger.store_web(
241 241 'user.logout', action_data=action_data,
242 242 user=auth_user, commit=True)
243 243 self.session.delete()
244 244 return HTTPFound(h.route_path('home'))
245 245
246 246 @HasPermissionAnyDecorator(
247 247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 248 def register(self, defaults=None, errors=None):
249 249 c = self.load_default_context()
250 250 defaults = defaults or {}
251 251 errors = errors or {}
252 252
253 253 settings = SettingsModel().get_all_settings()
254 254 register_message = settings.get('rhodecode_register_message') or ''
255 255 captcha = self._get_captcha_data()
256 256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 257 .AuthUser().permissions['global']
258 258
259 259 render_ctx = self._get_template_context(c)
260 260 render_ctx.update({
261 261 'defaults': defaults,
262 262 'errors': errors,
263 263 'auto_active': auto_active,
264 264 'captcha_active': captcha.active,
265 265 'captcha_public_key': captcha.public_key,
266 266 'register_message': register_message,
267 267 })
268 268 return render_ctx
269 269
270 270 @HasPermissionAnyDecorator(
271 271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 272 def register_post(self):
273 273 from rhodecode.authentication.plugins import auth_rhodecode
274 274
275 275 self.load_default_context()
276 276 captcha = self._get_captcha_data()
277 277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
278 278 .AuthUser().permissions['global']
279 279
280 280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
282 282
283 283 register_form = RegisterForm(self.request.translate)()
284 284 try:
285 285
286 286 form_result = register_form.to_python(self.request.POST)
287 287 form_result['active'] = auto_active
288 288 external_identity = self.request.POST.get('external_identity')
289 289
290 290 if external_identity:
291 291 extern_name = external_identity
292 292 extern_type = external_identity
293 293
294 294 if captcha.active:
295 295 captcha_status, captcha_message = self.validate_captcha(
296 296 captcha.private_key)
297 297
298 298 if not captcha_status:
299 299 _value = form_result
300 300 _msg = _('Bad captcha')
301 301 error_dict = {'recaptcha_field': captcha_message}
302 302 raise formencode.Invalid(
303 303 _msg, _value, None, error_dict=error_dict)
304 304
305 305 new_user = UserModel().create_registration(
306 306 form_result, extern_name=extern_name, extern_type=extern_type)
307 307
308 308 action_data = {'data': new_user.get_api_data(),
309 309 'user_agent': self.request.user_agent}
310 310
311 311 if external_identity:
312 312 action_data['external_identity'] = external_identity
313 313
314 314 audit_user = audit_logger.UserWrap(
315 315 username=new_user.username,
316 316 user_id=new_user.user_id,
317 317 ip_addr=self.request.remote_addr)
318 318
319 319 audit_logger.store_web(
320 320 'user.register', action_data=action_data,
321 321 user=audit_user)
322 322
323 323 event = UserRegistered(user=new_user, session=self.session)
324 324 trigger(event)
325 325 h.flash(
326 326 _('You have successfully registered with RhodeCode. You can log-in now.'),
327 327 category='success')
328 328 if external_identity:
329 329 h.flash(
330 330 _('Please use the {identity} button to log-in').format(
331 331 identity=external_identity),
332 332 category='success')
333 333 Session().commit()
334 334
335 335 redirect_ro = self.request.route_path('login')
336 336 raise HTTPFound(redirect_ro)
337 337
338 338 except formencode.Invalid as errors:
339 339 errors.value.pop('password', None)
340 340 errors.value.pop('password_confirmation', None)
341 341 return self.register(
342 342 defaults=errors.value, errors=errors.error_dict)
343 343
344 344 except UserCreationError as e:
345 345 # container auth or other auth functions that create users on
346 346 # the fly can throw this exception signaling that there's issue
347 347 # with user creation, explanation should be provided in
348 348 # Exception itself
349 349 h.flash(e, category='error')
350 350 return self.register()
351 351
352 352 def password_reset(self):
353 353 c = self.load_default_context()
354 354 captcha = self._get_captcha_data()
355 355
356 356 template_context = {
357 357 'captcha_active': captcha.active,
358 358 'captcha_public_key': captcha.public_key,
359 359 'defaults': {},
360 360 'errors': {},
361 361 }
362 362
363 363 # always send implicit message to prevent from discovery of
364 364 # matching emails
365 365 msg = _('If such email exists, a password reset link was sent to it.')
366 366
367 367 def default_response():
368 368 log.debug('faking response on invalid password reset')
369 369 # make this take 2s, to prevent brute forcing.
370 370 time.sleep(2)
371 371 h.flash(msg, category='success')
372 372 return HTTPFound(self.request.route_path('reset_password'))
373 373
374 374 if self.request.POST:
375 375 if h.HasPermissionAny('hg.password_reset.disabled')():
376 376 _email = self.request.POST.get('email', '')
377 377 log.error('Failed attempt to reset password for `%s`.', _email)
378 378 h.flash(_('Password reset has been disabled.'), category='error')
379 379 return HTTPFound(self.request.route_path('reset_password'))
380 380
381 381 password_reset_form = PasswordResetForm(self.request.translate)()
382 382 description = 'Generated token for password reset from {}'.format(
383 383 datetime.datetime.now().isoformat())
384 384
385 385 try:
386 386 form_result = password_reset_form.to_python(
387 387 self.request.POST)
388 388 user_email = form_result['email']
389 389
390 390 if captcha.active:
391 391 captcha_status, captcha_message = self.validate_captcha(
392 392 captcha.private_key)
393 393
394 394 if not captcha_status:
395 395 _value = form_result
396 396 _msg = _('Bad captcha')
397 397 error_dict = {'recaptcha_field': captcha_message}
398 398 raise formencode.Invalid(
399 399 _msg, _value, None, error_dict=error_dict)
400 400
401 401 # Generate reset URL and send mail.
402 402 user = User.get_by_email(user_email)
403 403
404 404 # only allow rhodecode based users to reset their password
405 405 # external auth shouldn't allow password reset
406 406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
407 407 log.warning('User %s with external type `%s` tried a password reset. '
408 408 'This try was rejected', user, user.extern_type)
409 409 return default_response()
410 410
411 411 # generate password reset token that expires in 10 minutes
412 412 reset_token = UserModel().add_auth_token(
413 413 user=user, lifetime_minutes=10,
414 414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
415 415 description=description)
416 416 Session().commit()
417 417
418 418 log.debug('Successfully created password recovery token')
419 419 password_reset_url = self.request.route_url(
420 420 'reset_password_confirmation',
421 421 _query={'key': reset_token.api_key})
422 422 UserModel().reset_password_link(
423 423 form_result, password_reset_url)
424 424
425 425 action_data = {'email': user_email,
426 426 'user_agent': self.request.user_agent}
427 427 audit_logger.store_web(
428 428 'user.password.reset_request', action_data=action_data,
429 429 user=self._rhodecode_user, commit=True)
430 430
431 431 return default_response()
432 432
433 433 except formencode.Invalid as errors:
434 434 template_context.update({
435 435 'defaults': errors.value,
436 436 'errors': errors.error_dict,
437 437 })
438 438 if not self.request.POST.get('email'):
439 439 # case of empty email, we want to report that
440 440 return self._get_template_context(c, **template_context)
441 441
442 442 if 'recaptcha_field' in errors.error_dict:
443 443 # case of failed captcha
444 444 return self._get_template_context(c, **template_context)
445 445
446 446 return default_response()
447 447
448 448 return self._get_template_context(c, **template_context)
449 449
450 @LoginRequired()
451 @NotAnonymous()
452 450 def password_reset_confirmation(self):
453 451 self.load_default_context()
454 if self.request.GET and self.request.GET.get('key'):
452
453 if key := self.request.GET.get('key'):
455 454 # make this take 2s, to prevent brute forcing.
456 455 time.sleep(2)
457 456
458 token = AuthTokenModel().get_auth_token(
459 self.request.GET.get('key'))
457 token = AuthTokenModel().get_auth_token(key)
460 458
461 459 # verify token is the correct role
462 460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
463 461 log.debug('Got token with role:%s expected is %s',
464 462 getattr(token, 'role', 'EMPTY_TOKEN'),
465 463 UserApiKeys.ROLE_PASSWORD_RESET)
466 464 h.flash(
467 465 _('Given reset token is invalid'), category='error')
468 466 return HTTPFound(self.request.route_path('reset_password'))
469 467
470 468 try:
471 469 owner = token.user
472 470 data = {'email': owner.email, 'token': token.api_key}
473 471 UserModel().reset_password(data)
474 472 h.flash(
475 473 _('Your password reset was successful, '
476 474 'a new password has been sent to your email'),
477 475 category='success')
478 476 except Exception as e:
479 477 log.error(e)
480 478 return HTTPFound(self.request.route_path('reset_password'))
481 479
482 480 return HTTPFound(self.request.route_path('login'))
483 481
484 482 @LoginRequired()
485 483 @NotAnonymous()
486 484 def setup_2fa(self):
487 485 _ = self.request.translate
488 486 c = self.load_default_context()
489 487 user_instance = self._rhodecode_db_user
490 488 form = TOTPForm(_, user_instance)()
491 489 render_ctx = {}
492 490 if self.request.method == 'POST':
493 491 post_items = dict(self.request.POST)
494 492
495 493 try:
496 494 form_details = form.to_python(post_items)
497 495 secret = form_details['secret_totp']
498 496
499 497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
500 498 user_instance.set_2fa_secret(secret)
501 499
502 500 Session().commit()
503 501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
504 502 except formencode.Invalid as errors:
505 503 defaults = errors.value
506 504 render_ctx = {
507 505 'errors': errors.error_dict,
508 506 'defaults': defaults,
509 507 }
510 508
511 509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
512 510 # only then we should persist it
513 511 secret = user_instance.init_secret_2fa(persist=False)
514 512
515 513 totp_name = f'RhodeCode token ({self.request.user.username})'
516 514
517 515 qr = qrcode.QRCode(version=1, box_size=10, border=5)
518 516 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
519 517 qr.make(fit=True)
520 518 img = qr.make_image(fill_color='black', back_color='white')
521 519 buffered = BytesIO()
522 520 img.save(buffered)
523 521 return self._get_template_context(
524 522 c,
525 523 qr=b64encode(buffered.getvalue()).decode("utf-8"),
526 524 key=secret,
527 525 totp_name=totp_name,
528 526 ** render_ctx
529 527 )
530 528
531 529 @LoginRequired()
532 530 @NotAnonymous()
533 531 def verify_2fa(self):
534 532 _ = self.request.translate
535 533 c = self.load_default_context()
536 534 render_ctx = {}
537 535 user_instance = self._rhodecode_db_user
538 536 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
539 537 if self.request.method == 'POST':
540 538 post_items = dict(self.request.POST)
541 539 # NOTE: inject secret, as it's a post configured saved item.
542 540 post_items['secret_totp'] = user_instance.get_secret_2fa()
543 541 try:
544 542 totp_form.to_python(post_items)
545 543 user_instance.has_check_2fa_flag = False
546 544 Session().commit()
547 545 raise HTTPFound(c.came_from)
548 546 except formencode.Invalid as errors:
549 547 defaults = errors.value
550 548 render_ctx = {
551 549 'errors': errors.error_dict,
552 550 'defaults': defaults,
553 551 }
554 552 return self._get_template_context(c, **render_ctx)
@@ -1,321 +1,322 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 def get_url_defs():
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22
23 23 return {
24 24 "home": "/",
25 25 "main_page_repos_data": "/_home_repos",
26 26 "main_page_repo_groups_data": "/_home_repo_groups",
27 27 "repo_group_home": "/{repo_group_name}",
28 28 "user_autocomplete_data": "/_users",
29 29 "user_group_autocomplete_data": "/_user_groups",
30 30 "repo_list_data": "/_repos",
31 31 "goto_switcher_data": "/_goto_data",
32 32 "admin_home": ADMIN_PREFIX + "",
33 33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
34 34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
35 35 "admin_defaults_repositories_update": ADMIN_PREFIX
36 36 + "/defaults/repositories/update",
37 37 "search": ADMIN_PREFIX + "/search",
38 38 "search_repo": "/{repo_name}/search",
39 39 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
40 40 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
41 41 "my_account_auth_tokens_delete": ADMIN_PREFIX
42 42 + "/my_account/auth_tokens/delete",
43 43 "repos": ADMIN_PREFIX + "/repos",
44 44 "repos_data": ADMIN_PREFIX + "/repos_data",
45 45 "repo_groups": ADMIN_PREFIX + "/repo_groups",
46 46 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
47 47 "user_groups": ADMIN_PREFIX + "/user_groups",
48 48 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
49 49 "user_profile": "/_profiles/{username}",
50 50 "profile_user_group": "/_profile_user_group/{user_group_name}",
51 51 "repo_summary": "/{repo_name}",
52 52 "repo_creating_check": "/{repo_name}/repo_creating_check",
53 53 "edit_repo": "/{repo_name}/settings",
54 54 "edit_repo_vcs": "/{repo_name}/settings/vcs",
55 55 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
56 56 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
57 57 "repo_archivefile": "/{repo_name}/archive/{fname}",
58 58 "repo_files_diff": "/{repo_name}/diff/{f_path}",
59 59 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
60 60 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
61 61 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
62 62 "repo_files:default_commit": "/{repo_name}/files",
63 63 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
64 64 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
65 65 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
66 66 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
67 67 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
68 68 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
69 69 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
70 70 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
71 71 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
72 72 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
73 73 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
74 74 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
75 75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
76 76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
77 77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
78 78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
79 79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
80 80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
81 81 "journal": ADMIN_PREFIX + "/journal",
82 82 "journal_rss": ADMIN_PREFIX + "/journal/rss",
83 83 "journal_atom": ADMIN_PREFIX + "/journal/atom",
84 84 "journal_public": ADMIN_PREFIX + "/public_journal",
85 85 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
86 86 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
87 87 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
88 88 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
89 89 "toggle_following": ADMIN_PREFIX + "/toggle_following",
90 90 "upload_file": "/_file_store/upload",
91 91 "download_file": "/_file_store/download/{fid}",
92 92 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
93 93 "gists_show": ADMIN_PREFIX + "/gists",
94 94 "gists_new": ADMIN_PREFIX + "/gists/new",
95 95 "gists_create": ADMIN_PREFIX + "/gists/create",
96 96 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
97 97 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
98 98 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
99 99 "gist_edit_check_revision": ADMIN_PREFIX
100 100 + "/gists/{gist_id}/edit/check_revision",
101 101 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
102 102 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
103 103 "gist_show_formatted": ADMIN_PREFIX
104 104 + "/gists/{gist_id}/rev/{revision}/{format}",
105 105 "gist_show_formatted_path": ADMIN_PREFIX
106 106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
107 107 "login": ADMIN_PREFIX + "/login",
108 108 "logout": ADMIN_PREFIX + "/logout",
109 "setup_2fa": ADMIN_PREFIX + "/setup_2fa",
109 110 "check_2fa": ADMIN_PREFIX + "/check_2fa",
110 111 "register": ADMIN_PREFIX + "/register",
111 112 "reset_password": ADMIN_PREFIX + "/password_reset",
112 113 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
113 114 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
114 115 "admin_permissions_application_update": ADMIN_PREFIX
115 116 + "/permissions/application/update",
116 117 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
117 118 "user_group_members_data": ADMIN_PREFIX
118 119 + "/user_groups/{user_group_id}/members",
119 120 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
120 121 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
121 122 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
122 123 "edit_user_group_advanced_sync": ADMIN_PREFIX
123 124 + "/user_groups/{user_group_id}/edit/advanced/sync",
124 125 "edit_user_group_global_perms_update": ADMIN_PREFIX
125 126 + "/user_groups/{user_group_id}/edit/global_permissions/update",
126 127 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
127 128 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
128 129 "edit_user_group_perms": ADMIN_PREFIX
129 130 + "/user_groups/{user_group_id}/edit/permissions",
130 131 "edit_user_group_perms_update": ADMIN_PREFIX
131 132 + "/user_groups/{user_group_id}/edit/permissions/update",
132 133 "edit_repo_group": "/{repo_group_name}/_edit",
133 134 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
134 135 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
135 136 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
136 137 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
137 138 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
138 139 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX
139 140 + "/users/{user_id}/edit/ssh_keys/generate",
140 141 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
141 142 "edit_user_ssh_keys_delete": ADMIN_PREFIX
142 143 + "/users/{user_id}/edit/ssh_keys/delete",
143 144 "users": ADMIN_PREFIX + "/users",
144 145 "users_data": ADMIN_PREFIX + "/users_data",
145 146 "users_create": ADMIN_PREFIX + "/users/create",
146 147 "users_new": ADMIN_PREFIX + "/users/new",
147 148 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
148 149 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
149 150 "user_edit_global_perms": ADMIN_PREFIX
150 151 + "/users/{user_id}/edit/global_permissions",
151 152 "user_edit_global_perms_update": ADMIN_PREFIX
152 153 + "/users/{user_id}/edit/global_permissions/update",
153 154 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
154 155 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
155 156 "user_create_personal_repo_group": ADMIN_PREFIX
156 157 + "/users/{user_id}/create_repo_group",
157 158 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
158 159 "edit_user_auth_tokens_add": ADMIN_PREFIX
159 160 + "/users/{user_id}/edit/auth_tokens/new",
160 161 "edit_user_auth_tokens_delete": ADMIN_PREFIX
161 162 + "/users/{user_id}/edit/auth_tokens/delete",
162 163 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
163 164 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
164 165 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
165 166 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
166 167 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
167 168 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
168 169 "edit_user_perms_summary": ADMIN_PREFIX
169 170 + "/users/{user_id}/edit/permissions_summary",
170 171 "edit_user_perms_summary_json": ADMIN_PREFIX
171 172 + "/users/{user_id}/edit/permissions_summary/json",
172 173 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
173 174 "edit_user_audit_logs_download": ADMIN_PREFIX
174 175 + "/users/{user_id}/edit/audit/download",
175 176 "admin_settings": ADMIN_PREFIX + "/settings",
176 177 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
177 178 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
178 179 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
179 180 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
180 181 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
181 182 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX
182 183 + "/settings/vcs/svn_pattern_delete",
183 184 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
184 185 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
185 186 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
186 187 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
187 188 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
188 189 "admin_settings_issuetracker_update": ADMIN_PREFIX
189 190 + "/settings/issue-tracker/update",
190 191 "admin_settings_issuetracker_test": ADMIN_PREFIX
191 192 + "/settings/issue-tracker/test",
192 193 "admin_settings_issuetracker_delete": ADMIN_PREFIX
193 194 + "/settings/issue-tracker/delete",
194 195 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
195 196 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
196 197 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
197 198 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
198 199 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
199 200 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
200 201 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
201 202 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
202 203 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
203 204 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
204 205 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
205 206 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
206 207 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
207 208 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
208 209 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
209 210 "repo_new": ADMIN_PREFIX + "/repos/new",
210 211 "repo_create": ADMIN_PREFIX + "/repos/create",
211 212 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
212 213 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
213 214 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
214 215 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
215 216 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
216 217 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
217 218 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
218 219 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
219 220 "admin_permissions_ssh_keys_update": ADMIN_PREFIX
220 221 + "/permissions/ssh_keys/update",
221 222 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
222 223 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
223 224 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
224 225 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
225 226 "notifications_show_all": ADMIN_PREFIX + "/notifications",
226 227 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
227 228 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
228 229 "notifications_update": ADMIN_PREFIX
229 230 + "/notifications/{notification_id}/update",
230 231 "notifications_delete": ADMIN_PREFIX
231 232 + "/notifications/{notification_id}/delete",
232 233 "my_account": ADMIN_PREFIX + "/my_account/profile",
233 234 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
234 235 "my_account_update": ADMIN_PREFIX + "/my_account/update",
235 236 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
236 237 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
237 238 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
238 239 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
239 240 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
240 241 "my_account_password": ADMIN_PREFIX + "/my_account/password",
241 242 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
242 243 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
243 244 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
244 245 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
245 246 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
246 247 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
247 248 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
248 249 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
249 250 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
250 251 "pullrequest_show_all": "/{repo_name}/pull-request",
251 252 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
252 253 "bookmarks_home": "/{repo_name}/bookmarks",
253 254 "branches_home": "/{repo_name}/branches",
254 255 "tags_home": "/{repo_name}/tags",
255 256 "repo_changelog": "/{repo_name}/changelog",
256 257 "repo_commits": "/{repo_name}/commits",
257 258 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
258 259 "repo_commits_elements": "/{repo_name}/commits_elements",
259 260 "repo_commit": "/{repo_name}/changeset/{commit_id}",
260 261 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
261 262 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
262 263 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
263 264 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
264 265 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
265 266 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
266 267 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
267 268 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
268 269 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
269 270 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
270 271 "repo_compare_select": "/{repo_name}/compare",
271 272 "rss_feed_home": "/{repo_name}/feed-rss",
272 273 "atom_feed_home": "/{repo_name}/feed-atom",
273 274 "rss_feed_home_old": "/{repo_name}/feed/rss",
274 275 "atom_feed_home_old": "/{repo_name}/feed/atom",
275 276 "repo_fork_new": "/{repo_name}/fork",
276 277 "repo_fork_create": "/{repo_name}/fork/create",
277 278 "repo_forks_show_all": "/{repo_name}/forks",
278 279 "repo_forks_data": "/{repo_name}/forks/data",
279 280 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
280 281 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
281 282 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
282 283 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
283 284 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
284 285 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
285 286 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
286 287 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
287 288 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
288 289 "pullrequest_new": "/{repo_name}/pull-request/new",
289 290 "pullrequest_create": "/{repo_name}/pull-request/create",
290 291 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
291 292 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
292 293 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
293 294 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
294 295 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
295 296 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
296 297 "edit_repo_caches": "/{repo_name}/settings/caches",
297 298 "edit_repo_perms": "/{repo_name}/settings/permissions",
298 299 "edit_repo_fields": "/{repo_name}/settings/fields",
299 300 "edit_repo_remote": "/{repo_name}/settings/remote",
300 301 "edit_repo_statistics": "/{repo_name}/settings/statistics",
301 302 "edit_repo_advanced": "/{repo_name}/settings/advanced",
302 303 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
303 304 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
304 305 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
305 306 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
306 307 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
307 308 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
308 309 "repo_refs_data": "/{repo_name}/refs-data",
309 310 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog",
310 311 "repo_artifacts_stream_store": "/_file_store/stream-upload",
311 312 }
312 313
313 314
314 315 def route_path(name, params=None, **kwargs):
315 316 import urllib.parse
316 317
317 318 base_url = get_url_defs()[name].format(**kwargs)
318 319
319 320 if params:
320 321 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
321 322 return base_url
General Comments 0
You need to be logged in to leave comments. Login now