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