##// END OF EJS Templates
Authentication: cache plugins for auth and their settings in the auth_registry....
marcink -
r4220:5a873939 stable
parent child Browse files
Show More
@@ -1,578 +1,580 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.tests import (
27 27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 28 no_newline_id_generator)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.lib.auth import check_password
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 33 from rhodecode.model.db import User, Notification, UserApiKeys
34 34 from rhodecode.model.meta import Session
35 35
36 36 fixture = Fixture()
37 37
38 38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 39
40 40
41 41 def route_path(name, params=None, **kwargs):
42 42 import urllib
43 43 from rhodecode.apps._base import ADMIN_PREFIX
44 44
45 45 base_url = {
46 46 'login': ADMIN_PREFIX + '/login',
47 47 'logout': ADMIN_PREFIX + '/logout',
48 48 'register': ADMIN_PREFIX + '/register',
49 49 'reset_password':
50 50 ADMIN_PREFIX + '/password_reset',
51 51 'reset_password_confirmation':
52 52 ADMIN_PREFIX + '/password_reset_confirmation',
53 53
54 54 'admin_permissions_application':
55 55 ADMIN_PREFIX + '/permissions/application',
56 56 'admin_permissions_application_update':
57 57 ADMIN_PREFIX + '/permissions/application/update',
58 58
59 59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 60
61 61 }[name].format(**kwargs)
62 62
63 63 if params:
64 64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 65 return base_url
66 66
67 67
68 68 @pytest.mark.usefixtures('app')
69 69 class TestLoginController(object):
70 70 destroy_users = set()
71 71
72 72 @classmethod
73 73 def teardown_class(cls):
74 74 fixture.destroy_users(cls.destroy_users)
75 75
76 76 def teardown_method(self, method):
77 77 for n in Notification.query().all():
78 78 Session().delete(n)
79 79
80 80 Session().commit()
81 81 assert Notification.query().all() == []
82 82
83 83 def test_index(self):
84 84 response = self.app.get(route_path('login'))
85 85 assert response.status == '200 OK'
86 86 # Test response...
87 87
88 88 def test_login_admin_ok(self):
89 89 response = self.app.post(route_path('login'),
90 90 {'username': 'test_admin',
91 91 'password': 'test12'}, status=302)
92 92 response = response.follow()
93 93 session = response.get_session_from_response()
94 94 username = session['rhodecode_user'].get('username')
95 95 assert username == 'test_admin'
96 96 response.mustcontain('logout')
97 97
98 98 def test_login_regular_ok(self):
99 99 response = self.app.post(route_path('login'),
100 100 {'username': 'test_regular',
101 101 'password': 'test12'}, status=302)
102 102
103 103 response = response.follow()
104 104 session = response.get_session_from_response()
105 105 username = session['rhodecode_user'].get('username')
106 106 assert username == 'test_regular'
107 107 response.mustcontain('logout')
108 108
109 109 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 with fixture.auth_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
111 with fixture.auth_restriction(self.app._pyramid_registry,
112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
112 113 response = self.app.post(route_path('login'),
113 114 {'username': 'test_regular',
114 115 'password': 'test12'})
115 116
116 117 response.mustcontain('invalid user name')
117 118 response.mustcontain('invalid password')
118 119
119 120 def test_login_regular_forbidden_when_scope_restriction(self):
120 121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 with fixture.scope_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
122 with fixture.scope_restriction(self.app._pyramid_registry,
123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
122 124 response = self.app.post(route_path('login'),
123 125 {'username': 'test_regular',
124 126 'password': 'test12'})
125 127
126 128 response.mustcontain('invalid user name')
127 129 response.mustcontain('invalid password')
128 130
129 131 def test_login_ok_came_from(self):
130 132 test_came_from = '/_admin/users?branch=stable'
131 133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
132 134 response = self.app.post(
133 135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
134 136
135 137 assert 'branch=stable' in response.location
136 138 response = response.follow()
137 139
138 140 assert response.status == '200 OK'
139 141 response.mustcontain('Users administration')
140 142
141 143 def test_redirect_to_login_with_get_args(self):
142 144 with fixture.anon_access(False):
143 145 kwargs = {'branch': 'stable'}
144 146 response = self.app.get(
145 147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
146 148 status=302)
147 149
148 150 response_query = urlparse.parse_qsl(response.location)
149 151 assert 'branch=stable' in response_query[0][1]
150 152
151 153 def test_login_form_with_get_args(self):
152 154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
153 155 response = self.app.get(_url)
154 156 assert 'branch%3Dstable' in response.form.action
155 157
156 158 @pytest.mark.parametrize("url_came_from", [
157 159 'data:text/html,<script>window.alert("xss")</script>',
158 160 'mailto:test@rhodecode.org',
159 161 'file:///etc/passwd',
160 162 'ftp://some.ftp.server',
161 163 'http://other.domain',
162 164 '/\r\nX-Forwarded-Host: http://example.org',
163 165 ], ids=no_newline_id_generator)
164 166 def test_login_bad_came_froms(self, url_came_from):
165 167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
166 168 response = self.app.post(
167 169 _url,
168 170 {'username': 'test_admin', 'password': 'test12'})
169 171 assert response.status == '302 Found'
170 172 response = response.follow()
171 173 assert response.status == '200 OK'
172 174 assert response.request.path == '/'
173 175
174 176 def test_login_short_password(self):
175 177 response = self.app.post(route_path('login'),
176 178 {'username': 'test_admin',
177 179 'password': 'as'})
178 180 assert response.status == '200 OK'
179 181
180 182 response.mustcontain('Enter 3 characters or more')
181 183
182 184 def test_login_wrong_non_ascii_password(self, user_regular):
183 185 response = self.app.post(
184 186 route_path('login'),
185 187 {'username': user_regular.username,
186 188 'password': u'invalid-non-asci\xe4'.encode('utf8')})
187 189
188 190 response.mustcontain('invalid user name')
189 191 response.mustcontain('invalid password')
190 192
191 193 def test_login_with_non_ascii_password(self, user_util):
192 194 password = u'valid-non-ascii\xe4'
193 195 user = user_util.create_user(password=password)
194 196 response = self.app.post(
195 197 route_path('login'),
196 198 {'username': user.username,
197 199 'password': password.encode('utf-8')})
198 200 assert response.status_code == 302
199 201
200 202 def test_login_wrong_username_password(self):
201 203 response = self.app.post(route_path('login'),
202 204 {'username': 'error',
203 205 'password': 'test12'})
204 206
205 207 response.mustcontain('invalid user name')
206 208 response.mustcontain('invalid password')
207 209
208 210 def test_login_admin_ok_password_migration(self, real_crypto_backend):
209 211 from rhodecode.lib import auth
210 212
211 213 # create new user, with sha256 password
212 214 temp_user = 'test_admin_sha256'
213 215 user = fixture.create_user(temp_user)
214 216 user.password = auth._RhodeCodeCryptoSha256().hash_create(
215 217 b'test123')
216 218 Session().add(user)
217 219 Session().commit()
218 220 self.destroy_users.add(temp_user)
219 221 response = self.app.post(route_path('login'),
220 222 {'username': temp_user,
221 223 'password': 'test123'}, status=302)
222 224
223 225 response = response.follow()
224 226 session = response.get_session_from_response()
225 227 username = session['rhodecode_user'].get('username')
226 228 assert username == temp_user
227 229 response.mustcontain('logout')
228 230
229 231 # new password should be bcrypted, after log-in and transfer
230 232 user = User.get_by_username(temp_user)
231 233 assert user.password.startswith('$')
232 234
233 235 # REGISTRATIONS
234 236 def test_register(self):
235 237 response = self.app.get(route_path('register'))
236 238 response.mustcontain('Create an Account')
237 239
238 240 def test_register_err_same_username(self):
239 241 uname = 'test_admin'
240 242 response = self.app.post(
241 243 route_path('register'),
242 244 {
243 245 'username': uname,
244 246 'password': 'test12',
245 247 'password_confirmation': 'test12',
246 248 'email': 'goodmail@domain.com',
247 249 'firstname': 'test',
248 250 'lastname': 'test'
249 251 }
250 252 )
251 253
252 254 assertr = response.assert_response()
253 255 msg = 'Username "%(username)s" already exists'
254 256 msg = msg % {'username': uname}
255 257 assertr.element_contains('#username+.error-message', msg)
256 258
257 259 def test_register_err_same_email(self):
258 260 response = self.app.post(
259 261 route_path('register'),
260 262 {
261 263 'username': 'test_admin_0',
262 264 'password': 'test12',
263 265 'password_confirmation': 'test12',
264 266 'email': 'test_admin@mail.com',
265 267 'firstname': 'test',
266 268 'lastname': 'test'
267 269 }
268 270 )
269 271
270 272 assertr = response.assert_response()
271 273 msg = u'This e-mail address is already taken'
272 274 assertr.element_contains('#email+.error-message', msg)
273 275
274 276 def test_register_err_same_email_case_sensitive(self):
275 277 response = self.app.post(
276 278 route_path('register'),
277 279 {
278 280 'username': 'test_admin_1',
279 281 'password': 'test12',
280 282 'password_confirmation': 'test12',
281 283 'email': 'TesT_Admin@mail.COM',
282 284 'firstname': 'test',
283 285 'lastname': 'test'
284 286 }
285 287 )
286 288 assertr = response.assert_response()
287 289 msg = u'This e-mail address is already taken'
288 290 assertr.element_contains('#email+.error-message', msg)
289 291
290 292 def test_register_err_wrong_data(self):
291 293 response = self.app.post(
292 294 route_path('register'),
293 295 {
294 296 'username': 'xs',
295 297 'password': 'test',
296 298 'password_confirmation': 'test',
297 299 'email': 'goodmailm',
298 300 'firstname': 'test',
299 301 'lastname': 'test'
300 302 }
301 303 )
302 304 assert response.status == '200 OK'
303 305 response.mustcontain('An email address must contain a single @')
304 306 response.mustcontain('Enter a value 6 characters long or more')
305 307
306 308 def test_register_err_username(self):
307 309 response = self.app.post(
308 310 route_path('register'),
309 311 {
310 312 'username': 'error user',
311 313 'password': 'test12',
312 314 'password_confirmation': 'test12',
313 315 'email': 'goodmailm',
314 316 'firstname': 'test',
315 317 'lastname': 'test'
316 318 }
317 319 )
318 320
319 321 response.mustcontain('An email address must contain a single @')
320 322 response.mustcontain(
321 323 'Username may only contain '
322 324 'alphanumeric characters underscores, '
323 325 'periods or dashes and must begin with '
324 326 'alphanumeric character')
325 327
326 328 def test_register_err_case_sensitive(self):
327 329 usr = 'Test_Admin'
328 330 response = self.app.post(
329 331 route_path('register'),
330 332 {
331 333 'username': usr,
332 334 'password': 'test12',
333 335 'password_confirmation': 'test12',
334 336 'email': 'goodmailm',
335 337 'firstname': 'test',
336 338 'lastname': 'test'
337 339 }
338 340 )
339 341
340 342 assertr = response.assert_response()
341 343 msg = u'Username "%(username)s" already exists'
342 344 msg = msg % {'username': usr}
343 345 assertr.element_contains('#username+.error-message', msg)
344 346
345 347 def test_register_special_chars(self):
346 348 response = self.app.post(
347 349 route_path('register'),
348 350 {
349 351 'username': 'xxxaxn',
350 352 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
351 353 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
352 354 'email': 'goodmailm@test.plx',
353 355 'firstname': 'test',
354 356 'lastname': 'test'
355 357 }
356 358 )
357 359
358 360 msg = u'Invalid characters (non-ascii) in password'
359 361 response.mustcontain(msg)
360 362
361 363 def test_register_password_mismatch(self):
362 364 response = self.app.post(
363 365 route_path('register'),
364 366 {
365 367 'username': 'xs',
366 368 'password': '123qwe',
367 369 'password_confirmation': 'qwe123',
368 370 'email': 'goodmailm@test.plxa',
369 371 'firstname': 'test',
370 372 'lastname': 'test'
371 373 }
372 374 )
373 375 msg = u'Passwords do not match'
374 376 response.mustcontain(msg)
375 377
376 378 def test_register_ok(self):
377 379 username = 'test_regular4'
378 380 password = 'qweqwe'
379 381 email = 'marcin@test.com'
380 382 name = 'testname'
381 383 lastname = 'testlastname'
382 384
383 385 # this initializes a session
384 386 response = self.app.get(route_path('register'))
385 387 response.mustcontain('Create an Account')
386 388
387 389
388 390 response = self.app.post(
389 391 route_path('register'),
390 392 {
391 393 'username': username,
392 394 'password': password,
393 395 'password_confirmation': password,
394 396 'email': email,
395 397 'firstname': name,
396 398 'lastname': lastname,
397 399 'admin': True
398 400 },
399 401 status=302
400 402 ) # This should be overridden
401 403
402 404 assert_session_flash(
403 405 response, 'You have successfully registered with RhodeCode. You can log-in now.')
404 406
405 407 ret = Session().query(User).filter(
406 408 User.username == 'test_regular4').one()
407 409 assert ret.username == username
408 410 assert check_password(password, ret.password)
409 411 assert ret.email == email
410 412 assert ret.name == name
411 413 assert ret.lastname == lastname
412 414 assert ret.auth_tokens is not None
413 415 assert not ret.admin
414 416
415 417 def test_forgot_password_wrong_mail(self):
416 418 bad_email = 'marcin@wrongmail.org'
417 419 # this initializes a session
418 420 self.app.get(route_path('reset_password'))
419 421
420 422 response = self.app.post(
421 423 route_path('reset_password'), {'email': bad_email, }
422 424 )
423 425 assert_session_flash(response,
424 426 'If such email exists, a password reset link was sent to it.')
425 427
426 428 def test_forgot_password(self, user_util):
427 429 # this initializes a session
428 430 self.app.get(route_path('reset_password'))
429 431
430 432 user = user_util.create_user()
431 433 user_id = user.user_id
432 434 email = user.email
433 435
434 436 response = self.app.post(route_path('reset_password'), {'email': email, })
435 437
436 438 assert_session_flash(response,
437 439 'If such email exists, a password reset link was sent to it.')
438 440
439 441 # BAD KEY
440 442 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
441 443 response = self.app.get(confirm_url, status=302)
442 444 assert response.location.endswith(route_path('reset_password'))
443 445 assert_session_flash(response, 'Given reset token is invalid')
444 446
445 447 response.follow() # cleanup flash
446 448
447 449 # GOOD KEY
448 450 key = UserApiKeys.query()\
449 451 .filter(UserApiKeys.user_id == user_id)\
450 452 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
451 453 .first()
452 454
453 455 assert key
454 456
455 457 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
456 458 response = self.app.get(confirm_url)
457 459 assert response.status == '302 Found'
458 460 assert response.location.endswith(route_path('login'))
459 461
460 462 assert_session_flash(
461 463 response,
462 464 'Your password reset was successful, '
463 465 'a new password has been sent to your email')
464 466
465 467 response.follow()
466 468
467 469 def _get_api_whitelist(self, values=None):
468 470 config = {'api_access_controllers_whitelist': values or []}
469 471 return config
470 472
471 473 @pytest.mark.parametrize("test_name, auth_token", [
472 474 ('none', None),
473 475 ('empty_string', ''),
474 476 ('fake_number', '123456'),
475 477 ('proper_auth_token', None)
476 478 ])
477 479 def test_access_not_whitelisted_page_via_auth_token(
478 480 self, test_name, auth_token, user_admin):
479 481
480 482 whitelist = self._get_api_whitelist([])
481 483 with mock.patch.dict('rhodecode.CONFIG', whitelist):
482 484 assert [] == whitelist['api_access_controllers_whitelist']
483 485 if test_name == 'proper_auth_token':
484 486 # use builtin if api_key is None
485 487 auth_token = user_admin.api_key
486 488
487 489 with fixture.anon_access(False):
488 490 self.app.get(
489 491 route_path('repo_commit_raw',
490 492 repo_name=HG_REPO, commit_id='tip',
491 493 params=dict(api_key=auth_token)),
492 494 status=302)
493 495
494 496 @pytest.mark.parametrize("test_name, auth_token, code", [
495 497 ('none', None, 302),
496 498 ('empty_string', '', 302),
497 499 ('fake_number', '123456', 302),
498 500 ('proper_auth_token', None, 200)
499 501 ])
500 502 def test_access_whitelisted_page_via_auth_token(
501 503 self, test_name, auth_token, code, user_admin):
502 504
503 505 whitelist = self._get_api_whitelist(whitelist_view)
504 506
505 507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
506 508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
507 509
508 510 if test_name == 'proper_auth_token':
509 511 auth_token = user_admin.api_key
510 512 assert auth_token
511 513
512 514 with fixture.anon_access(False):
513 515 self.app.get(
514 516 route_path('repo_commit_raw',
515 517 repo_name=HG_REPO, commit_id='tip',
516 518 params=dict(api_key=auth_token)),
517 519 status=code)
518 520
519 521 @pytest.mark.parametrize("test_name, auth_token, code", [
520 522 ('proper_auth_token', None, 200),
521 523 ('wrong_auth_token', '123456', 302),
522 524 ])
523 525 def test_access_whitelisted_page_via_auth_token_bound_to_token(
524 526 self, test_name, auth_token, code, user_admin):
525 527
526 528 expected_token = auth_token
527 529 if test_name == 'proper_auth_token':
528 530 auth_token = user_admin.api_key
529 531 expected_token = auth_token
530 532 assert auth_token
531 533
532 534 whitelist = self._get_api_whitelist([
533 535 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
534 536
535 537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
536 538
537 539 with fixture.anon_access(False):
538 540 self.app.get(
539 541 route_path('repo_commit_raw',
540 542 repo_name=HG_REPO, commit_id='tip',
541 543 params=dict(api_key=auth_token)),
542 544 status=code)
543 545
544 546 def test_access_page_via_extra_auth_token(self):
545 547 whitelist = self._get_api_whitelist(whitelist_view)
546 548 with mock.patch.dict('rhodecode.CONFIG', whitelist):
547 549 assert whitelist_view == \
548 550 whitelist['api_access_controllers_whitelist']
549 551
550 552 new_auth_token = AuthTokenModel().create(
551 553 TEST_USER_ADMIN_LOGIN, 'test')
552 554 Session().commit()
553 555 with fixture.anon_access(False):
554 556 self.app.get(
555 557 route_path('repo_commit_raw',
556 558 repo_name=HG_REPO, commit_id='tip',
557 559 params=dict(api_key=new_auth_token.api_key)),
558 560 status=200)
559 561
560 562 def test_access_page_via_expired_auth_token(self):
561 563 whitelist = self._get_api_whitelist(whitelist_view)
562 564 with mock.patch.dict('rhodecode.CONFIG', whitelist):
563 565 assert whitelist_view == \
564 566 whitelist['api_access_controllers_whitelist']
565 567
566 568 new_auth_token = AuthTokenModel().create(
567 569 TEST_USER_ADMIN_LOGIN, 'test')
568 570 Session().commit()
569 571 # patch the api key and make it expired
570 572 new_auth_token.expires = 0
571 573 Session().add(new_auth_token)
572 574 Session().commit()
573 575 with fixture.anon_access(False):
574 576 self.app.get(
575 577 route_path('repo_commit_raw',
576 578 repo_name=HG_REPO, commit_id='tip',
577 579 params=dict(api_key=new_auth_token.api_key)),
578 580 status=302)
@@ -1,797 +1,808 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24 import socket
25 25 import string
26 26 import colander
27 27 import copy
28 28 import logging
29 29 import time
30 30 import traceback
31 31 import warnings
32 32 import functools
33 33
34 34 from pyramid.threadlocal import get_current_registry
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int, safe_str
41 41 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
42 42 LdapPasswordError
43 43 from rhodecode.model.db import User
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import SettingsModel
46 46 from rhodecode.model.user import UserModel
47 47 from rhodecode.model.user_group import UserGroupModel
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52 # auth types that authenticate() function can receive
53 53 VCS_TYPE = 'vcs'
54 54 HTTP_TYPE = 'http'
55 55
56 56 external_auth_session_key = 'rhodecode.external_auth'
57 57
58 58
59 59 class hybrid_property(object):
60 60 """
61 61 a property decorator that works both for instance and class
62 62 """
63 63 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 64 self.fget = fget
65 65 self.fset = fset
66 66 self.fdel = fdel
67 67 self.expr = expr or fget
68 68 functools.update_wrapper(self, fget)
69 69
70 70 def __get__(self, instance, owner):
71 71 if instance is None:
72 72 return self.expr(owner)
73 73 else:
74 74 return self.fget(instance)
75 75
76 76 def __set__(self, instance, value):
77 77 self.fset(instance, value)
78 78
79 79 def __delete__(self, instance):
80 80 self.fdel(instance)
81 81
82 82
83 83 class LazyFormencode(object):
84 84 def __init__(self, formencode_obj, *args, **kwargs):
85 85 self.formencode_obj = formencode_obj
86 86 self.args = args
87 87 self.kwargs = kwargs
88 88
89 89 def __call__(self, *args, **kwargs):
90 90 from inspect import isfunction
91 91 formencode_obj = self.formencode_obj
92 92 if isfunction(formencode_obj):
93 93 # case we wrap validators into functions
94 94 formencode_obj = self.formencode_obj(*args, **kwargs)
95 95 return formencode_obj(*self.args, **self.kwargs)
96 96
97 97
98 98 class RhodeCodeAuthPluginBase(object):
99 99 # UID is used to register plugin to the registry
100 100 uid = None
101 101
102 102 # cache the authentication request for N amount of seconds. Some kind
103 103 # of authentication methods are very heavy and it's very efficient to cache
104 104 # the result of a call. If it's set to None (default) cache is off
105 105 AUTH_CACHE_TTL = None
106 106 AUTH_CACHE = {}
107 107
108 108 auth_func_attrs = {
109 109 "username": "unique username",
110 110 "firstname": "first name",
111 111 "lastname": "last name",
112 112 "email": "email address",
113 113 "groups": '["list", "of", "groups"]',
114 114 "user_group_sync":
115 115 'True|False defines if returned user groups should be synced',
116 116 "extern_name": "name in external source of record",
117 117 "extern_type": "type of external source of record",
118 118 "admin": 'True|False defines if user should be RhodeCode super admin',
119 119 "active":
120 120 'True|False defines active state of user internally for RhodeCode',
121 121 "active_from_extern":
122 122 "True|False|None, active state from the external auth, "
123 123 "None means use definition from RhodeCode extern_type active value"
124 124
125 125 }
126 126 # set on authenticate() method and via set_auth_type func.
127 127 auth_type = None
128 128
129 129 # set on authenticate() method and via set_calling_scope_repo, this is a
130 130 # calling scope repository when doing authentication most likely on VCS
131 131 # operations
132 132 acl_repo_name = None
133 133
134 134 # List of setting names to store encrypted. Plugins may override this list
135 135 # to store settings encrypted.
136 136 _settings_encrypted = []
137 137
138 138 # Mapping of python to DB settings model types. Plugins may override or
139 139 # extend this mapping.
140 140 _settings_type_map = {
141 141 colander.String: 'unicode',
142 142 colander.Integer: 'int',
143 143 colander.Boolean: 'bool',
144 144 colander.List: 'list',
145 145 }
146 146
147 147 # list of keys in settings that are unsafe to be logged, should be passwords
148 148 # or other crucial credentials
149 149 _settings_unsafe_keys = []
150 150
151 151 def __init__(self, plugin_id):
152 152 self._plugin_id = plugin_id
153 self._settings = {}
153 154
154 155 def __str__(self):
155 156 return self.get_id()
156 157
157 158 def _get_setting_full_name(self, name):
158 159 """
159 160 Return the full setting name used for storing values in the database.
160 161 """
161 162 # TODO: johbo: Using the name here is problematic. It would be good to
162 163 # introduce either new models in the database to hold Plugin and
163 164 # PluginSetting or to use the plugin id here.
164 165 return 'auth_{}_{}'.format(self.name, name)
165 166
166 167 def _get_setting_type(self, name):
167 168 """
168 169 Return the type of a setting. This type is defined by the SettingsModel
169 170 and determines how the setting is stored in DB. Optionally the suffix
170 171 `.encrypted` is appended to instruct SettingsModel to store it
171 172 encrypted.
172 173 """
173 174 schema_node = self.get_settings_schema().get(name)
174 175 db_type = self._settings_type_map.get(
175 176 type(schema_node.typ), 'unicode')
176 177 if name in self._settings_encrypted:
177 178 db_type = '{}.encrypted'.format(db_type)
178 179 return db_type
179 180
180 181 @classmethod
181 182 def docs(cls):
182 183 """
183 184 Defines documentation url which helps with plugin setup
184 185 """
185 186 return ''
186 187
187 188 @classmethod
188 189 def icon(cls):
189 190 """
190 191 Defines ICON in SVG format for authentication method
191 192 """
192 193 return ''
193 194
194 195 def is_enabled(self):
195 196 """
196 197 Returns true if this plugin is enabled. An enabled plugin can be
197 198 configured in the admin interface but it is not consulted during
198 199 authentication.
199 200 """
200 201 auth_plugins = SettingsModel().get_auth_plugins()
201 202 return self.get_id() in auth_plugins
202 203
203 204 def is_active(self, plugin_cached_settings=None):
204 205 """
205 206 Returns true if the plugin is activated. An activated plugin is
206 207 consulted during authentication, assumed it is also enabled.
207 208 """
208 209 return self.get_setting_by_name(
209 210 'enabled', plugin_cached_settings=plugin_cached_settings)
210 211
211 212 def get_id(self):
212 213 """
213 214 Returns the plugin id.
214 215 """
215 216 return self._plugin_id
216 217
217 218 def get_display_name(self):
218 219 """
219 220 Returns a translation string for displaying purposes.
220 221 """
221 222 raise NotImplementedError('Not implemented in base class')
222 223
223 224 def get_settings_schema(self):
224 225 """
225 226 Returns a colander schema, representing the plugin settings.
226 227 """
227 228 return AuthnPluginSettingsSchemaBase()
228 229
229 def get_settings(self):
230 """
231 Returns the plugin settings as dictionary.
232 """
230 def _propagate_settings(self, raw_settings):
233 231 settings = {}
234 raw_settings = SettingsModel().get_all_settings()
235 232 for node in self.get_settings_schema():
236 233 settings[node.name] = self.get_setting_by_name(
237 234 node.name, plugin_cached_settings=raw_settings)
238 235 return settings
239 236
237 def get_settings(self, use_cache=True):
238 """
239 Returns the plugin settings as dictionary.
240 """
241 if self._settings != {} and use_cache:
242 return self._settings
243
244 raw_settings = SettingsModel().get_all_settings()
245 settings = self._propagate_settings(raw_settings)
246
247 self._settings = settings
248 return self._settings
249
240 250 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
241 251 """
242 252 Returns a plugin setting by name.
243 253 """
244 254 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
245 255 if plugin_cached_settings:
246 256 plugin_settings = plugin_cached_settings
247 257 else:
248 258 plugin_settings = SettingsModel().get_all_settings()
249 259
250 260 if full_name in plugin_settings:
251 261 return plugin_settings[full_name]
252 262 else:
253 263 return default
254 264
255 265 def create_or_update_setting(self, name, value):
256 266 """
257 267 Create or update a setting for this plugin in the persistent storage.
258 268 """
259 269 full_name = self._get_setting_full_name(name)
260 270 type_ = self._get_setting_type(name)
261 271 db_setting = SettingsModel().create_or_update_setting(
262 272 full_name, value, type_)
263 273 return db_setting.app_settings_value
264 274
265 275 def log_safe_settings(self, settings):
266 276 """
267 277 returns a log safe representation of settings, without any secrets
268 278 """
269 279 settings_copy = copy.deepcopy(settings)
270 280 for k in self._settings_unsafe_keys:
271 281 if k in settings_copy:
272 282 del settings_copy[k]
273 283 return settings_copy
274 284
275 285 @hybrid_property
276 286 def name(self):
277 287 """
278 288 Returns the name of this authentication plugin.
279 289
280 290 :returns: string
281 291 """
282 292 raise NotImplementedError("Not implemented in base class")
283 293
284 294 def get_url_slug(self):
285 295 """
286 296 Returns a slug which should be used when constructing URLs which refer
287 297 to this plugin. By default it returns the plugin name. If the name is
288 298 not suitable for using it in an URL the plugin should override this
289 299 method.
290 300 """
291 301 return self.name
292 302
293 303 @property
294 304 def is_headers_auth(self):
295 305 """
296 306 Returns True if this authentication plugin uses HTTP headers as
297 307 authentication method.
298 308 """
299 309 return False
300 310
301 311 @hybrid_property
302 312 def is_container_auth(self):
303 313 """
304 314 Deprecated method that indicates if this authentication plugin uses
305 315 HTTP headers as authentication method.
306 316 """
307 317 warnings.warn(
308 318 'Use is_headers_auth instead.', category=DeprecationWarning)
309 319 return self.is_headers_auth
310 320
311 321 @hybrid_property
312 322 def allows_creating_users(self):
313 323 """
314 324 Defines if Plugin allows users to be created on-the-fly when
315 325 authentication is called. Controls how external plugins should behave
316 326 in terms if they are allowed to create new users, or not. Base plugins
317 327 should not be allowed to, but External ones should be !
318 328
319 329 :return: bool
320 330 """
321 331 return False
322 332
323 333 def set_auth_type(self, auth_type):
324 334 self.auth_type = auth_type
325 335
326 336 def set_calling_scope_repo(self, acl_repo_name):
327 337 self.acl_repo_name = acl_repo_name
328 338
329 339 def allows_authentication_from(
330 340 self, user, allows_non_existing_user=True,
331 341 allowed_auth_plugins=None, allowed_auth_sources=None):
332 342 """
333 343 Checks if this authentication module should accept a request for
334 344 the current user.
335 345
336 346 :param user: user object fetched using plugin's get_user() method.
337 347 :param allows_non_existing_user: if True, don't allow the
338 348 user to be empty, meaning not existing in our database
339 349 :param allowed_auth_plugins: if provided, users extern_type will be
340 350 checked against a list of provided extern types, which are plugin
341 351 auth_names in the end
342 352 :param allowed_auth_sources: authentication type allowed,
343 353 `http` or `vcs` default is both.
344 354 defines if plugin will accept only http authentication vcs
345 355 authentication(git/hg) or both
346 356 :returns: boolean
347 357 """
348 358 if not user and not allows_non_existing_user:
349 359 log.debug('User is empty but plugin does not allow empty users,'
350 360 'not allowed to authenticate')
351 361 return False
352 362
353 363 expected_auth_plugins = allowed_auth_plugins or [self.name]
354 364 if user and (user.extern_type and
355 365 user.extern_type not in expected_auth_plugins):
356 366 log.debug(
357 367 'User `%s` is bound to `%s` auth type. Plugin allows only '
358 368 '%s, skipping', user, user.extern_type, expected_auth_plugins)
359 369
360 370 return False
361 371
362 372 # by default accept both
363 373 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
364 374 if self.auth_type not in expected_auth_from:
365 375 log.debug('Current auth source is %s but plugin only allows %s',
366 376 self.auth_type, expected_auth_from)
367 377 return False
368 378
369 379 return True
370 380
371 381 def get_user(self, username=None, **kwargs):
372 382 """
373 383 Helper method for user fetching in plugins, by default it's using
374 384 simple fetch by username, but this method can be custimized in plugins
375 385 eg. headers auth plugin to fetch user by environ params
376 386
377 387 :param username: username if given to fetch from database
378 388 :param kwargs: extra arguments needed for user fetching.
379 389 """
380 390 user = None
381 391 log.debug(
382 392 'Trying to fetch user `%s` from RhodeCode database', username)
383 393 if username:
384 394 user = User.get_by_username(username)
385 395 if not user:
386 396 log.debug('User not found, fallback to fetch user in '
387 397 'case insensitive mode')
388 398 user = User.get_by_username(username, case_insensitive=True)
389 399 else:
390 400 log.debug('provided username:`%s` is empty skipping...', username)
391 401 if not user:
392 402 log.debug('User `%s` not found in database', username)
393 403 else:
394 404 log.debug('Got DB user:%s', user)
395 405 return user
396 406
397 407 def user_activation_state(self):
398 408 """
399 409 Defines user activation state when creating new users
400 410
401 411 :returns: boolean
402 412 """
403 413 raise NotImplementedError("Not implemented in base class")
404 414
405 415 def auth(self, userobj, username, passwd, settings, **kwargs):
406 416 """
407 417 Given a user object (which may be null), username, a plaintext
408 418 password, and a settings object (containing all the keys needed as
409 419 listed in settings()), authenticate this user's login attempt.
410 420
411 421 Return None on failure. On success, return a dictionary of the form:
412 422
413 423 see: RhodeCodeAuthPluginBase.auth_func_attrs
414 424 This is later validated for correctness
415 425 """
416 426 raise NotImplementedError("not implemented in base class")
417 427
418 428 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
419 429 """
420 430 Wrapper to call self.auth() that validates call on it
421 431
422 432 :param userobj: userobj
423 433 :param username: username
424 434 :param passwd: plaintext password
425 435 :param settings: plugin settings
426 436 """
427 437 auth = self.auth(userobj, username, passwd, settings, **kwargs)
428 438 if auth:
429 439 auth['_plugin'] = self.name
430 440 auth['_ttl_cache'] = self.get_ttl_cache(settings)
431 441 # check if hash should be migrated ?
432 442 new_hash = auth.get('_hash_migrate')
433 443 if new_hash:
434 444 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
435 445 if 'user_group_sync' not in auth:
436 446 auth['user_group_sync'] = False
437 447 return self._validate_auth_return(auth)
438 448 return auth
439 449
440 450 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
441 451 new_hash_cypher = _RhodeCodeCryptoBCrypt()
442 452 # extra checks, so make sure new hash is correct.
443 453 password_encoded = safe_str(password)
444 454 if new_hash and new_hash_cypher.hash_check(
445 455 password_encoded, new_hash):
446 456 cur_user = User.get_by_username(username)
447 457 cur_user.password = new_hash
448 458 Session().add(cur_user)
449 459 Session().flush()
450 460 log.info('Migrated user %s hash to bcrypt', cur_user)
451 461
452 462 def _validate_auth_return(self, ret):
453 463 if not isinstance(ret, dict):
454 464 raise Exception('returned value from auth must be a dict')
455 465 for k in self.auth_func_attrs:
456 466 if k not in ret:
457 467 raise Exception('Missing %s attribute from returned data' % k)
458 468 return ret
459 469
460 470 def get_ttl_cache(self, settings=None):
461 471 plugin_settings = settings or self.get_settings()
462 472 # we set default to 30, we make a compromise here,
463 473 # performance > security, mostly due to LDAP/SVN, majority
464 474 # of users pick cache_ttl to be enabled
465 475 from rhodecode.authentication import plugin_default_auth_ttl
466 476 cache_ttl = plugin_default_auth_ttl
467 477
468 478 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
469 479 # plugin cache set inside is more important than the settings value
470 480 cache_ttl = self.AUTH_CACHE_TTL
471 481 elif plugin_settings.get('cache_ttl'):
472 482 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
473 483
474 484 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
475 485 return plugin_cache_active, cache_ttl
476 486
477 487
478 488 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
479 489
480 490 @hybrid_property
481 491 def allows_creating_users(self):
482 492 return True
483 493
484 494 def use_fake_password(self):
485 495 """
486 496 Return a boolean that indicates whether or not we should set the user's
487 497 password to a random value when it is authenticated by this plugin.
488 498 If your plugin provides authentication, then you will generally
489 499 want this.
490 500
491 501 :returns: boolean
492 502 """
493 503 raise NotImplementedError("Not implemented in base class")
494 504
495 505 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
496 506 # at this point _authenticate calls plugin's `auth()` function
497 507 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
498 508 userobj, username, passwd, settings, **kwargs)
499 509
500 510 if auth:
501 511 # maybe plugin will clean the username ?
502 512 # we should use the return value
503 513 username = auth['username']
504 514
505 515 # if external source tells us that user is not active, we should
506 516 # skip rest of the process. This can prevent from creating users in
507 517 # RhodeCode when using external authentication, but if it's
508 518 # inactive user we shouldn't create that user anyway
509 519 if auth['active_from_extern'] is False:
510 520 log.warning(
511 521 "User %s authenticated against %s, but is inactive",
512 522 username, self.__module__)
513 523 return None
514 524
515 525 cur_user = User.get_by_username(username, case_insensitive=True)
516 526 is_user_existing = cur_user is not None
517 527
518 528 if is_user_existing:
519 529 log.debug('Syncing user `%s` from '
520 530 '`%s` plugin', username, self.name)
521 531 else:
522 532 log.debug('Creating non existing user `%s` from '
523 533 '`%s` plugin', username, self.name)
524 534
525 535 if self.allows_creating_users:
526 536 log.debug('Plugin `%s` allows to '
527 537 'create new users', self.name)
528 538 else:
529 539 log.debug('Plugin `%s` does not allow to '
530 540 'create new users', self.name)
531 541
532 542 user_parameters = {
533 543 'username': username,
534 544 'email': auth["email"],
535 545 'firstname': auth["firstname"],
536 546 'lastname': auth["lastname"],
537 547 'active': auth["active"],
538 548 'admin': auth["admin"],
539 549 'extern_name': auth["extern_name"],
540 550 'extern_type': self.name,
541 551 'plugin': self,
542 552 'allow_to_create_user': self.allows_creating_users,
543 553 }
544 554
545 555 if not is_user_existing:
546 556 if self.use_fake_password():
547 557 # Randomize the PW because we don't need it, but don't want
548 558 # them blank either
549 559 passwd = PasswordGenerator().gen_password(length=16)
550 560 user_parameters['password'] = passwd
551 561 else:
552 562 # Since the password is required by create_or_update method of
553 563 # UserModel, we need to set it explicitly.
554 564 # The create_or_update method is smart and recognises the
555 565 # password hashes as well.
556 566 user_parameters['password'] = cur_user.password
557 567
558 568 # we either create or update users, we also pass the flag
559 569 # that controls if this method can actually do that.
560 570 # raises NotAllowedToCreateUserError if it cannot, and we try to.
561 571 user = UserModel().create_or_update(**user_parameters)
562 572 Session().flush()
563 573 # enforce user is just in given groups, all of them has to be ones
564 574 # created from plugins. We store this info in _group_data JSON
565 575 # field
566 576
567 577 if auth['user_group_sync']:
568 578 try:
569 579 groups = auth['groups'] or []
570 580 log.debug(
571 581 'Performing user_group sync based on set `%s` '
572 582 'returned by `%s` plugin', groups, self.name)
573 583 UserGroupModel().enforce_groups(user, groups, self.name)
574 584 except Exception:
575 585 # for any reason group syncing fails, we should
576 586 # proceed with login
577 587 log.error(traceback.format_exc())
578 588
579 589 Session().commit()
580 590 return auth
581 591
582 592
583 593 class AuthLdapBase(object):
584 594
585 595 @classmethod
586 596 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
587 597
588 598 def host_resolver(host, port, full_resolve=True):
589 599 """
590 600 Main work for this function is to prevent ldap connection issues,
591 601 and detect them early using a "greenified" sockets
592 602 """
593 603 host = host.strip()
594 604 if not full_resolve:
595 605 return '{}:{}'.format(host, port)
596 606
597 607 log.debug('LDAP: Resolving IP for LDAP host %s', host)
598 608 try:
599 609 ip = socket.gethostbyname(host)
600 610 log.debug('Got LDAP server %s ip %s', host, ip)
601 611 except Exception:
602 612 raise LdapConnectionError(
603 613 'Failed to resolve host: `{}`'.format(host))
604 614
605 615 log.debug('LDAP: Checking if IP %s is accessible', ip)
606 616 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
607 617 try:
608 618 s.connect((ip, int(port)))
609 619 s.shutdown(socket.SHUT_RD)
610 620 except Exception:
611 621 raise LdapConnectionError(
612 622 'Failed to connect to host: `{}:{}`'.format(host, port))
613 623
614 624 return '{}:{}'.format(host, port)
615 625
616 626 if len(ldap_server) == 1:
617 627 # in case of single server use resolver to detect potential
618 628 # connection issues
619 629 full_resolve = True
620 630 else:
621 631 full_resolve = False
622 632
623 633 return ', '.join(
624 634 ["{}://{}".format(
625 635 ldap_server_type,
626 636 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
627 637 for host in ldap_server])
628 638
629 639 @classmethod
630 640 def _get_server_list(cls, servers):
631 641 return map(string.strip, servers.split(','))
632 642
633 643 @classmethod
634 644 def get_uid(cls, username, server_addresses):
635 645 uid = username
636 646 for server_addr in server_addresses:
637 647 uid = chop_at(username, "@%s" % server_addr)
638 648 return uid
639 649
640 650 @classmethod
641 651 def validate_username(cls, username):
642 652 if "," in username:
643 653 raise LdapUsernameError(
644 654 "invalid character `,` in username: `{}`".format(username))
645 655
646 656 @classmethod
647 657 def validate_password(cls, username, password):
648 658 if not password:
649 659 msg = "Authenticating user %s with blank password not allowed"
650 660 log.warning(msg, username)
651 661 raise LdapPasswordError(msg)
652 662
653 663
654 664 def loadplugin(plugin_id):
655 665 """
656 666 Loads and returns an instantiated authentication plugin.
657 667 Returns the RhodeCodeAuthPluginBase subclass on success,
658 668 or None on failure.
659 669 """
660 670 # TODO: Disusing pyramids thread locals to retrieve the registry.
661 671 authn_registry = get_authn_registry()
662 672 plugin = authn_registry.get_plugin(plugin_id)
663 673 if plugin is None:
664 674 log.error('Authentication plugin not found: "%s"', plugin_id)
665 675 return plugin
666 676
667 677
668 678 def get_authn_registry(registry=None):
669 679 registry = registry or get_current_registry()
670 authn_registry = registry.getUtility(IAuthnPluginRegistry)
680 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
671 681 return authn_registry
672 682
673 683
674 684 def authenticate(username, password, environ=None, auth_type=None,
675 685 skip_missing=False, registry=None, acl_repo_name=None):
676 686 """
677 687 Authentication function used for access control,
678 688 It tries to authenticate based on enabled authentication modules.
679 689
680 690 :param username: username can be empty for headers auth
681 691 :param password: password can be empty for headers auth
682 692 :param environ: environ headers passed for headers auth
683 693 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
684 694 :param skip_missing: ignores plugins that are in db but not in environment
685 695 :returns: None if auth failed, plugin_user dict if auth is correct
686 696 """
687 697 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
688 698 raise ValueError('auth type must be on of http, vcs got "%s" instead'
689 699 % auth_type)
690 700 headers_only = environ and not (username and password)
691 701
692 702 authn_registry = get_authn_registry(registry)
703
693 704 plugins_to_check = authn_registry.get_plugins_for_authentication()
694 705 log.debug('Starting ordered authentication chain using %s plugins',
695 706 [x.name for x in plugins_to_check])
696 707 for plugin in plugins_to_check:
697 708 plugin.set_auth_type(auth_type)
698 709 plugin.set_calling_scope_repo(acl_repo_name)
699 710
700 711 if headers_only and not plugin.is_headers_auth:
701 712 log.debug('Auth type is for headers only and plugin `%s` is not '
702 713 'headers plugin, skipping...', plugin.get_id())
703 714 continue
704 715
705 716 log.debug('Trying authentication using ** %s **', plugin.get_id())
706 717
707 718 # load plugin settings from RhodeCode database
708 719 plugin_settings = plugin.get_settings()
709 720 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
710 721 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
711 722
712 723 # use plugin's method of user extraction.
713 724 user = plugin.get_user(username, environ=environ,
714 725 settings=plugin_settings)
715 726 display_user = user.username if user else username
716 727 log.debug(
717 728 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
718 729
719 730 if not plugin.allows_authentication_from(user):
720 731 log.debug('Plugin %s does not accept user `%s` for authentication',
721 732 plugin.get_id(), display_user)
722 733 continue
723 734 else:
724 735 log.debug('Plugin %s accepted user `%s` for authentication',
725 736 plugin.get_id(), display_user)
726 737
727 738 log.info('Authenticating user `%s` using %s plugin',
728 739 display_user, plugin.get_id())
729 740
730 741 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
731 742
732 743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
733 744 plugin.get_id(), plugin_cache_active, cache_ttl)
734 745
735 746 user_id = user.user_id if user else None
736 747 # don't cache for empty users
737 748 plugin_cache_active = plugin_cache_active and user_id
738 749 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
739 750 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
740 751
741 752 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
742 753 expiration_time=cache_ttl,
743 754 condition=plugin_cache_active)
744 755 def compute_auth(
745 756 cache_name, plugin_name, username, password):
746 757
747 758 # _authenticate is a wrapper for .auth() method of plugin.
748 759 # it checks if .auth() sends proper data.
749 760 # For RhodeCodeExternalAuthPlugin it also maps users to
750 761 # Database and maps the attributes returned from .auth()
751 762 # to RhodeCode database. If this function returns data
752 763 # then auth is correct.
753 764 log.debug('Running plugin `%s` _authenticate method '
754 765 'using username and password', plugin.get_id())
755 766 return plugin._authenticate(
756 767 user, username, password, plugin_settings,
757 768 environ=environ or {})
758 769
759 770 start = time.time()
760 771 # for environ based auth, password can be empty, but then the validation is
761 772 # on the server that fills in the env data needed for authentication
762 773 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
763 774
764 775 auth_time = time.time() - start
765 776 log.debug('Authentication for plugin `%s` completed in %.4fs, '
766 777 'expiration time of fetched cache %.1fs.',
767 778 plugin.get_id(), auth_time, cache_ttl)
768 779
769 780 log.debug('PLUGIN USER DATA: %s', plugin_user)
770 781
771 782 if plugin_user:
772 783 log.debug('Plugin returned proper authentication data')
773 784 return plugin_user
774 785 # we failed to Auth because .auth() method didn't return proper user
775 786 log.debug("User `%s` failed to authenticate against %s",
776 787 display_user, plugin.get_id())
777 788
778 789 # case when we failed to authenticate against all defined plugins
779 790 return None
780 791
781 792
782 793 def chop_at(s, sub, inclusive=False):
783 794 """Truncate string ``s`` at the first occurrence of ``sub``.
784 795
785 796 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
786 797
787 798 >>> chop_at("plutocratic brats", "rat")
788 799 'plutoc'
789 800 >>> chop_at("plutocratic brats", "rat", True)
790 801 'plutocrat'
791 802 """
792 803 pos = s.find(sub)
793 804 if pos == -1:
794 805 return s
795 806 if inclusive:
796 807 return s[:pos+len(sub)]
797 808 return s[:pos]
@@ -1,94 +1,107 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.exceptions import ConfigurationError
24 24 from zope.interface import implementer
25 25
26 26 from rhodecode.authentication.interface import IAuthnPluginRegistry
27 27 from rhodecode.lib.utils2 import safe_str
28 28 from rhodecode.model.settings import SettingsModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 @implementer(IAuthnPluginRegistry)
34 34 class AuthenticationPluginRegistry(object):
35 35
36 36 # INI settings key to set a fallback authentication plugin.
37 37 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
38 38
39 39 def __init__(self, settings):
40 40 self._plugins = {}
41 self._plugins_for_auth = None
41 42 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
42 43
43 44 def add_authn_plugin(self, config, plugin):
44 45 plugin_id = plugin.get_id()
45 46 if plugin_id in self._plugins.keys():
46 47 raise ConfigurationError(
47 48 'Cannot register authentication plugin twice: "%s"', plugin_id)
48 49 else:
49 50 log.debug('Register authentication plugin: "%s"', plugin_id)
50 51 self._plugins[plugin_id] = plugin
51 52
52 53 def get_plugins(self):
53 54 def sort_key(plugin):
54 55 return str.lower(safe_str(plugin.get_display_name()))
55 56
56 57 return sorted(self._plugins.values(), key=sort_key)
57 58
58 59 def get_plugin(self, plugin_id):
59 60 return self._plugins.get(plugin_id, None)
60 61
61 62 def get_plugin_by_uid(self, plugin_uid):
62 63 for plugin in self._plugins.values():
63 64 if plugin.uid == plugin_uid:
64 65 return plugin
65 66
67 def invalidate_plugins_for_auth(self):
68 log.debug('Invalidating cached plugins for authentication')
69 self._plugins_for_auth = None
70
66 71 def get_plugins_for_authentication(self):
67 72 """
68 73 Returns a list of plugins which should be consulted when authenticating
69 74 a user. It only returns plugins which are enabled and active.
70 75 Additionally it includes the fallback plugin from the INI file, if
71 76 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
72 77 """
78 if self._plugins_for_auth is not None:
79 return self._plugins_for_auth
80
73 81 plugins = []
74 82
75 83 # Add all enabled and active plugins to the list. We iterate over the
76 84 # auth_plugins setting from DB because it also represents the ordering.
77 85 enabled_plugins = SettingsModel().get_auth_plugins()
78 86 raw_settings = SettingsModel().get_all_settings()
79 87 for plugin_id in enabled_plugins:
80 88 plugin = self.get_plugin(plugin_id)
81 89 if plugin is not None and plugin.is_active(
82 90 plugin_cached_settings=raw_settings):
91
92 # inject settings into plugin, we can re-use the DB fetched settings here
93 plugin._settings = plugin._propagate_settings(raw_settings)
83 94 plugins.append(plugin)
84 95
85 96 # Add the fallback plugin from ini file.
86 97 if self._fallback_plugin:
87 98 log.warn(
88 99 'Using fallback authentication plugin from INI file: "%s"',
89 100 self._fallback_plugin)
90 101 plugin = self.get_plugin(self._fallback_plugin)
91 102 if plugin is not None and plugin not in plugins:
103 plugin._settings = plugin._propagate_settings(raw_settings)
92 104 plugins.append(plugin)
93 105
94 return plugins
106 self._plugins_for_auth = plugins
107 return self._plugins_for_auth
@@ -1,179 +1,180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import formencode.htmlfill
23 23 import logging
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import BaseAppView
30 30 from rhodecode.authentication.base import get_authn_registry
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.model.forms import AuthSettingsForm
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.settings import SettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class AuthnPluginViewBase(BaseAppView):
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45 self.plugin = self.context.plugin
46 46 return c
47 47
48 48 @LoginRequired()
49 49 @HasPermissionAllDecorator('hg.admin')
50 50 def settings_get(self, defaults=None, errors=None):
51 51 """
52 52 View that displays the plugin settings as a form.
53 53 """
54 54 c = self.load_default_context()
55 55 defaults = defaults or {}
56 56 errors = errors or {}
57 57 schema = self.plugin.get_settings_schema()
58 58
59 59 # Compute default values for the form. Priority is:
60 60 # 1. Passed to this method 2. DB value 3. Schema default
61 61 for node in schema:
62 62 if node.name not in defaults:
63 63 defaults[node.name] = self.plugin.get_setting_by_name(
64 64 node.name, node.default)
65 65
66 66 template_context = {
67 67 'defaults': defaults,
68 68 'errors': errors,
69 69 'plugin': self.context.plugin,
70 70 'resource': self.context,
71 71 }
72 72
73 73 return self._get_template_context(c, **template_context)
74 74
75 75 @LoginRequired()
76 76 @HasPermissionAllDecorator('hg.admin')
77 77 @CSRFRequired()
78 78 def settings_post(self):
79 79 """
80 80 View that validates and stores the plugin settings.
81 81 """
82 82 _ = self.request.translate
83 83 self.load_default_context()
84 84 schema = self.plugin.get_settings_schema()
85 85 data = self.request.params
86 86
87 87 try:
88 88 valid_data = schema.deserialize(data)
89 89 except colander.Invalid as e:
90 90 # Display error message and display form again.
91 91 h.flash(
92 92 _('Errors exist when saving plugin settings. '
93 93 'Please check the form inputs.'),
94 94 category='error')
95 95 defaults = {key: data[key] for key in data if key in schema}
96 96 return self.settings_get(errors=e.asdict(), defaults=defaults)
97 97
98 98 # Store validated data.
99 99 for name, value in valid_data.items():
100 100 self.plugin.create_or_update_setting(name, value)
101 101 Session().commit()
102 SettingsModel().invalidate_settings_cache()
102 103
103 104 # Display success message and redirect.
104 105 h.flash(_('Auth settings updated successfully.'), category='success')
105 redirect_to = self.request.resource_path(
106 self.context, route_name='auth_home')
106 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
107
107 108 return HTTPFound(redirect_to)
108 109
109 110
110 111 class AuthSettingsView(BaseAppView):
111 112 def load_default_context(self):
112 113 c = self._get_local_tmpl_context()
113 114 return c
114 115
115 116 @LoginRequired()
116 117 @HasPermissionAllDecorator('hg.admin')
117 118 def index(self, defaults=None, errors=None, prefix_error=False):
118 119 c = self.load_default_context()
119 120
120 121 defaults = defaults or {}
121 122 authn_registry = get_authn_registry(self.request.registry)
122 123 enabled_plugins = SettingsModel().get_auth_plugins()
123 124
124 125 # Create template context and render it.
125 126 template_context = {
126 127 'resource': self.context,
127 128 'available_plugins': authn_registry.get_plugins(),
128 129 'enabled_plugins': enabled_plugins,
129 130 }
130 131 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
131 132 self._get_template_context(c, **template_context),
132 133 self.request)
133 134
134 135 # Create form default values and fill the form.
135 136 form_defaults = {
136 137 'auth_plugins': ',\n'.join(enabled_plugins)
137 138 }
138 139 form_defaults.update(defaults)
139 140 html = formencode.htmlfill.render(
140 141 html,
141 142 defaults=form_defaults,
142 143 errors=errors,
143 144 prefix_error=prefix_error,
144 145 encoding="UTF-8",
145 146 force_defaults=False)
146 147
147 148 return Response(html)
148 149
149 150 @LoginRequired()
150 151 @HasPermissionAllDecorator('hg.admin')
151 152 @CSRFRequired()
152 153 def auth_settings(self):
153 154 _ = self.request.translate
154 155 try:
155 156 form = AuthSettingsForm(self.request.translate)()
156 157 form_result = form.to_python(self.request.POST)
157 158 plugins = ','.join(form_result['auth_plugins'])
158 159 setting = SettingsModel().create_or_update_setting(
159 160 'auth_plugins', plugins)
160 161 Session().add(setting)
161 162 Session().commit()
162
163 SettingsModel().invalidate_settings_cache()
163 164 h.flash(_('Auth settings updated successfully.'), category='success')
164 165 except formencode.Invalid as errors:
165 166 e = errors.error_dict or {}
166 167 h.flash(_('Errors exist when saving plugin setting. '
167 168 'Please check the form inputs.'), category='error')
168 169 return self.index(
169 170 defaults=errors.value,
170 171 errors=e,
171 172 prefix_error=False)
172 173 except Exception:
173 174 log.exception('Exception in auth_settings')
174 175 h.flash(_('Error occurred during update of auth settings.'),
175 176 category='error')
176 177
177 redirect_to = self.request.resource_path(
178 self.context, route_name='auth_home')
178 redirect_to = self.request.resource_path(self.context, route_name='auth_home')
179
179 180 return HTTPFound(redirect_to)
@@ -1,615 +1,616 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 45 from rhodecode.lib.utils2 import (
46 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def _filter_proxy(ip):
55 55 """
56 56 Passed in IP addresses in HEADERS can be in a special format of multiple
57 57 ips. Those comma separated IPs are passed from various proxies in the
58 58 chain of request processing. The left-most being the original client.
59 59 We only care about the first IP which came from the org. client.
60 60
61 61 :param ip: ip string from headers
62 62 """
63 63 if ',' in ip:
64 64 _ips = ip.split(',')
65 65 _first_ip = _ips[0].strip()
66 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 67 return _first_ip
68 68 return ip
69 69
70 70
71 71 def _filter_port(ip):
72 72 """
73 73 Removes a port from ip, there are 4 main cases to handle here.
74 74 - ipv4 eg. 127.0.0.1
75 75 - ipv6 eg. ::1
76 76 - ipv4+port eg. 127.0.0.1:8080
77 77 - ipv6+port eg. [::1]:8080
78 78
79 79 :param ip:
80 80 """
81 81 def is_ipv6(ip_addr):
82 82 if hasattr(socket, 'inet_pton'):
83 83 try:
84 84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 85 except socket.error:
86 86 return False
87 87 else:
88 88 # fallback to ipaddress
89 89 try:
90 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 91 except Exception:
92 92 return False
93 93 return True
94 94
95 95 if ':' not in ip: # must be ipv4 pure ip
96 96 return ip
97 97
98 98 if '[' in ip and ']' in ip: # ipv6 with port
99 99 return ip.split(']')[0][1:].lower()
100 100
101 101 # must be ipv6 or ipv4 with port
102 102 if is_ipv6(ip):
103 103 return ip
104 104 else:
105 105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 106 return ip
107 107
108 108
109 109 def get_ip_addr(environ):
110 110 proxy_key = 'HTTP_X_REAL_IP'
111 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 112 def_key = 'REMOTE_ADDR'
113 113 _filters = lambda x: _filter_port(_filter_proxy(x))
114 114
115 115 ip = environ.get(proxy_key)
116 116 if ip:
117 117 return _filters(ip)
118 118
119 119 ip = environ.get(proxy_key2)
120 120 if ip:
121 121 return _filters(ip)
122 122
123 123 ip = environ.get(def_key, '0.0.0.0')
124 124 return _filters(ip)
125 125
126 126
127 127 def get_server_ip_addr(environ, log_errors=True):
128 128 hostname = environ.get('SERVER_NAME')
129 129 try:
130 130 return socket.gethostbyname(hostname)
131 131 except Exception as e:
132 132 if log_errors:
133 133 # in some cases this lookup is not possible, and we don't want to
134 134 # make it an exception in logs
135 135 log.exception('Could not retrieve server ip address: %s', e)
136 136 return hostname
137 137
138 138
139 139 def get_server_port(environ):
140 140 return environ.get('SERVER_PORT')
141 141
142 142
143 143 def get_access_path(environ):
144 144 path = environ.get('PATH_INFO')
145 145 org_req = environ.get('pylons.original_request')
146 146 if org_req:
147 147 path = org_req.environ.get('PATH_INFO')
148 148 return path
149 149
150 150
151 151 def get_user_agent(environ):
152 152 return environ.get('HTTP_USER_AGENT')
153 153
154 154
155 155 def vcs_operation_context(
156 156 environ, repo_name, username, action, scm, check_locking=True,
157 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 158 """
159 159 Generate the context for a vcs operation, e.g. push or pull.
160 160
161 161 This context is passed over the layers so that hooks triggered by the
162 162 vcs operation know details like the user, the user's IP address etc.
163 163
164 164 :param check_locking: Allows to switch of the computation of the locking
165 165 data. This serves mainly the need of the simplevcs middleware to be
166 166 able to disable this for certain operations.
167 167
168 168 """
169 169 # Tri-state value: False: unlock, None: nothing, True: lock
170 170 make_lock = None
171 171 locked_by = [None, None, None]
172 172 is_anonymous = username == User.DEFAULT_USER
173 173 user = User.get_by_username(username)
174 174 if not is_anonymous and check_locking:
175 175 log.debug('Checking locking on repository "%s"', repo_name)
176 176 repo = Repository.get_by_repo_name(repo_name)
177 177 make_lock, __, locked_by = repo.get_locking_state(
178 178 action, user.user_id)
179 179 user_id = user.user_id
180 180 settings_model = VcsSettingsModel(repo=repo_name)
181 181 ui_settings = settings_model.get_ui_settings()
182 182
183 183 # NOTE(marcink): This should be also in sync with
184 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 185 store = [x for x in ui_settings if x.key == '/']
186 186 repo_store = ''
187 187 if store:
188 188 repo_store = store[0].value
189 189
190 190 scm_data = {
191 191 'ip': get_ip_addr(environ),
192 192 'username': username,
193 193 'user_id': user_id,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'repo_store': repo_store,
199 199 'make_lock': make_lock,
200 200 'locked_by': locked_by,
201 201 'server_url': utils2.get_server_url(environ),
202 202 'user_agent': get_user_agent(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 'detect_force_push': detect_force_push,
206 206 'check_branch_perms': check_branch_perms,
207 207 }
208 208 return scm_data
209 209
210 210
211 211 class BasicAuth(AuthBasicAuthenticator):
212 212
213 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 initial_call_detection=False, acl_repo_name=None):
214 initial_call_detection=False, acl_repo_name=None, rc_realm=''):
215 215 self.realm = realm
216 self.rc_realm = rc_realm
216 217 self.initial_call = initial_call_detection
217 218 self.authfunc = authfunc
218 219 self.registry = registry
219 220 self.acl_repo_name = acl_repo_name
220 221 self._rc_auth_http_code = auth_http_code
221 222
222 223 def _get_response_from_code(self, http_code):
223 224 try:
224 225 return get_exception(safe_int(http_code))
225 226 except Exception:
226 227 log.exception('Failed to fetch response for code %s', http_code)
227 228 return HTTPForbidden
228 229
229 230 def get_rc_realm(self):
230 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
231 return safe_str(self.rc_realm)
231 232
232 233 def build_authentication(self):
233 234 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 235 if self._rc_auth_http_code and not self.initial_call:
235 236 # return alternative HTTP code if alternative http return code
236 237 # is specified in RhodeCode config, but ONLY if it's not the
237 238 # FIRST call
238 239 custom_response_klass = self._get_response_from_code(
239 240 self._rc_auth_http_code)
240 241 return custom_response_klass(headers=head)
241 242 return HTTPUnauthorized(headers=head)
242 243
243 244 def authenticate(self, environ):
244 245 authorization = AUTHORIZATION(environ)
245 246 if not authorization:
246 247 return self.build_authentication()
247 248 (authmeth, auth) = authorization.split(' ', 1)
248 249 if 'basic' != authmeth.lower():
249 250 return self.build_authentication()
250 251 auth = auth.strip().decode('base64')
251 252 _parts = auth.split(':', 1)
252 253 if len(_parts) == 2:
253 254 username, password = _parts
254 255 auth_data = self.authfunc(
255 256 username, password, environ, VCS_TYPE,
256 257 registry=self.registry, acl_repo_name=self.acl_repo_name)
257 258 if auth_data:
258 259 return {'username': username, 'auth_data': auth_data}
259 260 if username and password:
260 261 # we mark that we actually executed authentication once, at
261 262 # that point we can use the alternative auth code
262 263 self.initial_call = False
263 264
264 265 return self.build_authentication()
265 266
266 267 __call__ = authenticate
267 268
268 269
269 270 def calculate_version_hash(config):
270 271 return sha1(
271 272 config.get('beaker.session.secret', '') +
272 273 rhodecode.__version__)[:8]
273 274
274 275
275 276 def get_current_lang(request):
276 277 # NOTE(marcink): remove after pyramid move
277 278 try:
278 279 return translation.get_lang()[0]
279 280 except:
280 281 pass
281 282
282 283 return getattr(request, '_LOCALE_', request.locale_name)
283 284
284 285
285 286 def attach_context_attributes(context, request, user_id=None):
286 287 """
287 288 Attach variables into template context called `c`.
288 289 """
289 290 config = request.registry.settings
290 291
291 292 rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
292 293 context.rc_config = rc_config
293 294 context.rhodecode_version = rhodecode.__version__
294 295 context.rhodecode_edition = config.get('rhodecode.edition')
295 296 # unique secret + version does not leak the version but keep consistency
296 297 context.rhodecode_version_hash = calculate_version_hash(config)
297 298
298 299 # Default language set for the incoming request
299 300 context.language = get_current_lang(request)
300 301
301 302 # Visual options
302 303 context.visual = AttributeDict({})
303 304
304 305 # DB stored Visual Items
305 306 context.visual.show_public_icon = str2bool(
306 307 rc_config.get('rhodecode_show_public_icon'))
307 308 context.visual.show_private_icon = str2bool(
308 309 rc_config.get('rhodecode_show_private_icon'))
309 310 context.visual.stylify_metatags = str2bool(
310 311 rc_config.get('rhodecode_stylify_metatags'))
311 312 context.visual.dashboard_items = safe_int(
312 313 rc_config.get('rhodecode_dashboard_items', 100))
313 314 context.visual.admin_grid_items = safe_int(
314 315 rc_config.get('rhodecode_admin_grid_items', 100))
315 316 context.visual.show_revision_number = str2bool(
316 317 rc_config.get('rhodecode_show_revision_number', True))
317 318 context.visual.show_sha_length = safe_int(
318 319 rc_config.get('rhodecode_show_sha_length', 100))
319 320 context.visual.repository_fields = str2bool(
320 321 rc_config.get('rhodecode_repository_fields'))
321 322 context.visual.show_version = str2bool(
322 323 rc_config.get('rhodecode_show_version'))
323 324 context.visual.use_gravatar = str2bool(
324 325 rc_config.get('rhodecode_use_gravatar'))
325 326 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
326 327 context.visual.default_renderer = rc_config.get(
327 328 'rhodecode_markup_renderer', 'rst')
328 329 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
329 330 context.visual.rhodecode_support_url = \
330 331 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
331 332
332 333 context.visual.affected_files_cut_off = 60
333 334
334 335 context.pre_code = rc_config.get('rhodecode_pre_code')
335 336 context.post_code = rc_config.get('rhodecode_post_code')
336 337 context.rhodecode_name = rc_config.get('rhodecode_title')
337 338 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
338 339 # if we have specified default_encoding in the request, it has more
339 340 # priority
340 341 if request.GET.get('default_encoding'):
341 342 context.default_encodings.insert(0, request.GET.get('default_encoding'))
342 343 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
343 344 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
344 345
345 346 # INI stored
346 347 context.labs_active = str2bool(
347 348 config.get('labs_settings_active', 'false'))
348 349 context.ssh_enabled = str2bool(
349 350 config.get('ssh.generate_authorized_keyfile', 'false'))
350 351 context.ssh_key_generator_enabled = str2bool(
351 352 config.get('ssh.enable_ui_key_generator', 'true'))
352 353
353 354 context.visual.allow_repo_location_change = str2bool(
354 355 config.get('allow_repo_location_change', True))
355 356 context.visual.allow_custom_hooks_settings = str2bool(
356 357 config.get('allow_custom_hooks_settings', True))
357 358 context.debug_style = str2bool(config.get('debug_style', False))
358 359
359 360 context.rhodecode_instanceid = config.get('instance_id')
360 361
361 362 context.visual.cut_off_limit_diff = safe_int(
362 363 config.get('cut_off_limit_diff'))
363 364 context.visual.cut_off_limit_file = safe_int(
364 365 config.get('cut_off_limit_file'))
365 366
366 367 context.license = AttributeDict({})
367 368 context.license.hide_license_info = str2bool(
368 369 config.get('license.hide_license_info', False))
369 370
370 371 # AppEnlight
371 372 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
372 373 context.appenlight_api_public_key = config.get(
373 374 'appenlight.api_public_key', '')
374 375 context.appenlight_server_url = config.get('appenlight.server_url', '')
375 376
376 377 diffmode = {
377 378 "unified": "unified",
378 379 "sideside": "sideside"
379 380 }.get(request.GET.get('diffmode'))
380 381
381 382 is_api = hasattr(request, 'rpc_user')
382 383 session_attrs = {
383 384 # defaults
384 385 "clone_url_format": "http",
385 386 "diffmode": "sideside"
386 387 }
387 388
388 389 if not is_api:
389 390 # don't access pyramid session for API calls
390 391 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
391 392 request.session['rc_user_session_attr.diffmode'] = diffmode
392 393
393 394 # session settings per user
394 395
395 396 for k, v in request.session.items():
396 397 pref = 'rc_user_session_attr.'
397 398 if k and k.startswith(pref):
398 399 k = k[len(pref):]
399 400 session_attrs[k] = v
400 401
401 402 context.user_session_attrs = session_attrs
402 403
403 404 # JS template context
404 405 context.template_context = {
405 406 'repo_name': None,
406 407 'repo_type': None,
407 408 'repo_landing_commit': None,
408 409 'rhodecode_user': {
409 410 'username': None,
410 411 'email': None,
411 412 'notification_status': False
412 413 },
413 414 'session_attrs': session_attrs,
414 415 'visual': {
415 416 'default_renderer': None
416 417 },
417 418 'commit_data': {
418 419 'commit_id': None
419 420 },
420 421 'pull_request_data': {'pull_request_id': None},
421 422 'timeago': {
422 423 'refresh_time': 120 * 1000,
423 424 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
424 425 },
425 426 'pyramid_dispatch': {
426 427
427 428 },
428 429 'extra': {'plugins': {}}
429 430 }
430 431 # END CONFIG VARS
431 432 if is_api:
432 433 csrf_token = None
433 434 else:
434 435 csrf_token = auth.get_csrf_token(session=request.session)
435 436
436 437 context.csrf_token = csrf_token
437 438 context.backends = rhodecode.BACKENDS.keys()
438 439 context.backends.sort()
439 440 unread_count = 0
440 441 user_bookmark_list = []
441 442 if user_id:
442 443 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
443 444 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
444 445 context.unread_notifications = unread_count
445 446 context.bookmark_items = user_bookmark_list
446 447
447 448 # web case
448 449 if hasattr(request, 'user'):
449 450 context.auth_user = request.user
450 451 context.rhodecode_user = request.user
451 452
452 453 # api case
453 454 if hasattr(request, 'rpc_user'):
454 455 context.auth_user = request.rpc_user
455 456 context.rhodecode_user = request.rpc_user
456 457
457 458 # attach the whole call context to the request
458 459 request.call_context = context
459 460
460 461
461 462 def get_auth_user(request):
462 463 environ = request.environ
463 464 session = request.session
464 465
465 466 ip_addr = get_ip_addr(environ)
466 467
467 468 # make sure that we update permissions each time we call controller
468 469 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
469 470 if not _auth_token and request.matchdict:
470 471 url_auth_token = request.matchdict.get('_auth_token')
471 472 _auth_token = url_auth_token
472 473 if _auth_token:
473 474 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
474 475
475 476 if _auth_token:
476 477 # when using API_KEY we assume user exists, and
477 478 # doesn't need auth based on cookies.
478 479 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
479 480 authenticated = False
480 481 else:
481 482 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
482 483 try:
483 484 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
484 485 ip_addr=ip_addr)
485 486 except UserCreationError as e:
486 487 h.flash(e, 'error')
487 488 # container auth or other auth functions that create users
488 489 # on the fly can throw this exception signaling that there's
489 490 # issue with user creation, explanation should be provided
490 491 # in Exception itself. We then create a simple blank
491 492 # AuthUser
492 493 auth_user = AuthUser(ip_addr=ip_addr)
493 494
494 495 # in case someone changes a password for user it triggers session
495 496 # flush and forces a re-login
496 497 if password_changed(auth_user, session):
497 498 session.invalidate()
498 499 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
499 500 auth_user = AuthUser(ip_addr=ip_addr)
500 501
501 502 authenticated = cookie_store.get('is_authenticated')
502 503
503 504 if not auth_user.is_authenticated and auth_user.is_user_object:
504 505 # user is not authenticated and not empty
505 506 auth_user.set_authenticated(authenticated)
506 507
507 508 return auth_user, _auth_token
508 509
509 510
510 511 def h_filter(s):
511 512 """
512 513 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
513 514 we wrap this with additional functionality that converts None to empty
514 515 strings
515 516 """
516 517 if s is None:
517 518 return markupsafe.Markup()
518 519 return markupsafe.escape(s)
519 520
520 521
521 522 def add_events_routes(config):
522 523 """
523 524 Adds routing that can be used in events. Because some events are triggered
524 525 outside of pyramid context, we need to bootstrap request with some
525 526 routing registered
526 527 """
527 528
528 529 from rhodecode.apps._base import ADMIN_PREFIX
529 530
530 531 config.add_route(name='home', pattern='/')
531 532 config.add_route(name='main_page_repos_data', pattern='/_home_repos')
532 533 config.add_route(name='main_page_repo_groups_data', pattern='/_home_repo_groups')
533 534
534 535 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
535 536 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
536 537 config.add_route(name='repo_summary', pattern='/{repo_name}')
537 538 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
538 539 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
539 540
540 541 config.add_route(name='pullrequest_show',
541 542 pattern='/{repo_name}/pull-request/{pull_request_id}')
542 543 config.add_route(name='pull_requests_global',
543 544 pattern='/pull-request/{pull_request_id}')
544 545
545 546 config.add_route(name='repo_commit',
546 547 pattern='/{repo_name}/changeset/{commit_id}')
547 548 config.add_route(name='repo_files',
548 549 pattern='/{repo_name}/files/{commit_id}/{f_path}')
549 550
550 551 config.add_route(name='hovercard_user',
551 552 pattern='/_hovercard/user/{user_id}')
552 553
553 554 config.add_route(name='hovercard_user_group',
554 555 pattern='/_hovercard/user_group/{user_group_id}')
555 556
556 557 config.add_route(name='hovercard_pull_request',
557 558 pattern='/_hovercard/pull_request/{pull_request_id}')
558 559
559 560 config.add_route(name='hovercard_repo_commit',
560 561 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
561 562
562 563
563 564 def bootstrap_config(request):
564 565 import pyramid.testing
565 566 registry = pyramid.testing.Registry('RcTestRegistry')
566 567
567 568 config = pyramid.testing.setUp(registry=registry, request=request)
568 569
569 570 # allow pyramid lookup in testing
570 571 config.include('pyramid_mako')
571 572 config.include('rhodecode.lib.rc_beaker')
572 573 config.include('rhodecode.lib.rc_cache')
573 574
574 575 add_events_routes(config)
575 576
576 577 return config
577 578
578 579
579 580 def bootstrap_request(**kwargs):
580 581 import pyramid.testing
581 582
582 583 class TestRequest(pyramid.testing.DummyRequest):
583 584 application_url = kwargs.pop('application_url', 'http://example.com')
584 585 host = kwargs.pop('host', 'example.com:80')
585 586 domain = kwargs.pop('domain', 'example.com')
586 587
587 588 def translate(self, msg):
588 589 return msg
589 590
590 591 def plularize(self, singular, plural, n):
591 592 return singular
592 593
593 594 def get_partial_renderer(self, tmpl_name):
594 595
595 596 from rhodecode.lib.partial_renderer import get_partial_renderer
596 597 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
597 598
598 599 _call_context = TemplateArgs()
599 600 _call_context.visual = TemplateArgs()
600 601 _call_context.visual.show_sha_length = 12
601 602 _call_context.visual.show_revision_number = True
602 603
603 604 @property
604 605 def call_context(self):
605 606 return self._call_context
606 607
607 608 class TestDummySession(pyramid.testing.DummySession):
608 609 def save(*arg, **kw):
609 610 pass
610 611
611 612 request = TestRequest(**kwargs)
612 613 request.session = TestDummySession()
613 614
614 615 return request
615 616
@@ -1,678 +1,679 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31 from StringIO import StringIO
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 49 from rhodecode.lib.middleware import appenlight
50 50 from rhodecode.lib.middleware.utils import scm_app_http
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def extract_svn_txn_id(acl_repo_name, data):
66 66 """
67 67 Helper method for extraction of svn txn_id from submitted XML data during
68 68 POST operations
69 69 """
70 70 try:
71 71 root = etree.fromstring(data)
72 72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 73 for el in root:
74 74 if el.tag == '{DAV:}source':
75 75 for sub_el in el:
76 76 if sub_el.tag == '{DAV:}href':
77 77 match = pat.search(sub_el.text)
78 78 if match:
79 79 svn_tx_id = match.groupdict()['txn_id']
80 80 txn_id = rc_cache.utils.compute_key_from_params(
81 81 acl_repo_name, svn_tx_id)
82 82 return txn_id
83 83 except Exception:
84 84 log.exception('Failed to extract txn_id')
85 85
86 86
87 87 def initialize_generator(factory):
88 88 """
89 89 Initializes the returned generator by draining its first element.
90 90
91 91 This can be used to give a generator an initializer, which is the code
92 92 up to the first yield statement. This decorator enforces that the first
93 93 produced element has the value ``"__init__"`` to make its special
94 94 purpose very explicit in the using code.
95 95 """
96 96
97 97 @wraps(factory)
98 98 def wrapper(*args, **kwargs):
99 99 gen = factory(*args, **kwargs)
100 100 try:
101 101 init = gen.next()
102 102 except StopIteration:
103 103 raise ValueError('Generator must yield at least one element.')
104 104 if init != "__init__":
105 105 raise ValueError('First yielded element must be "__init__".')
106 106 return gen
107 107 return wrapper
108 108
109 109
110 110 class SimpleVCS(object):
111 111 """Common functionality for SCM HTTP handlers."""
112 112
113 113 SCM = 'unknown'
114 114
115 115 acl_repo_name = None
116 116 url_repo_name = None
117 117 vcs_repo_name = None
118 118 rc_extras = {}
119 119
120 120 # We have to handle requests to shadow repositories different than requests
121 121 # to normal repositories. Therefore we have to distinguish them. To do this
122 122 # we use this regex which will match only on URLs pointing to shadow
123 123 # repositories.
124 124 shadow_repo_re = re.compile(
125 125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 126 '(?P<target>{slug_pat})/' # target repo
127 127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 128 'repository$' # shadow repo
129 129 .format(slug_pat=SLUG_RE.pattern))
130 130
131 131 def __init__(self, config, registry):
132 132 self.registry = registry
133 133 self.config = config
134 134 # re-populated by specialized middleware
135 135 self.repo_vcs_config = base.Config()
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137 136
138 registry.rhodecode_settings = self.rhodecode_settings
137 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
138 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
139
139 140 # authenticate this VCS request using authfunc
140 141 auth_ret_code_detection = \
141 142 str2bool(self.config.get('auth_ret_code_detection', False))
142 143 self.authenticate = BasicAuth(
143 144 '', authenticate, registry, config.get('auth_ret_code'),
144 auth_ret_code_detection)
145 auth_ret_code_detection, rc_realm=realm)
145 146 self.ip_addr = '0.0.0.0'
146 147
147 148 @LazyProperty
148 149 def global_vcs_config(self):
149 150 try:
150 151 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 152 except Exception:
152 153 return base.Config()
153 154
154 155 @property
155 156 def base_path(self):
156 157 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
157 158
158 159 if not settings_path:
159 160 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
160 161
161 162 if not settings_path:
162 163 # try, maybe we passed in explicitly as config option
163 164 settings_path = self.config.get('base_path')
164 165
165 166 if not settings_path:
166 167 raise ValueError('FATAL: base_path is empty')
167 168 return settings_path
168 169
169 170 def set_repo_names(self, environ):
170 171 """
171 172 This will populate the attributes acl_repo_name, url_repo_name,
172 173 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
173 174 shadow) repositories all names are equal. In case of requests to a
174 175 shadow repository the acl-name points to the target repo of the pull
175 176 request and the vcs-name points to the shadow repo file system path.
176 177 The url-name is always the URL used by the vcs client program.
177 178
178 179 Example in case of a shadow repo:
179 180 acl_repo_name = RepoGroup/MyRepo
180 181 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
181 182 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
182 183 """
183 184 # First we set the repo name from URL for all attributes. This is the
184 185 # default if handling normal (non shadow) repo requests.
185 186 self.url_repo_name = self._get_repository_name(environ)
186 187 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
187 188 self.is_shadow_repo = False
188 189
189 190 # Check if this is a request to a shadow repository.
190 191 match = self.shadow_repo_re.match(self.url_repo_name)
191 192 if match:
192 193 match_dict = match.groupdict()
193 194
194 195 # Build acl repo name from regex match.
195 196 acl_repo_name = safe_unicode('{groups}{target}'.format(
196 197 groups=match_dict['groups'] or '',
197 198 target=match_dict['target']))
198 199
199 200 # Retrieve pull request instance by ID from regex match.
200 201 pull_request = PullRequest.get(match_dict['pr_id'])
201 202
202 203 # Only proceed if we got a pull request and if acl repo name from
203 204 # URL equals the target repo name of the pull request.
204 205 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
205 206
206 207 # Get file system path to shadow repository.
207 208 workspace_id = PullRequestModel()._workspace_id(pull_request)
208 209 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
209 210
210 211 # Store names for later usage.
211 212 self.vcs_repo_name = vcs_repo_name
212 213 self.acl_repo_name = acl_repo_name
213 214 self.is_shadow_repo = True
214 215
215 216 log.debug('Setting all VCS repository names: %s', {
216 217 'acl_repo_name': self.acl_repo_name,
217 218 'url_repo_name': self.url_repo_name,
218 219 'vcs_repo_name': self.vcs_repo_name,
219 220 })
220 221
221 222 @property
222 223 def scm_app(self):
223 224 custom_implementation = self.config['vcs.scm_app_implementation']
224 225 if custom_implementation == 'http':
225 226 log.debug('Using HTTP implementation of scm app.')
226 227 scm_app_impl = scm_app_http
227 228 else:
228 229 log.debug('Using custom implementation of scm_app: "{}"'.format(
229 230 custom_implementation))
230 231 scm_app_impl = importlib.import_module(custom_implementation)
231 232 return scm_app_impl
232 233
233 234 def _get_by_id(self, repo_name):
234 235 """
235 236 Gets a special pattern _<ID> from clone url and tries to replace it
236 237 with a repository_name for support of _<ID> non changeable urls
237 238 """
238 239
239 240 data = repo_name.split('/')
240 241 if len(data) >= 2:
241 242 from rhodecode.model.repo import RepoModel
242 243 by_id_match = RepoModel().get_repo_by_id(repo_name)
243 244 if by_id_match:
244 245 data[1] = by_id_match.repo_name
245 246
246 247 return safe_str('/'.join(data))
247 248
248 249 def _invalidate_cache(self, repo_name):
249 250 """
250 251 Set's cache for this repository for invalidation on next access
251 252
252 253 :param repo_name: full repo name, also a cache key
253 254 """
254 255 ScmModel().mark_for_invalidation(repo_name)
255 256
256 257 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
257 258 db_repo = Repository.get_by_repo_name(repo_name)
258 259 if not db_repo:
259 260 log.debug('Repository `%s` not found inside the database.',
260 261 repo_name)
261 262 return False
262 263
263 264 if db_repo.repo_type != scm_type:
264 265 log.warning(
265 266 'Repository `%s` have incorrect scm_type, expected %s got %s',
266 267 repo_name, db_repo.repo_type, scm_type)
267 268 return False
268 269
269 270 config = db_repo._config
270 271 config.set('extensions', 'largefiles', '')
271 272 return is_valid_repo(
272 273 repo_name, base_path,
273 274 explicit_scm=scm_type, expect_scm=scm_type, config=config)
274 275
275 276 def valid_and_active_user(self, user):
276 277 """
277 278 Checks if that user is not empty, and if it's actually object it checks
278 279 if he's active.
279 280
280 281 :param user: user object or None
281 282 :return: boolean
282 283 """
283 284 if user is None:
284 285 return False
285 286
286 287 elif user.active:
287 288 return True
288 289
289 290 return False
290 291
291 292 @property
292 293 def is_shadow_repo_dir(self):
293 294 return os.path.isdir(self.vcs_repo_name)
294 295
295 296 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
296 297 plugin_id='', plugin_cache_active=False, cache_ttl=0):
297 298 """
298 299 Checks permissions using action (push/pull) user and repository
299 300 name. If plugin_cache and ttl is set it will use the plugin which
300 301 authenticated the user to store the cached permissions result for N
301 302 amount of seconds as in cache_ttl
302 303
303 304 :param action: push or pull action
304 305 :param user: user instance
305 306 :param repo_name: repository name
306 307 """
307 308
308 309 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
309 310 plugin_id, plugin_cache_active, cache_ttl)
310 311
311 312 user_id = user.user_id
312 313 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
313 314 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
314 315
315 316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
316 317 expiration_time=cache_ttl,
317 318 condition=plugin_cache_active)
318 319 def compute_perm_vcs(
319 320 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
320 321
321 322 log.debug('auth: calculating permission access now...')
322 323 # check IP
323 324 inherit = user.inherit_default_permissions
324 325 ip_allowed = AuthUser.check_ip_allowed(
325 326 user_id, ip_addr, inherit_from_default=inherit)
326 327 if ip_allowed:
327 328 log.info('Access for IP:%s allowed', ip_addr)
328 329 else:
329 330 return False
330 331
331 332 if action == 'push':
332 333 perms = ('repository.write', 'repository.admin')
333 334 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
334 335 return False
335 336
336 337 else:
337 338 # any other action need at least read permission
338 339 perms = (
339 340 'repository.read', 'repository.write', 'repository.admin')
340 341 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
341 342 return False
342 343
343 344 return True
344 345
345 346 start = time.time()
346 347 log.debug('Running plugin `%s` permissions check', plugin_id)
347 348
348 349 # for environ based auth, password can be empty, but then the validation is
349 350 # on the server that fills in the env data needed for authentication
350 351 perm_result = compute_perm_vcs(
351 352 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
352 353
353 354 auth_time = time.time() - start
354 355 log.debug('Permissions for plugin `%s` completed in %.4fs, '
355 356 'expiration time of fetched cache %.1fs.',
356 357 plugin_id, auth_time, cache_ttl)
357 358
358 359 return perm_result
359 360
360 361 def _get_http_scheme(self, environ):
361 362 try:
362 363 return environ['wsgi.url_scheme']
363 364 except Exception:
364 365 log.exception('Failed to read http scheme')
365 366 return 'http'
366 367
367 368 def _check_ssl(self, environ, start_response):
368 369 """
369 370 Checks the SSL check flag and returns False if SSL is not present
370 371 and required True otherwise
371 372 """
372 373 org_proto = environ['wsgi._org_proto']
373 374 # check if we have SSL required ! if not it's a bad request !
374 375 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
375 376 if require_ssl and org_proto == 'http':
376 377 log.debug(
377 378 'Bad request: detected protocol is `%s` and '
378 379 'SSL/HTTPS is required.', org_proto)
379 380 return False
380 381 return True
381 382
382 383 def _get_default_cache_ttl(self):
383 384 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
384 385 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
385 386 plugin_settings = plugin.get_settings()
386 387 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
387 388 plugin_settings) or (False, 0)
388 389 return plugin_cache_active, cache_ttl
389 390
390 391 def __call__(self, environ, start_response):
391 392 try:
392 393 return self._handle_request(environ, start_response)
393 394 except Exception:
394 395 log.exception("Exception while handling request")
395 396 appenlight.track_exception(environ)
396 397 return HTTPInternalServerError()(environ, start_response)
397 398 finally:
398 399 meta.Session.remove()
399 400
400 401 def _handle_request(self, environ, start_response):
401 402 if not self._check_ssl(environ, start_response):
402 403 reason = ('SSL required, while RhodeCode was unable '
403 404 'to detect this as SSL request')
404 405 log.debug('User not allowed to proceed, %s', reason)
405 406 return HTTPNotAcceptable(reason)(environ, start_response)
406 407
407 408 if not self.url_repo_name:
408 409 log.warning('Repository name is empty: %s', self.url_repo_name)
409 410 # failed to get repo name, we fail now
410 411 return HTTPNotFound()(environ, start_response)
411 412 log.debug('Extracted repo name is %s', self.url_repo_name)
412 413
413 414 ip_addr = get_ip_addr(environ)
414 415 user_agent = get_user_agent(environ)
415 416 username = None
416 417
417 418 # skip passing error to error controller
418 419 environ['pylons.status_code_redirect'] = True
419 420
420 421 # ======================================================================
421 422 # GET ACTION PULL or PUSH
422 423 # ======================================================================
423 424 action = self._get_action(environ)
424 425
425 426 # ======================================================================
426 427 # Check if this is a request to a shadow repository of a pull request.
427 428 # In this case only pull action is allowed.
428 429 # ======================================================================
429 430 if self.is_shadow_repo and action != 'pull':
430 431 reason = 'Only pull action is allowed for shadow repositories.'
431 432 log.debug('User not allowed to proceed, %s', reason)
432 433 return HTTPNotAcceptable(reason)(environ, start_response)
433 434
434 435 # Check if the shadow repo actually exists, in case someone refers
435 436 # to it, and it has been deleted because of successful merge.
436 437 if self.is_shadow_repo and not self.is_shadow_repo_dir:
437 438 log.debug(
438 439 'Shadow repo detected, and shadow repo dir `%s` is missing',
439 440 self.is_shadow_repo_dir)
440 441 return HTTPNotFound()(environ, start_response)
441 442
442 443 # ======================================================================
443 444 # CHECK ANONYMOUS PERMISSION
444 445 # ======================================================================
445 446 detect_force_push = False
446 447 check_branch_perms = False
447 448 if action in ['pull', 'push']:
448 449 user_obj = anonymous_user = User.get_default_user()
449 450 auth_user = user_obj.AuthUser()
450 451 username = anonymous_user.username
451 452 if anonymous_user.active:
452 453 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
453 454 # ONLY check permissions if the user is activated
454 455 anonymous_perm = self._check_permission(
455 456 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
456 457 plugin_id='anonymous_access',
457 458 plugin_cache_active=plugin_cache_active,
458 459 cache_ttl=cache_ttl,
459 460 )
460 461 else:
461 462 anonymous_perm = False
462 463
463 464 if not anonymous_user.active or not anonymous_perm:
464 465 if not anonymous_user.active:
465 466 log.debug('Anonymous access is disabled, running '
466 467 'authentication')
467 468
468 469 if not anonymous_perm:
469 470 log.debug('Not enough credentials to access this '
470 471 'repository as anonymous user')
471 472
472 473 username = None
473 474 # ==============================================================
474 475 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
475 476 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
476 477 # ==============================================================
477 478
478 479 # try to auth based on environ, container auth methods
479 480 log.debug('Running PRE-AUTH for container based authentication')
480 481 pre_auth = authenticate(
481 482 '', '', environ, VCS_TYPE, registry=self.registry,
482 483 acl_repo_name=self.acl_repo_name)
483 484 if pre_auth and pre_auth.get('username'):
484 485 username = pre_auth['username']
485 486 log.debug('PRE-AUTH got %s as username', username)
486 487 if pre_auth:
487 488 log.debug('PRE-AUTH successful from %s',
488 489 pre_auth.get('auth_data', {}).get('_plugin'))
489 490
490 491 # If not authenticated by the container, running basic auth
491 492 # before inject the calling repo_name for special scope checks
492 493 self.authenticate.acl_repo_name = self.acl_repo_name
493 494
494 495 plugin_cache_active, cache_ttl = False, 0
495 496 plugin = None
496 497 if not username:
497 498 self.authenticate.realm = self.authenticate.get_rc_realm()
498 499
499 500 try:
500 501 auth_result = self.authenticate(environ)
501 502 except (UserCreationError, NotAllowedToCreateUserError) as e:
502 503 log.error(e)
503 504 reason = safe_str(e)
504 505 return HTTPNotAcceptable(reason)(environ, start_response)
505 506
506 507 if isinstance(auth_result, dict):
507 508 AUTH_TYPE.update(environ, 'basic')
508 509 REMOTE_USER.update(environ, auth_result['username'])
509 510 username = auth_result['username']
510 511 plugin = auth_result.get('auth_data', {}).get('_plugin')
511 512 log.info(
512 513 'MAIN-AUTH successful for user `%s` from %s plugin',
513 514 username, plugin)
514 515
515 516 plugin_cache_active, cache_ttl = auth_result.get(
516 517 'auth_data', {}).get('_ttl_cache') or (False, 0)
517 518 else:
518 519 return auth_result.wsgi_application(environ, start_response)
519 520
520 521 # ==============================================================
521 522 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
522 523 # ==============================================================
523 524 user = User.get_by_username(username)
524 525 if not self.valid_and_active_user(user):
525 526 return HTTPForbidden()(environ, start_response)
526 527 username = user.username
527 528 user_id = user.user_id
528 529
529 530 # check user attributes for password change flag
530 531 user_obj = user
531 532 auth_user = user_obj.AuthUser()
532 533 if user_obj and user_obj.username != User.DEFAULT_USER and \
533 534 user_obj.user_data.get('force_password_change'):
534 535 reason = 'password change required'
535 536 log.debug('User not allowed to authenticate, %s', reason)
536 537 return HTTPNotAcceptable(reason)(environ, start_response)
537 538
538 539 # check permissions for this repository
539 540 perm = self._check_permission(
540 541 action, user, auth_user, self.acl_repo_name, ip_addr,
541 542 plugin, plugin_cache_active, cache_ttl)
542 543 if not perm:
543 544 return HTTPForbidden()(environ, start_response)
544 545 environ['rc_auth_user_id'] = user_id
545 546
546 547 if action == 'push':
547 548 perms = auth_user.get_branch_permissions(self.acl_repo_name)
548 549 if perms:
549 550 check_branch_perms = True
550 551 detect_force_push = True
551 552
552 553 # extras are injected into UI object and later available
553 554 # in hooks executed by RhodeCode
554 555 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
555 556
556 557 extras = vcs_operation_context(
557 558 environ, repo_name=self.acl_repo_name, username=username,
558 559 action=action, scm=self.SCM, check_locking=check_locking,
559 560 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
560 561 detect_force_push=detect_force_push
561 562 )
562 563
563 564 # ======================================================================
564 565 # REQUEST HANDLING
565 566 # ======================================================================
566 567 repo_path = os.path.join(
567 568 safe_str(self.base_path), safe_str(self.vcs_repo_name))
568 569 log.debug('Repository path is %s', repo_path)
569 570
570 571 fix_PATH()
571 572
572 573 log.info(
573 574 '%s action on %s repo "%s" by "%s" from %s %s',
574 575 action, self.SCM, safe_str(self.url_repo_name),
575 576 safe_str(username), ip_addr, user_agent)
576 577
577 578 return self._generate_vcs_response(
578 579 environ, start_response, repo_path, extras, action)
579 580
580 581 @initialize_generator
581 582 def _generate_vcs_response(
582 583 self, environ, start_response, repo_path, extras, action):
583 584 """
584 585 Returns a generator for the response content.
585 586
586 587 This method is implemented as a generator, so that it can trigger
587 588 the cache validation after all content sent back to the client. It
588 589 also handles the locking exceptions which will be triggered when
589 590 the first chunk is produced by the underlying WSGI application.
590 591 """
591 592 txn_id = ''
592 593 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
593 594 # case for SVN, we want to re-use the callback daemon port
594 595 # so we use the txn_id, for this we peek the body, and still save
595 596 # it as wsgi.input
596 597 data = environ['wsgi.input'].read()
597 598 environ['wsgi.input'] = StringIO(data)
598 599 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
599 600
600 601 callback_daemon, extras = self._prepare_callback_daemon(
601 602 extras, environ, action, txn_id=txn_id)
602 603 log.debug('HOOKS extras is %s', extras)
603 604
604 605 http_scheme = self._get_http_scheme(environ)
605 606
606 607 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
607 608 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
608 609 with callback_daemon:
609 610 app.rc_extras = extras
610 611
611 612 try:
612 613 response = app(environ, start_response)
613 614 finally:
614 615 # This statement works together with the decorator
615 616 # "initialize_generator" above. The decorator ensures that
616 617 # we hit the first yield statement before the generator is
617 618 # returned back to the WSGI server. This is needed to
618 619 # ensure that the call to "app" above triggers the
619 620 # needed callback to "start_response" before the
620 621 # generator is actually used.
621 622 yield "__init__"
622 623
623 624 # iter content
624 625 for chunk in response:
625 626 yield chunk
626 627
627 628 try:
628 629 # invalidate cache on push
629 630 if action == 'push':
630 631 self._invalidate_cache(self.url_repo_name)
631 632 finally:
632 633 meta.Session.remove()
633 634
634 635 def _get_repository_name(self, environ):
635 636 """Get repository name out of the environmnent
636 637
637 638 :param environ: WSGI environment
638 639 """
639 640 raise NotImplementedError()
640 641
641 642 def _get_action(self, environ):
642 643 """Map request commands into a pull or push command.
643 644
644 645 :param environ: WSGI environment
645 646 """
646 647 raise NotImplementedError()
647 648
648 649 def _create_wsgi_app(self, repo_path, repo_name, config):
649 650 """Return the WSGI app that will finally handle the request."""
650 651 raise NotImplementedError()
651 652
652 653 def _create_config(self, extras, repo_name, scheme='http'):
653 654 """Create a safe config representation."""
654 655 raise NotImplementedError()
655 656
656 657 def _should_use_callback_daemon(self, extras, environ, action):
657 658 if extras.get('is_shadow_repo'):
658 659 # we don't want to execute hooks, and callback daemon for shadow repos
659 660 return False
660 661 return True
661 662
662 663 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
663 664 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
664 665 if not self._should_use_callback_daemon(extras, environ, action):
665 666 # disable callback daemon for actions that don't require it
666 667 direct_calls = True
667 668
668 669 return prepare_callback_daemon(
669 670 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
670 671 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
671 672
672 673
673 674 def _should_check_locking(query_string):
674 675 # this is kind of hacky, but due to how mercurial handles client-server
675 676 # server see all operation on commit; bookmarks, phases and
676 677 # obsolescence marker in different transaction, we don't want to check
677 678 # locking on those
678 679 return query_string not in ['cmd=listkeys']
@@ -1,913 +1,920 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import hashlib
23 23 import logging
24 24 import re
25 25 from collections import namedtuple
26 26 from functools import wraps
27 27 import bleach
28 from pyramid.threadlocal import get_current_request
28 from pyramid.threadlocal import get_current_request, get_current_registry
29 29
30 30 from rhodecode.lib import rc_cache
31 31 from rhodecode.lib.utils2 import (
32 32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
33 33 from rhodecode.lib.vcs.backends import base
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import (
36 36 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting, CacheKey)
37 37 from rhodecode.model.meta import Session
38 38
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 UiSetting = namedtuple(
44 44 'UiSetting', ['section', 'key', 'value', 'active'])
45 45
46 46 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
47 47
48 48
49 49 class SettingNotFound(Exception):
50 50 def __init__(self, setting_id):
51 51 msg = 'Setting `{}` is not found'.format(setting_id)
52 52 super(SettingNotFound, self).__init__(msg)
53 53
54 54
55 55 class SettingsModel(BaseModel):
56 56 BUILTIN_HOOKS = (
57 57 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
58 58 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
59 59 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
60 60 RhodeCodeUi.HOOK_PUSH_KEY,)
61 61 HOOKS_SECTION = 'hooks'
62 62
63 63 def __init__(self, sa=None, repo=None):
64 64 self.repo = repo
65 65 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
66 66 self.SettingsDbModel = (
67 67 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
68 68 super(SettingsModel, self).__init__(sa)
69 69
70 70 def get_ui_by_key(self, key):
71 71 q = self.UiDbModel.query()
72 72 q = q.filter(self.UiDbModel.ui_key == key)
73 73 q = self._filter_by_repo(RepoRhodeCodeUi, q)
74 74 return q.scalar()
75 75
76 76 def get_ui_by_section(self, section):
77 77 q = self.UiDbModel.query()
78 78 q = q.filter(self.UiDbModel.ui_section == section)
79 79 q = self._filter_by_repo(RepoRhodeCodeUi, q)
80 80 return q.all()
81 81
82 82 def get_ui_by_section_and_key(self, section, key):
83 83 q = self.UiDbModel.query()
84 84 q = q.filter(self.UiDbModel.ui_section == section)
85 85 q = q.filter(self.UiDbModel.ui_key == key)
86 86 q = self._filter_by_repo(RepoRhodeCodeUi, q)
87 87 return q.scalar()
88 88
89 89 def get_ui(self, section=None, key=None):
90 90 q = self.UiDbModel.query()
91 91 q = self._filter_by_repo(RepoRhodeCodeUi, q)
92 92
93 93 if section:
94 94 q = q.filter(self.UiDbModel.ui_section == section)
95 95 if key:
96 96 q = q.filter(self.UiDbModel.ui_key == key)
97 97
98 98 # TODO: mikhail: add caching
99 99 result = [
100 100 UiSetting(
101 101 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
102 102 value=safe_str(r.ui_value), active=r.ui_active
103 103 )
104 104 for r in q.all()
105 105 ]
106 106 return result
107 107
108 108 def get_builtin_hooks(self):
109 109 q = self.UiDbModel.query()
110 110 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
111 111 return self._get_hooks(q)
112 112
113 113 def get_custom_hooks(self):
114 114 q = self.UiDbModel.query()
115 115 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
116 116 return self._get_hooks(q)
117 117
118 118 def create_ui_section_value(self, section, val, key=None, active=True):
119 119 new_ui = self.UiDbModel()
120 120 new_ui.ui_section = section
121 121 new_ui.ui_value = val
122 122 new_ui.ui_active = active
123 123
124 124 repository_id = ''
125 125 if self.repo:
126 126 repo = self._get_repo(self.repo)
127 127 repository_id = repo.repo_id
128 128 new_ui.repository_id = repository_id
129 129
130 130 if not key:
131 131 # keys are unique so they need appended info
132 132 if self.repo:
133 133 key = hashlib.sha1(
134 134 '{}{}{}'.format(section, val, repository_id)).hexdigest()
135 135 else:
136 136 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
137 137
138 138 new_ui.ui_key = key
139 139
140 140 Session().add(new_ui)
141 141 return new_ui
142 142
143 143 def create_or_update_hook(self, key, value):
144 144 ui = (
145 145 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
146 146 self.UiDbModel())
147 147 ui.ui_section = self.HOOKS_SECTION
148 148 ui.ui_active = True
149 149 ui.ui_key = key
150 150 ui.ui_value = value
151 151
152 152 if self.repo:
153 153 repo = self._get_repo(self.repo)
154 154 repository_id = repo.repo_id
155 155 ui.repository_id = repository_id
156 156
157 157 Session().add(ui)
158 158 return ui
159 159
160 160 def delete_ui(self, id_):
161 161 ui = self.UiDbModel.get(id_)
162 162 if not ui:
163 163 raise SettingNotFound(id_)
164 164 Session().delete(ui)
165 165
166 166 def get_setting_by_name(self, name):
167 167 q = self._get_settings_query()
168 168 q = q.filter(self.SettingsDbModel.app_settings_name == name)
169 169 return q.scalar()
170 170
171 171 def create_or_update_setting(
172 172 self, name, val=Optional(''), type_=Optional('unicode')):
173 173 """
174 174 Creates or updates RhodeCode setting. If updates is triggered it will
175 175 only update parameters that are explicityl set Optional instance will
176 176 be skipped
177 177
178 178 :param name:
179 179 :param val:
180 180 :param type_:
181 181 :return:
182 182 """
183 183
184 184 res = self.get_setting_by_name(name)
185 185 repo = self._get_repo(self.repo) if self.repo else None
186 186
187 187 if not res:
188 188 val = Optional.extract(val)
189 189 type_ = Optional.extract(type_)
190 190
191 191 args = (
192 192 (repo.repo_id, name, val, type_)
193 193 if repo else (name, val, type_))
194 194 res = self.SettingsDbModel(*args)
195 195
196 196 else:
197 197 if self.repo:
198 198 res.repository_id = repo.repo_id
199 199
200 200 res.app_settings_name = name
201 201 if not isinstance(type_, Optional):
202 202 # update if set
203 203 res.app_settings_type = type_
204 204 if not isinstance(val, Optional):
205 205 # update if set
206 206 res.app_settings_value = val
207 207
208 208 Session().add(res)
209 209 return res
210 210
211 211 def invalidate_settings_cache(self):
212 212 invalidation_namespace = CacheKey.SETTINGS_INVALIDATION_NAMESPACE
213 213 CacheKey.set_invalidate(invalidation_namespace)
214 214
215 215 def get_all_settings(self, cache=False, from_request=True):
216 from rhodecode.authentication.base import get_authn_registry
217
216 218 # defines if we use GLOBAL, or PER_REPO
217 219 repo = self._get_repo(self.repo) if self.repo else None
218 220 key = "settings_repo.{}".format(repo.repo_id) if repo else "settings_app"
219 221
220 222 # initially try the requests context, this is the fastest
221 223 # we only fetch global config
222 224 if from_request:
223 225 request = get_current_request()
224 226
225 227 if request and not repo and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
226 228 rc_config = request.call_context.rc_config
227 229 if rc_config:
228 230 return rc_config
229 231
230 232 region = rc_cache.get_or_create_region('sql_cache_short')
231 233 invalidation_namespace = CacheKey.SETTINGS_INVALIDATION_NAMESPACE
232 234
233 235 @region.conditional_cache_on_arguments(condition=cache)
234 236 def _get_all_settings(name, key):
235 237 q = self._get_settings_query()
236 238 if not q:
237 239 raise Exception('Could not get application settings !')
238 240
239 241 settings = {
240 242 'rhodecode_' + result.app_settings_name: result.app_settings_value
241 243 for result in q
242 244 }
243 245 return settings
244 246
245 247 inv_context_manager = rc_cache.InvalidationContext(
246 248 uid='cache_settings', invalidation_namespace=invalidation_namespace)
247 249 with inv_context_manager as invalidation_context:
248 250 # check for stored invalidation signal, and maybe purge the cache
249 251 # before computing it again
250 252 if invalidation_context.should_invalidate():
251 253 # NOTE:(marcink) we flush the whole sql_cache_short region, because it
252 254 # reads different settings etc. It's little too much but those caches
253 255 # are anyway very short lived and it's a safest way.
254 256 region = rc_cache.get_or_create_region('sql_cache_short')
255 257 region.invalidate()
258 registry = get_current_registry()
259 if registry:
260 authn_registry = get_authn_registry(registry)
261 if authn_registry:
262 authn_registry.invalidate_plugins_for_auth()
256 263
257 264 result = _get_all_settings('rhodecode_settings', key)
258 265 log.debug('Fetching app settings for key: %s took: %.4fs', key,
259 266 inv_context_manager.compute_time)
260 267
261 268 return result
262 269
263 270 def get_auth_settings(self):
264 271 q = self._get_settings_query()
265 272 q = q.filter(
266 273 self.SettingsDbModel.app_settings_name.startswith('auth_'))
267 274 rows = q.all()
268 275 auth_settings = {
269 276 row.app_settings_name: row.app_settings_value for row in rows}
270 277 return auth_settings
271 278
272 279 def get_auth_plugins(self):
273 280 auth_plugins = self.get_setting_by_name("auth_plugins")
274 281 return auth_plugins.app_settings_value
275 282
276 283 def get_default_repo_settings(self, strip_prefix=False):
277 284 q = self._get_settings_query()
278 285 q = q.filter(
279 286 self.SettingsDbModel.app_settings_name.startswith('default_'))
280 287 rows = q.all()
281 288
282 289 result = {}
283 290 for row in rows:
284 291 key = row.app_settings_name
285 292 if strip_prefix:
286 293 key = remove_prefix(key, prefix='default_')
287 294 result.update({key: row.app_settings_value})
288 295 return result
289 296
290 297 def get_repo(self):
291 298 repo = self._get_repo(self.repo)
292 299 if not repo:
293 300 raise Exception(
294 301 'Repository `{}` cannot be found inside the database'.format(
295 302 self.repo))
296 303 return repo
297 304
298 305 def _filter_by_repo(self, model, query):
299 306 if self.repo:
300 307 repo = self.get_repo()
301 308 query = query.filter(model.repository_id == repo.repo_id)
302 309 return query
303 310
304 311 def _get_hooks(self, query):
305 312 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
306 313 query = self._filter_by_repo(RepoRhodeCodeUi, query)
307 314 return query.all()
308 315
309 316 def _get_settings_query(self):
310 317 q = self.SettingsDbModel.query()
311 318 return self._filter_by_repo(RepoRhodeCodeSetting, q)
312 319
313 320 def list_enabled_social_plugins(self, settings):
314 321 enabled = []
315 322 for plug in SOCIAL_PLUGINS_LIST:
316 323 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
317 324 )):
318 325 enabled.append(plug)
319 326 return enabled
320 327
321 328
322 329 def assert_repo_settings(func):
323 330 @wraps(func)
324 331 def _wrapper(self, *args, **kwargs):
325 332 if not self.repo_settings:
326 333 raise Exception('Repository is not specified')
327 334 return func(self, *args, **kwargs)
328 335 return _wrapper
329 336
330 337
331 338 class IssueTrackerSettingsModel(object):
332 339 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
333 340 SETTINGS_PREFIX = 'issuetracker_'
334 341
335 342 def __init__(self, sa=None, repo=None):
336 343 self.global_settings = SettingsModel(sa=sa)
337 344 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
338 345
339 346 @property
340 347 def inherit_global_settings(self):
341 348 if not self.repo_settings:
342 349 return True
343 350 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
344 351 return setting.app_settings_value if setting else True
345 352
346 353 @inherit_global_settings.setter
347 354 def inherit_global_settings(self, value):
348 355 if self.repo_settings:
349 356 settings = self.repo_settings.create_or_update_setting(
350 357 self.INHERIT_SETTINGS, value, type_='bool')
351 358 Session().add(settings)
352 359
353 360 def _get_keyname(self, key, uid, prefix=''):
354 361 return '{0}{1}{2}_{3}'.format(
355 362 prefix, self.SETTINGS_PREFIX, key, uid)
356 363
357 364 def _make_dict_for_settings(self, qs):
358 365 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
359 366
360 367 issuetracker_entries = {}
361 368 # create keys
362 369 for k, v in qs.items():
363 370 if k.startswith(prefix_match):
364 371 uid = k[len(prefix_match):]
365 372 issuetracker_entries[uid] = None
366 373
367 374 def url_cleaner(input_str):
368 375 input_str = input_str.replace('"', '').replace("'", '')
369 376 input_str = bleach.clean(input_str, strip=True)
370 377 return input_str
371 378
372 379 # populate
373 380 for uid in issuetracker_entries:
374 381 url_data = qs.get(self._get_keyname('url', uid, 'rhodecode_'))
375 382
376 383 pat = qs.get(self._get_keyname('pat', uid, 'rhodecode_'))
377 384 try:
378 385 pat_compiled = re.compile(r'%s' % pat)
379 386 except re.error:
380 387 pat_compiled = None
381 388
382 389 issuetracker_entries[uid] = AttributeDict({
383 390 'pat': pat,
384 391 'pat_compiled': pat_compiled,
385 392 'url': url_cleaner(
386 393 qs.get(self._get_keyname('url', uid, 'rhodecode_')) or ''),
387 394 'pref': bleach.clean(
388 395 qs.get(self._get_keyname('pref', uid, 'rhodecode_')) or ''),
389 396 'desc': qs.get(
390 397 self._get_keyname('desc', uid, 'rhodecode_')),
391 398 })
392 399
393 400 return issuetracker_entries
394 401
395 402 def get_global_settings(self, cache=False):
396 403 """
397 404 Returns list of global issue tracker settings
398 405 """
399 406 defaults = self.global_settings.get_all_settings(cache=cache)
400 407 settings = self._make_dict_for_settings(defaults)
401 408 return settings
402 409
403 410 def get_repo_settings(self, cache=False):
404 411 """
405 412 Returns list of issue tracker settings per repository
406 413 """
407 414 if not self.repo_settings:
408 415 raise Exception('Repository is not specified')
409 416 all_settings = self.repo_settings.get_all_settings(cache=cache)
410 417 settings = self._make_dict_for_settings(all_settings)
411 418 return settings
412 419
413 420 def get_settings(self, cache=False):
414 421 if self.inherit_global_settings:
415 422 return self.get_global_settings(cache=cache)
416 423 else:
417 424 return self.get_repo_settings(cache=cache)
418 425
419 426 def delete_entries(self, uid):
420 427 if self.repo_settings:
421 428 all_patterns = self.get_repo_settings()
422 429 settings_model = self.repo_settings
423 430 else:
424 431 all_patterns = self.get_global_settings()
425 432 settings_model = self.global_settings
426 433 entries = all_patterns.get(uid, [])
427 434
428 435 for del_key in entries:
429 436 setting_name = self._get_keyname(del_key, uid)
430 437 entry = settings_model.get_setting_by_name(setting_name)
431 438 if entry:
432 439 Session().delete(entry)
433 440
434 441 Session().commit()
435 442
436 443 def create_or_update_setting(
437 444 self, name, val=Optional(''), type_=Optional('unicode')):
438 445 if self.repo_settings:
439 446 setting = self.repo_settings.create_or_update_setting(
440 447 name, val, type_)
441 448 else:
442 449 setting = self.global_settings.create_or_update_setting(
443 450 name, val, type_)
444 451 return setting
445 452
446 453
447 454 class VcsSettingsModel(object):
448 455
449 456 INHERIT_SETTINGS = 'inherit_vcs_settings'
450 457 GENERAL_SETTINGS = (
451 458 'use_outdated_comments',
452 459 'pr_merge_enabled',
453 460 'hg_use_rebase_for_merging',
454 461 'hg_close_branch_before_merging',
455 462 'git_use_rebase_for_merging',
456 463 'git_close_branch_before_merging',
457 464 'diff_cache',
458 465 )
459 466
460 467 HOOKS_SETTINGS = (
461 468 ('hooks', 'changegroup.repo_size'),
462 469 ('hooks', 'changegroup.push_logger'),
463 470 ('hooks', 'outgoing.pull_logger'),
464 471 )
465 472 HG_SETTINGS = (
466 473 ('extensions', 'largefiles'),
467 474 ('phases', 'publish'),
468 475 ('extensions', 'evolve'),
469 476 ('extensions', 'topic'),
470 477 ('experimental', 'evolution'),
471 478 ('experimental', 'evolution.exchange'),
472 479 )
473 480 GIT_SETTINGS = (
474 481 ('vcs_git_lfs', 'enabled'),
475 482 )
476 483 GLOBAL_HG_SETTINGS = (
477 484 ('extensions', 'largefiles'),
478 485 ('largefiles', 'usercache'),
479 486 ('phases', 'publish'),
480 487 ('extensions', 'hgsubversion'),
481 488 ('extensions', 'evolve'),
482 489 ('extensions', 'topic'),
483 490 ('experimental', 'evolution'),
484 491 ('experimental', 'evolution.exchange'),
485 492 )
486 493
487 494 GLOBAL_GIT_SETTINGS = (
488 495 ('vcs_git_lfs', 'enabled'),
489 496 ('vcs_git_lfs', 'store_location')
490 497 )
491 498
492 499 GLOBAL_SVN_SETTINGS = (
493 500 ('vcs_svn_proxy', 'http_requests_enabled'),
494 501 ('vcs_svn_proxy', 'http_server_url')
495 502 )
496 503
497 504 SVN_BRANCH_SECTION = 'vcs_svn_branch'
498 505 SVN_TAG_SECTION = 'vcs_svn_tag'
499 506 SSL_SETTING = ('web', 'push_ssl')
500 507 PATH_SETTING = ('paths', '/')
501 508
502 509 def __init__(self, sa=None, repo=None):
503 510 self.global_settings = SettingsModel(sa=sa)
504 511 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
505 512 self._ui_settings = (
506 513 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
507 514 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
508 515
509 516 @property
510 517 @assert_repo_settings
511 518 def inherit_global_settings(self):
512 519 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
513 520 return setting.app_settings_value if setting else True
514 521
515 522 @inherit_global_settings.setter
516 523 @assert_repo_settings
517 524 def inherit_global_settings(self, value):
518 525 self.repo_settings.create_or_update_setting(
519 526 self.INHERIT_SETTINGS, value, type_='bool')
520 527
521 528 def get_global_svn_branch_patterns(self):
522 529 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
523 530
524 531 @assert_repo_settings
525 532 def get_repo_svn_branch_patterns(self):
526 533 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
527 534
528 535 def get_global_svn_tag_patterns(self):
529 536 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
530 537
531 538 @assert_repo_settings
532 539 def get_repo_svn_tag_patterns(self):
533 540 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
534 541
535 542 def get_global_settings(self):
536 543 return self._collect_all_settings(global_=True)
537 544
538 545 @assert_repo_settings
539 546 def get_repo_settings(self):
540 547 return self._collect_all_settings(global_=False)
541 548
542 549 @assert_repo_settings
543 550 def get_repo_settings_inherited(self):
544 551 global_settings = self.get_global_settings()
545 552 global_settings.update(self.get_repo_settings())
546 553 return global_settings
547 554
548 555 @assert_repo_settings
549 556 def create_or_update_repo_settings(
550 557 self, data, inherit_global_settings=False):
551 558 from rhodecode.model.scm import ScmModel
552 559
553 560 self.inherit_global_settings = inherit_global_settings
554 561
555 562 repo = self.repo_settings.get_repo()
556 563 if not inherit_global_settings:
557 564 if repo.repo_type == 'svn':
558 565 self.create_repo_svn_settings(data)
559 566 else:
560 567 self.create_or_update_repo_hook_settings(data)
561 568 self.create_or_update_repo_pr_settings(data)
562 569
563 570 if repo.repo_type == 'hg':
564 571 self.create_or_update_repo_hg_settings(data)
565 572
566 573 if repo.repo_type == 'git':
567 574 self.create_or_update_repo_git_settings(data)
568 575
569 576 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
570 577
571 578 @assert_repo_settings
572 579 def create_or_update_repo_hook_settings(self, data):
573 580 for section, key in self.HOOKS_SETTINGS:
574 581 data_key = self._get_form_ui_key(section, key)
575 582 if data_key not in data:
576 583 raise ValueError(
577 584 'The given data does not contain {} key'.format(data_key))
578 585
579 586 active = data.get(data_key)
580 587 repo_setting = self.repo_settings.get_ui_by_section_and_key(
581 588 section, key)
582 589 if not repo_setting:
583 590 global_setting = self.global_settings.\
584 591 get_ui_by_section_and_key(section, key)
585 592 self.repo_settings.create_ui_section_value(
586 593 section, global_setting.ui_value, key=key, active=active)
587 594 else:
588 595 repo_setting.ui_active = active
589 596 Session().add(repo_setting)
590 597
591 598 def update_global_hook_settings(self, data):
592 599 for section, key in self.HOOKS_SETTINGS:
593 600 data_key = self._get_form_ui_key(section, key)
594 601 if data_key not in data:
595 602 raise ValueError(
596 603 'The given data does not contain {} key'.format(data_key))
597 604 active = data.get(data_key)
598 605 repo_setting = self.global_settings.get_ui_by_section_and_key(
599 606 section, key)
600 607 repo_setting.ui_active = active
601 608 Session().add(repo_setting)
602 609
603 610 @assert_repo_settings
604 611 def create_or_update_repo_pr_settings(self, data):
605 612 return self._create_or_update_general_settings(
606 613 self.repo_settings, data)
607 614
608 615 def create_or_update_global_pr_settings(self, data):
609 616 return self._create_or_update_general_settings(
610 617 self.global_settings, data)
611 618
612 619 @assert_repo_settings
613 620 def create_repo_svn_settings(self, data):
614 621 return self._create_svn_settings(self.repo_settings, data)
615 622
616 623 def _set_evolution(self, settings, is_enabled):
617 624 if is_enabled:
618 625 # if evolve is active set evolution=all
619 626
620 627 self._create_or_update_ui(
621 628 settings, *('experimental', 'evolution'), value='all',
622 629 active=True)
623 630 self._create_or_update_ui(
624 631 settings, *('experimental', 'evolution.exchange'), value='yes',
625 632 active=True)
626 633 # if evolve is active set topics server support
627 634 self._create_or_update_ui(
628 635 settings, *('extensions', 'topic'), value='',
629 636 active=True)
630 637
631 638 else:
632 639 self._create_or_update_ui(
633 640 settings, *('experimental', 'evolution'), value='',
634 641 active=False)
635 642 self._create_or_update_ui(
636 643 settings, *('experimental', 'evolution.exchange'), value='no',
637 644 active=False)
638 645 self._create_or_update_ui(
639 646 settings, *('extensions', 'topic'), value='',
640 647 active=False)
641 648
642 649 @assert_repo_settings
643 650 def create_or_update_repo_hg_settings(self, data):
644 651 largefiles, phases, evolve = \
645 652 self.HG_SETTINGS[:3]
646 653 largefiles_key, phases_key, evolve_key = \
647 654 self._get_settings_keys(self.HG_SETTINGS[:3], data)
648 655
649 656 self._create_or_update_ui(
650 657 self.repo_settings, *largefiles, value='',
651 658 active=data[largefiles_key])
652 659 self._create_or_update_ui(
653 660 self.repo_settings, *evolve, value='',
654 661 active=data[evolve_key])
655 662 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
656 663
657 664 self._create_or_update_ui(
658 665 self.repo_settings, *phases, value=safe_str(data[phases_key]))
659 666
660 667 def create_or_update_global_hg_settings(self, data):
661 668 largefiles, largefiles_store, phases, hgsubversion, evolve \
662 669 = self.GLOBAL_HG_SETTINGS[:5]
663 670 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
664 671 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data)
665 672
666 673 self._create_or_update_ui(
667 674 self.global_settings, *largefiles, value='',
668 675 active=data[largefiles_key])
669 676 self._create_or_update_ui(
670 677 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
671 678 self._create_or_update_ui(
672 679 self.global_settings, *phases, value=safe_str(data[phases_key]))
673 680 self._create_or_update_ui(
674 681 self.global_settings, *hgsubversion, active=data[subversion_key])
675 682 self._create_or_update_ui(
676 683 self.global_settings, *evolve, value='',
677 684 active=data[evolve_key])
678 685 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
679 686
680 687 def create_or_update_repo_git_settings(self, data):
681 688 # NOTE(marcink): # comma makes unpack work properly
682 689 lfs_enabled, \
683 690 = self.GIT_SETTINGS
684 691
685 692 lfs_enabled_key, \
686 693 = self._get_settings_keys(self.GIT_SETTINGS, data)
687 694
688 695 self._create_or_update_ui(
689 696 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
690 697 active=data[lfs_enabled_key])
691 698
692 699 def create_or_update_global_git_settings(self, data):
693 700 lfs_enabled, lfs_store_location \
694 701 = self.GLOBAL_GIT_SETTINGS
695 702 lfs_enabled_key, lfs_store_location_key \
696 703 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
697 704
698 705 self._create_or_update_ui(
699 706 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
700 707 active=data[lfs_enabled_key])
701 708 self._create_or_update_ui(
702 709 self.global_settings, *lfs_store_location,
703 710 value=data[lfs_store_location_key])
704 711
705 712 def create_or_update_global_svn_settings(self, data):
706 713 # branch/tags patterns
707 714 self._create_svn_settings(self.global_settings, data)
708 715
709 716 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
710 717 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
711 718 self.GLOBAL_SVN_SETTINGS, data)
712 719
713 720 self._create_or_update_ui(
714 721 self.global_settings, *http_requests_enabled,
715 722 value=safe_str(data[http_requests_enabled_key]))
716 723 self._create_or_update_ui(
717 724 self.global_settings, *http_server_url,
718 725 value=data[http_server_url_key])
719 726
720 727 def update_global_ssl_setting(self, value):
721 728 self._create_or_update_ui(
722 729 self.global_settings, *self.SSL_SETTING, value=value)
723 730
724 731 def update_global_path_setting(self, value):
725 732 self._create_or_update_ui(
726 733 self.global_settings, *self.PATH_SETTING, value=value)
727 734
728 735 @assert_repo_settings
729 736 def delete_repo_svn_pattern(self, id_):
730 737 ui = self.repo_settings.UiDbModel.get(id_)
731 738 if ui and ui.repository.repo_name == self.repo_settings.repo:
732 739 # only delete if it's the same repo as initialized settings
733 740 self.repo_settings.delete_ui(id_)
734 741 else:
735 742 # raise error as if we wouldn't find this option
736 743 self.repo_settings.delete_ui(-1)
737 744
738 745 def delete_global_svn_pattern(self, id_):
739 746 self.global_settings.delete_ui(id_)
740 747
741 748 @assert_repo_settings
742 749 def get_repo_ui_settings(self, section=None, key=None):
743 750 global_uis = self.global_settings.get_ui(section, key)
744 751 repo_uis = self.repo_settings.get_ui(section, key)
745 752
746 753 filtered_repo_uis = self._filter_ui_settings(repo_uis)
747 754 filtered_repo_uis_keys = [
748 755 (s.section, s.key) for s in filtered_repo_uis]
749 756
750 757 def _is_global_ui_filtered(ui):
751 758 return (
752 759 (ui.section, ui.key) in filtered_repo_uis_keys
753 760 or ui.section in self._svn_sections)
754 761
755 762 filtered_global_uis = [
756 763 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
757 764
758 765 return filtered_global_uis + filtered_repo_uis
759 766
760 767 def get_global_ui_settings(self, section=None, key=None):
761 768 return self.global_settings.get_ui(section, key)
762 769
763 770 def get_ui_settings_as_config_obj(self, section=None, key=None):
764 771 config = base.Config()
765 772
766 773 ui_settings = self.get_ui_settings(section=section, key=key)
767 774
768 775 for entry in ui_settings:
769 776 config.set(entry.section, entry.key, entry.value)
770 777
771 778 return config
772 779
773 780 def get_ui_settings(self, section=None, key=None):
774 781 if not self.repo_settings or self.inherit_global_settings:
775 782 return self.get_global_ui_settings(section, key)
776 783 else:
777 784 return self.get_repo_ui_settings(section, key)
778 785
779 786 def get_svn_patterns(self, section=None):
780 787 if not self.repo_settings:
781 788 return self.get_global_ui_settings(section)
782 789 else:
783 790 return self.get_repo_ui_settings(section)
784 791
785 792 @assert_repo_settings
786 793 def get_repo_general_settings(self):
787 794 global_settings = self.global_settings.get_all_settings()
788 795 repo_settings = self.repo_settings.get_all_settings()
789 796 filtered_repo_settings = self._filter_general_settings(repo_settings)
790 797 global_settings.update(filtered_repo_settings)
791 798 return global_settings
792 799
793 800 def get_global_general_settings(self):
794 801 return self.global_settings.get_all_settings()
795 802
796 803 def get_general_settings(self):
797 804 if not self.repo_settings or self.inherit_global_settings:
798 805 return self.get_global_general_settings()
799 806 else:
800 807 return self.get_repo_general_settings()
801 808
802 809 def get_repos_location(self):
803 810 return self.global_settings.get_ui_by_key('/').ui_value
804 811
805 812 def _filter_ui_settings(self, settings):
806 813 filtered_settings = [
807 814 s for s in settings if self._should_keep_setting(s)]
808 815 return filtered_settings
809 816
810 817 def _should_keep_setting(self, setting):
811 818 keep = (
812 819 (setting.section, setting.key) in self._ui_settings or
813 820 setting.section in self._svn_sections)
814 821 return keep
815 822
816 823 def _filter_general_settings(self, settings):
817 824 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
818 825 return {
819 826 k: settings[k]
820 827 for k in settings if k in keys}
821 828
822 829 def _collect_all_settings(self, global_=False):
823 830 settings = self.global_settings if global_ else self.repo_settings
824 831 result = {}
825 832
826 833 for section, key in self._ui_settings:
827 834 ui = settings.get_ui_by_section_and_key(section, key)
828 835 result_key = self._get_form_ui_key(section, key)
829 836
830 837 if ui:
831 838 if section in ('hooks', 'extensions'):
832 839 result[result_key] = ui.ui_active
833 840 elif result_key in ['vcs_git_lfs_enabled']:
834 841 result[result_key] = ui.ui_active
835 842 else:
836 843 result[result_key] = ui.ui_value
837 844
838 845 for name in self.GENERAL_SETTINGS:
839 846 setting = settings.get_setting_by_name(name)
840 847 if setting:
841 848 result_key = 'rhodecode_{}'.format(name)
842 849 result[result_key] = setting.app_settings_value
843 850
844 851 return result
845 852
846 853 def _get_form_ui_key(self, section, key):
847 854 return '{section}_{key}'.format(
848 855 section=section, key=key.replace('.', '_'))
849 856
850 857 def _create_or_update_ui(
851 858 self, settings, section, key, value=None, active=None):
852 859 ui = settings.get_ui_by_section_and_key(section, key)
853 860 if not ui:
854 861 active = True if active is None else active
855 862 settings.create_ui_section_value(
856 863 section, value, key=key, active=active)
857 864 else:
858 865 if active is not None:
859 866 ui.ui_active = active
860 867 if value is not None:
861 868 ui.ui_value = value
862 869 Session().add(ui)
863 870
864 871 def _create_svn_settings(self, settings, data):
865 872 svn_settings = {
866 873 'new_svn_branch': self.SVN_BRANCH_SECTION,
867 874 'new_svn_tag': self.SVN_TAG_SECTION
868 875 }
869 876 for key in svn_settings:
870 877 if data.get(key):
871 878 settings.create_ui_section_value(svn_settings[key], data[key])
872 879
873 880 def _create_or_update_general_settings(self, settings, data):
874 881 for name in self.GENERAL_SETTINGS:
875 882 data_key = 'rhodecode_{}'.format(name)
876 883 if data_key not in data:
877 884 raise ValueError(
878 885 'The given data does not contain {} key'.format(data_key))
879 886 setting = settings.create_or_update_setting(
880 887 name, data[data_key], 'bool')
881 888 Session().add(setting)
882 889
883 890 def _get_settings_keys(self, settings, data):
884 891 data_keys = [self._get_form_ui_key(*s) for s in settings]
885 892 for data_key in data_keys:
886 893 if data_key not in data:
887 894 raise ValueError(
888 895 'The given data does not contain {} key'.format(data_key))
889 896 return data_keys
890 897
891 898 def create_largeobjects_dirs_if_needed(self, repo_store_path):
892 899 """
893 900 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
894 901 does a repository scan if enabled in the settings.
895 902 """
896 903
897 904 from rhodecode.lib.vcs.backends.hg import largefiles_store
898 905 from rhodecode.lib.vcs.backends.git import lfs_store
899 906
900 907 paths = [
901 908 largefiles_store(repo_store_path),
902 909 lfs_store(repo_store_path)]
903 910
904 911 for path in paths:
905 912 if os.path.isdir(path):
906 913 continue
907 914 if os.path.isfile(path):
908 915 continue
909 916 # not a file nor dir, we try to create it
910 917 try:
911 918 os.makedirs(path)
912 919 except Exception:
913 920 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,416 +1,417 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helpers for fixture generation
23 23 """
24 24
25 25 import os
26 26 import time
27 27 import tempfile
28 28 import shutil
29 29
30 30 import configobj
31 31
32 from rhodecode.model.settings import SettingsModel
32 33 from rhodecode.tests import *
33 34 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap
34 35 from rhodecode.model.meta import Session
35 36 from rhodecode.model.repo import RepoModel
36 37 from rhodecode.model.user import UserModel
37 38 from rhodecode.model.repo_group import RepoGroupModel
38 39 from rhodecode.model.user_group import UserGroupModel
39 40 from rhodecode.model.gist import GistModel
40 41 from rhodecode.model.auth_token import AuthTokenModel
41 42 from rhodecode.authentication.plugins.auth_rhodecode import \
42 43 RhodeCodeAuthPlugin
43 44
44 45 dn = os.path.dirname
45 46 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures')
46 47
47 48
48 49 def error_function(*args, **kwargs):
49 50 raise Exception('Total Crash !')
50 51
51 52
52 53 class TestINI(object):
53 54 """
54 55 Allows to create a new test.ini file as a copy of existing one with edited
55 56 data. Example usage::
56 57
57 58 with TestINI('test.ini', [{'section':{'key':val'}]) as new_test_ini_path:
58 59 print('paster server %s' % new_test_ini)
59 60 """
60 61
61 62 def __init__(self, ini_file_path, ini_params, new_file_prefix='DEFAULT',
62 63 destroy=True, dir=None):
63 64 self.ini_file_path = ini_file_path
64 65 self.ini_params = ini_params
65 66 self.new_path = None
66 67 self.new_path_prefix = new_file_prefix
67 68 self._destroy = destroy
68 69 self._dir = dir
69 70
70 71 def __enter__(self):
71 72 return self.create()
72 73
73 74 def __exit__(self, exc_type, exc_val, exc_tb):
74 75 self.destroy()
75 76
76 77 def create(self):
77 78 config = configobj.ConfigObj(
78 79 self.ini_file_path, file_error=True, write_empty_values=True)
79 80
80 81 for data in self.ini_params:
81 82 section, ini_params = data.items()[0]
82 83 for key, val in ini_params.items():
83 84 config[section][key] = val
84 85 with tempfile.NamedTemporaryFile(
85 86 prefix=self.new_path_prefix, suffix='.ini', dir=self._dir,
86 87 delete=False) as new_ini_file:
87 88 config.write(new_ini_file)
88 89 self.new_path = new_ini_file.name
89 90
90 91 return self.new_path
91 92
92 93 def destroy(self):
93 94 if self._destroy:
94 95 os.remove(self.new_path)
95 96
96 97
97 98 class Fixture(object):
98 99
99 100 def anon_access(self, status):
100 101 """
101 102 Context process for disabling anonymous access. use like:
102 103 fixture = Fixture()
103 104 with fixture.anon_access(False):
104 105 #tests
105 106
106 107 after this block anon access will be set to `not status`
107 108 """
108 109
109 110 class context(object):
110 111 def __enter__(self):
111 112 anon = User.get_default_user()
112 113 anon.active = status
113 114 Session().add(anon)
114 115 Session().commit()
115 116 time.sleep(1.5) # must sleep for cache (1s to expire)
116 117
117 118 def __exit__(self, exc_type, exc_val, exc_tb):
118 119 anon = User.get_default_user()
119 120 anon.active = not status
120 121 Session().add(anon)
121 122 Session().commit()
122 123
123 124 return context()
124 125
125 def auth_restriction(self, auth_restriction):
126 def auth_restriction(self, registry, auth_restriction):
126 127 """
127 128 Context process for changing the builtin rhodecode plugin auth restrictions.
128 129 Use like:
129 130 fixture = Fixture()
130 131 with fixture.auth_restriction('super_admin'):
131 132 #tests
132 133
133 134 after this block auth restriction will be taken off
134 135 """
135 136
136 137 class context(object):
137 138 def _get_pluing(self):
138 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
139 RhodeCodeAuthPlugin.uid)
139 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
140 140 plugin = RhodeCodeAuthPlugin(plugin_id)
141 141 return plugin
142 142
143 143 def __enter__(self):
144 144 plugin = self._get_pluing()
145 plugin.create_or_update_setting(
146 'auth_restriction', auth_restriction)
145 plugin.create_or_update_setting('auth_restriction', auth_restriction)
147 146 Session().commit()
147 SettingsModel().invalidate_settings_cache()
148 148
149 149 def __exit__(self, exc_type, exc_val, exc_tb):
150 150 plugin = self._get_pluing()
151 151 plugin.create_or_update_setting(
152 152 'auth_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE)
153 153 Session().commit()
154 SettingsModel().invalidate_settings_cache()
154 155
155 156 return context()
156 157
157 def scope_restriction(self, scope_restriction):
158 def scope_restriction(self, registry, scope_restriction):
158 159 """
159 160 Context process for changing the builtin rhodecode plugin scope restrictions.
160 161 Use like:
161 162 fixture = Fixture()
162 163 with fixture.scope_restriction('scope_http'):
163 164 #tests
164 165
165 166 after this block scope restriction will be taken off
166 167 """
167 168
168 169 class context(object):
169 170 def _get_pluing(self):
170 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
171 RhodeCodeAuthPlugin.uid)
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 172 plugin = RhodeCodeAuthPlugin(plugin_id)
173 173 return plugin
174 174
175 175 def __enter__(self):
176 176 plugin = self._get_pluing()
177 plugin.create_or_update_setting(
178 'scope_restriction', scope_restriction)
177 plugin.create_or_update_setting('scope_restriction', scope_restriction)
179 178 Session().commit()
179 SettingsModel().invalidate_settings_cache()
180 180
181 181 def __exit__(self, exc_type, exc_val, exc_tb):
182 182 plugin = self._get_pluing()
183 183 plugin.create_or_update_setting(
184 184 'scope_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL)
185 185 Session().commit()
186 SettingsModel().invalidate_settings_cache()
186 187
187 188 return context()
188 189
189 190 def _get_repo_create_params(self, **custom):
190 191 defs = {
191 192 'repo_name': None,
192 193 'repo_type': 'hg',
193 194 'clone_uri': '',
194 195 'push_uri': '',
195 196 'repo_group': '-1',
196 197 'repo_description': 'DESC',
197 198 'repo_private': False,
198 199 'repo_landing_rev': 'rev:tip',
199 200 'repo_copy_permissions': False,
200 201 'repo_state': Repository.STATE_CREATED,
201 202 }
202 203 defs.update(custom)
203 204 if 'repo_name_full' not in custom:
204 205 defs.update({'repo_name_full': defs['repo_name']})
205 206
206 207 # fix the repo name if passed as repo_name_full
207 208 if defs['repo_name']:
208 209 defs['repo_name'] = defs['repo_name'].split('/')[-1]
209 210
210 211 return defs
211 212
212 213 def _get_group_create_params(self, **custom):
213 214 defs = {
214 215 'group_name': None,
215 216 'group_description': 'DESC',
216 217 'perm_updates': [],
217 218 'perm_additions': [],
218 219 'perm_deletions': [],
219 220 'group_parent_id': -1,
220 221 'enable_locking': False,
221 222 'recursive': False,
222 223 }
223 224 defs.update(custom)
224 225
225 226 return defs
226 227
227 228 def _get_user_create_params(self, name, **custom):
228 229 defs = {
229 230 'username': name,
230 231 'password': 'qweqwe',
231 232 'email': '%s+test@rhodecode.org' % name,
232 233 'firstname': 'TestUser',
233 234 'lastname': 'Test',
234 235 'description': 'test description',
235 236 'active': True,
236 237 'admin': False,
237 238 'extern_type': 'rhodecode',
238 239 'extern_name': None,
239 240 }
240 241 defs.update(custom)
241 242
242 243 return defs
243 244
244 245 def _get_user_group_create_params(self, name, **custom):
245 246 defs = {
246 247 'users_group_name': name,
247 248 'user_group_description': 'DESC',
248 249 'users_group_active': True,
249 250 'user_group_data': {},
250 251 }
251 252 defs.update(custom)
252 253
253 254 return defs
254 255
255 256 def create_repo(self, name, **kwargs):
256 257 repo_group = kwargs.get('repo_group')
257 258 if isinstance(repo_group, RepoGroup):
258 259 kwargs['repo_group'] = repo_group.group_id
259 260 name = name.split(Repository.NAME_SEP)[-1]
260 261 name = Repository.NAME_SEP.join((repo_group.group_name, name))
261 262
262 263 if 'skip_if_exists' in kwargs:
263 264 del kwargs['skip_if_exists']
264 265 r = Repository.get_by_repo_name(name)
265 266 if r:
266 267 return r
267 268
268 269 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
269 270 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
270 271 RepoModel().create(form_data, cur_user)
271 272 Session().commit()
272 273 repo = Repository.get_by_repo_name(name)
273 274 assert repo
274 275 return repo
275 276
276 277 def create_fork(self, repo_to_fork, fork_name, **kwargs):
277 278 repo_to_fork = Repository.get_by_repo_name(repo_to_fork)
278 279
279 280 form_data = self._get_repo_create_params(repo_name=fork_name,
280 281 fork_parent_id=repo_to_fork.repo_id,
281 282 repo_type=repo_to_fork.repo_type,
282 283 **kwargs)
283 284 #TODO: fix it !!
284 285 form_data['description'] = form_data['repo_description']
285 286 form_data['private'] = form_data['repo_private']
286 287 form_data['landing_rev'] = form_data['repo_landing_rev']
287 288
288 289 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
289 290 RepoModel().create_fork(form_data, cur_user=owner)
290 291 Session().commit()
291 292 r = Repository.get_by_repo_name(fork_name)
292 293 assert r
293 294 return r
294 295
295 296 def destroy_repo(self, repo_name, **kwargs):
296 297 RepoModel().delete(repo_name, pull_requests='delete', **kwargs)
297 298 Session().commit()
298 299
299 300 def destroy_repo_on_filesystem(self, repo_name):
300 301 rm_path = os.path.join(RepoModel().repos_path, repo_name)
301 302 if os.path.isdir(rm_path):
302 303 shutil.rmtree(rm_path)
303 304
304 305 def create_repo_group(self, name, **kwargs):
305 306 if 'skip_if_exists' in kwargs:
306 307 del kwargs['skip_if_exists']
307 308 gr = RepoGroup.get_by_group_name(group_name=name)
308 309 if gr:
309 310 return gr
310 311 form_data = self._get_group_create_params(group_name=name, **kwargs)
311 312 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
312 313 gr = RepoGroupModel().create(
313 314 group_name=form_data['group_name'],
314 315 group_description=form_data['group_name'],
315 316 owner=owner)
316 317 Session().commit()
317 318 gr = RepoGroup.get_by_group_name(gr.group_name)
318 319 return gr
319 320
320 321 def destroy_repo_group(self, repogroupid):
321 322 RepoGroupModel().delete(repogroupid)
322 323 Session().commit()
323 324
324 325 def create_user(self, name, **kwargs):
325 326 if 'skip_if_exists' in kwargs:
326 327 del kwargs['skip_if_exists']
327 328 user = User.get_by_username(name)
328 329 if user:
329 330 return user
330 331 form_data = self._get_user_create_params(name, **kwargs)
331 332 user = UserModel().create(form_data)
332 333
333 334 # create token for user
334 335 AuthTokenModel().create(
335 336 user=user, description=u'TEST_USER_TOKEN')
336 337
337 338 Session().commit()
338 339 user = User.get_by_username(user.username)
339 340 return user
340 341
341 342 def destroy_user(self, userid):
342 343 UserModel().delete(userid)
343 344 Session().commit()
344 345
345 346 def create_additional_user_email(self, user, email):
346 347 uem = UserEmailMap()
347 348 uem.user = user
348 349 uem.email = email
349 350 Session().add(uem)
350 351 return uem
351 352
352 353 def destroy_users(self, userid_iter):
353 354 for user_id in userid_iter:
354 355 if User.get_by_username(user_id):
355 356 UserModel().delete(user_id)
356 357 Session().commit()
357 358
358 359 def create_user_group(self, name, **kwargs):
359 360 if 'skip_if_exists' in kwargs:
360 361 del kwargs['skip_if_exists']
361 362 gr = UserGroup.get_by_group_name(group_name=name)
362 363 if gr:
363 364 return gr
364 365 # map active flag to the real attribute. For API consistency of fixtures
365 366 if 'active' in kwargs:
366 367 kwargs['users_group_active'] = kwargs['active']
367 368 del kwargs['active']
368 369 form_data = self._get_user_group_create_params(name, **kwargs)
369 370 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
370 371 user_group = UserGroupModel().create(
371 372 name=form_data['users_group_name'],
372 373 description=form_data['user_group_description'],
373 374 owner=owner, active=form_data['users_group_active'],
374 375 group_data=form_data['user_group_data'])
375 376 Session().commit()
376 377 user_group = UserGroup.get_by_group_name(user_group.users_group_name)
377 378 return user_group
378 379
379 380 def destroy_user_group(self, usergroupid):
380 381 UserGroupModel().delete(user_group=usergroupid, force=True)
381 382 Session().commit()
382 383
383 384 def create_gist(self, **kwargs):
384 385 form_data = {
385 386 'description': 'new-gist',
386 387 'owner': TEST_USER_ADMIN_LOGIN,
387 388 'gist_type': GistModel.cls.GIST_PUBLIC,
388 389 'lifetime': -1,
389 390 'acl_level': Gist.ACL_LEVEL_PUBLIC,
390 391 'gist_mapping': {'filename1.txt': {'content': 'hello world'},}
391 392 }
392 393 form_data.update(kwargs)
393 394 gist = GistModel().create(
394 395 description=form_data['description'], owner=form_data['owner'],
395 396 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
396 397 lifetime=form_data['lifetime'], gist_acl_level=form_data['acl_level']
397 398 )
398 399 Session().commit()
399 400 return gist
400 401
401 402 def destroy_gists(self, gistid=None):
402 403 for g in GistModel.cls.get_all():
403 404 if gistid:
404 405 if gistid == g.gist_access_id:
405 406 GistModel().delete(g)
406 407 else:
407 408 GistModel().delete(g)
408 409 Session().commit()
409 410
410 411 def load_resource(self, resource_name, strip=False):
411 412 with open(os.path.join(FIXTURES, resource_name)) as f:
412 413 source = f.read()
413 414 if strip:
414 415 source = source.strip()
415 416
416 417 return source
@@ -1,340 +1,342 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 py.test config for test suite for making push/pull operations.
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30 import os
31 31 import tempfile
32 32 import textwrap
33 33 import pytest
34 34
35 35 from rhodecode import events
36 36 from rhodecode.model.db import Integration, UserRepoToPerm, Permission, \
37 37 UserToRepoBranchPermission, User
38 38 from rhodecode.model.integration import IntegrationModel
39 39 from rhodecode.model.db import Repository
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import SettingsModel
42 42 from rhodecode.integrations.types.webhook import WebhookIntegrationType
43 43
44 44 from rhodecode.tests import GIT_REPO, HG_REPO
45 45 from rhodecode.tests.fixture import Fixture
46 46 from rhodecode.tests.server_utils import RcWebServer
47 47
48 48 REPO_GROUP = 'a_repo_group'
49 49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
50 50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
51 51
52 52
53 53 @pytest.fixture(scope="module")
54 54 def rcextensions(request, db_connection, tmpdir_factory):
55 55 """
56 56 Installs a testing rcextensions pack to ensure they work as expected.
57 57 """
58 58 init_content = textwrap.dedent("""
59 59 # Forward import the example rcextensions to make it
60 60 # active for our tests.
61 61 from rhodecode.tests.other.example_rcextensions import *
62 62 """)
63 63
64 64 # Note: rcextensions are looked up based on the path of the ini file
65 65 root_path = tmpdir_factory.getbasetemp()
66 66 rcextensions_path = root_path.join('rcextensions')
67 67 init_path = rcextensions_path.join('__init__.py')
68 68
69 69 if rcextensions_path.check():
70 70 pytest.fail(
71 71 "Path for rcextensions already exists, please clean up before "
72 72 "test run this path: %s" % (rcextensions_path, ))
73 73 return
74 74
75 75 request.addfinalizer(rcextensions_path.remove)
76 76 init_path.write_binary(init_content, ensure=True)
77 77
78 78
79 79 @pytest.fixture(scope="module")
80 80 def repos(request, db_connection):
81 81 """Create a copy of each test repo in a repo group."""
82 82 fixture = Fixture()
83 83 repo_group = fixture.create_repo_group(REPO_GROUP)
84 84 repo_group_id = repo_group.group_id
85 85 fixture.create_fork(HG_REPO, HG_REPO,
86 86 repo_name_full=HG_REPO_WITH_GROUP,
87 87 repo_group=repo_group_id)
88 88 fixture.create_fork(GIT_REPO, GIT_REPO,
89 89 repo_name_full=GIT_REPO_WITH_GROUP,
90 90 repo_group=repo_group_id)
91 91
92 92 @request.addfinalizer
93 93 def cleanup():
94 94 fixture.destroy_repo(HG_REPO_WITH_GROUP)
95 95 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
96 96 fixture.destroy_repo_group(repo_group_id)
97 97
98 98
99 99 @pytest.fixture(scope="module")
100 100 def rc_web_server_config_modification():
101 101 return []
102 102
103 103
104 104 @pytest.fixture(scope="module")
105 105 def rc_web_server_config_factory(testini_factory, rc_web_server_config_modification):
106 106 """
107 107 Configuration file used for the fixture `rc_web_server`.
108 108 """
109 109
110 110 def factory(rcweb_port, vcsserver_port):
111 111 custom_params = [
112 112 {'handler_console': {'level': 'DEBUG'}},
113 113 {'server:main': {'port': rcweb_port}},
114 114 {'app:main': {'vcs.server': 'localhost:%s' % vcsserver_port}}
115 115 ]
116 116 custom_params.extend(rc_web_server_config_modification)
117 117 return testini_factory(custom_params)
118 118 return factory
119 119
120 120
121 121 @pytest.fixture(scope="module")
122 122 def rc_web_server(
123 123 request, vcsserver_factory, available_port_factory,
124 124 rc_web_server_config_factory, repos, rcextensions):
125 125 """
126 126 Run the web server as a subprocess. with it's own instance of vcsserver
127 127 """
128 128 rcweb_port = available_port_factory()
129 129 print('Using rcweb ops test port {}'.format(rcweb_port))
130 130
131 131 vcsserver_port = available_port_factory()
132 132 print('Using vcsserver ops test port {}'.format(vcsserver_port))
133 133
134 134 vcs_log = os.path.join(tempfile.gettempdir(), 'rc_op_vcs.log')
135 135 vcsserver_factory(
136 136 request, vcsserver_port=vcsserver_port,
137 137 log_file=vcs_log,
138 138 overrides=(
139 139 {'server:main': {'workers': 2}},
140 140 {'server:main': {'graceful_timeout': 10}},
141 141 ))
142 142
143 143 rc_log = os.path.join(tempfile.gettempdir(), 'rc_op_web.log')
144 144 rc_web_server_config = rc_web_server_config_factory(
145 145 rcweb_port=rcweb_port,
146 146 vcsserver_port=vcsserver_port)
147 147 server = RcWebServer(rc_web_server_config, log_file=rc_log)
148 148 server.start()
149 149
150 150 @request.addfinalizer
151 151 def cleanup():
152 152 server.shutdown()
153 153
154 154 server.wait_until_ready()
155 155 return server
156 156
157 157
158 158 @pytest.fixture()
159 159 def disable_locking(baseapp):
160 160 r = Repository.get_by_repo_name(GIT_REPO)
161 161 Repository.unlock(r)
162 162 r.enable_locking = False
163 163 Session().add(r)
164 164 Session().commit()
165 165
166 166 r = Repository.get_by_repo_name(HG_REPO)
167 167 Repository.unlock(r)
168 168 r.enable_locking = False
169 169 Session().add(r)
170 170 Session().commit()
171 171
172 172
173 173 @pytest.fixture()
174 174 def enable_auth_plugins(request, baseapp, csrf_token):
175 175 """
176 176 Return a factory object that when called, allows to control which
177 177 authentication plugins are enabled.
178 178 """
179 179 def _enable_plugins(plugins_list, override=None):
180 180 override = override or {}
181 181 params = {
182 182 'auth_plugins': ','.join(plugins_list),
183 183 }
184 184
185 185 # helper translate some names to others
186 186 name_map = {
187 187 'token': 'authtoken'
188 188 }
189 189
190 190 for module in plugins_list:
191 191 plugin_name = module.partition('#')[-1]
192 192 if plugin_name in name_map:
193 193 plugin_name = name_map[plugin_name]
194 194 enabled_plugin = 'auth_%s_enabled' % plugin_name
195 195 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
196 196
197 197 # default params that are needed for each plugin,
198 198 # `enabled` and `cache_ttl`
199 199 params.update({
200 200 enabled_plugin: True,
201 201 cache_ttl: 0
202 202 })
203 203 if override.get:
204 204 params.update(override.get(module, {}))
205 205
206 206 validated_params = params
207 207 for k, v in validated_params.items():
208 208 setting = SettingsModel().create_or_update_setting(k, v)
209 209 Session().add(setting)
210 210 Session().commit()
211 211
212 SettingsModel().invalidate_settings_cache()
213
212 214 def cleanup():
213 215 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
214 216
215 217 request.addfinalizer(cleanup)
216 218
217 219 return _enable_plugins
218 220
219 221
220 222 @pytest.fixture()
221 223 def fs_repo_only(request, rhodecode_fixtures):
222 224 def fs_repo_fabric(repo_name, repo_type):
223 225 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
224 226 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
225 227
226 228 def cleanup():
227 229 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
228 230 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
229 231
230 232 request.addfinalizer(cleanup)
231 233
232 234 return fs_repo_fabric
233 235
234 236
235 237 @pytest.fixture()
236 238 def enable_webhook_push_integration(request):
237 239 integration = Integration()
238 240 integration.integration_type = WebhookIntegrationType.key
239 241 Session().add(integration)
240 242
241 243 settings = dict(
242 244 url='http://httpbin.org/post',
243 245 secret_token='secret',
244 246 username=None,
245 247 password=None,
246 248 custom_header_key=None,
247 249 custom_header_val=None,
248 250 method_type='post',
249 251 events=[events.RepoPushEvent.name],
250 252 log_data=True
251 253 )
252 254
253 255 IntegrationModel().update_integration(
254 256 integration,
255 257 name='IntegrationWebhookTest',
256 258 enabled=True,
257 259 settings=settings,
258 260 repo=None,
259 261 repo_group=None,
260 262 child_repos_only=False,
261 263 )
262 264 Session().commit()
263 265 integration_id = integration.integration_id
264 266
265 267 @request.addfinalizer
266 268 def cleanup():
267 269 integration = Integration.get(integration_id)
268 270 Session().delete(integration)
269 271 Session().commit()
270 272
271 273
272 274 @pytest.fixture()
273 275 def branch_permission_setter(request):
274 276 """
275 277
276 278 def my_test(branch_permission_setter)
277 279 branch_permission_setter(repo_name, username, pattern='*', permission='branch.push')
278 280
279 281 """
280 282
281 283 rule_id = None
282 284 write_perm_id = None
283 285 write_perm = None
284 286 rule = None
285 287
286 288 def _branch_permissions_setter(
287 289 repo_name, username, pattern='*', permission='branch.push_force'):
288 290 global rule_id, write_perm_id
289 291 global rule, write_perm
290 292
291 293 repo = Repository.get_by_repo_name(repo_name)
292 294 repo_id = repo.repo_id
293 295
294 296 user = User.get_by_username(username)
295 297 user_id = user.user_id
296 298
297 299 rule_perm_obj = Permission.get_by_key(permission)
298 300
299 301 # add new entry, based on existing perm entry
300 302 perm = UserRepoToPerm.query() \
301 303 .filter(UserRepoToPerm.repository_id == repo_id) \
302 304 .filter(UserRepoToPerm.user_id == user_id) \
303 305 .first()
304 306
305 307 if not perm:
306 308 # such user isn't defined in Permissions for repository
307 309 # we now on-the-fly add new permission
308 310
309 311 write_perm = UserRepoToPerm()
310 312 write_perm.permission = Permission.get_by_key('repository.write')
311 313 write_perm.repository_id = repo_id
312 314 write_perm.user_id = user_id
313 315 Session().add(write_perm)
314 316 Session().flush()
315 317
316 318 perm = write_perm
317 319
318 320 rule = UserToRepoBranchPermission()
319 321 rule.rule_to_perm_id = perm.repo_to_perm_id
320 322 rule.branch_pattern = pattern
321 323 rule.rule_order = 10
322 324 rule.permission = rule_perm_obj
323 325 rule.repository_id = repo_id
324 326 Session().add(rule)
325 327 Session().commit()
326 328
327 329 return rule
328 330
329 331 @request.addfinalizer
330 332 def cleanup():
331 333 if rule:
332 334 Session().delete(rule)
333 335 Session().commit()
334 336 if write_perm:
335 337 Session().delete(write_perm)
336 338 Session().commit()
337 339
338 340 return _branch_permissions_setter
339 341
340 342
General Comments 0
You need to be logged in to leave comments. Login now