##// END OF EJS Templates
feat(login by email option): added ability to log in with user primary email. Fixes: RCCE-63
ilin.s -
r5358:2095c653 default
parent child Browse files
Show More
@@ -1,581 +1,593 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import urllib.parse
20 20
21 21 import mock
22 22 import pytest
23 23
24 24
25 25 from rhodecode.lib.auth import check_password
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.auth_token import AuthTokenModel
28 28 from rhodecode.model.db import User, Notification, UserApiKeys
29 29 from rhodecode.model.meta import Session
30 30
31 31 from rhodecode.tests import (
32 32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
33 33 no_newline_id_generator)
34 34 from rhodecode.tests.fixture import Fixture
35 35 from rhodecode.tests.routes import route_path
36 36
37 37 fixture = Fixture()
38 38
39 39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
40 40
41 41
42 42 @pytest.mark.usefixtures('app')
43 43 class TestLoginController(object):
44 44 destroy_users = set()
45 45
46 46 @classmethod
47 47 def teardown_class(cls):
48 48 fixture.destroy_users(cls.destroy_users)
49 49
50 50 def teardown_method(self, method):
51 51 for n in Notification.query().all():
52 52 Session().delete(n)
53 53
54 54 Session().commit()
55 55 assert Notification.query().all() == []
56 56
57 57 def test_index(self):
58 58 response = self.app.get(route_path('login'))
59 59 assert response.status == '200 OK'
60 60 # Test response...
61 61
62 62 def test_login_admin_ok(self):
63 63 response = self.app.post(route_path('login'),
64 64 {'username': 'test_admin',
65 65 'password': 'test12'}, status=302)
66 66 response = response.follow()
67 67 session = response.get_session_from_response()
68 68 username = session['rhodecode_user'].get('username')
69 69 assert username == 'test_admin'
70 70 response.mustcontain('logout')
71 71
72 72 def test_login_regular_ok(self):
73 73 response = self.app.post(route_path('login'),
74 74 {'username': 'test_regular',
75 75 'password': 'test12'}, status=302)
76 76
77 77 response = response.follow()
78 78 session = response.get_session_from_response()
79 79 username = session['rhodecode_user'].get('username')
80 80 assert username == 'test_regular'
81 81 response.mustcontain('logout')
82 82
83 def test_login_with_primary_email(self):
84 user_email = 'test_regular@mail.com'
85 response = self.app.post(route_path('login'),
86 {'username': user_email,
87 'password': 'test12'}, status=302)
88 response = response.follow()
89 session = response.get_session_from_response()
90 user = session['rhodecode_user']
91 assert user['username'] == user_email.split('@')[0]
92 assert user['is_authenticated']
93 response.mustcontain('logout')
94
83 95 def test_login_regular_forbidden_when_super_admin_restriction(self):
84 96 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
85 97 with fixture.auth_restriction(self.app._pyramid_registry,
86 98 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
87 99 response = self.app.post(route_path('login'),
88 100 {'username': 'test_regular',
89 101 'password': 'test12'})
90 102
91 103 response.mustcontain('invalid user name')
92 104 response.mustcontain('invalid password')
93 105
94 106 def test_login_regular_forbidden_when_scope_restriction(self):
95 107 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
96 108 with fixture.scope_restriction(self.app._pyramid_registry,
97 109 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
98 110 response = self.app.post(route_path('login'),
99 111 {'username': 'test_regular',
100 112 'password': 'test12'})
101 113
102 114 response.mustcontain('invalid user name')
103 115 response.mustcontain('invalid password')
104 116
105 117 def test_login_ok_came_from(self):
106 118 test_came_from = '/_admin/users?branch=stable'
107 119 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
108 120 response = self.app.post(
109 121 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
110 122
111 123 assert 'branch=stable' in response.location
112 124 response = response.follow()
113 125
114 126 assert response.status == '200 OK'
115 127 response.mustcontain('Users administration')
116 128
117 129 def test_redirect_to_login_with_get_args(self):
118 130 with fixture.anon_access(False):
119 131 kwargs = {'branch': 'stable'}
120 132 response = self.app.get(
121 133 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
122 134 status=302)
123 135
124 136 response_query = urllib.parse.parse_qsl(response.location)
125 137 assert 'branch=stable' in response_query[0][1]
126 138
127 139 def test_login_form_with_get_args(self):
128 140 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
129 141 response = self.app.get(_url)
130 142 assert 'branch%3Dstable' in response.form.action
131 143
132 144 @pytest.mark.parametrize("url_came_from", [
133 145 'data:text/html,<script>window.alert("xss")</script>',
134 146 'mailto:test@rhodecode.org',
135 147 'file:///etc/passwd',
136 148 'ftp://some.ftp.server',
137 149 'http://other.domain',
138 150 ], ids=no_newline_id_generator)
139 151 def test_login_bad_came_froms(self, url_came_from):
140 152 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
141 153 response = self.app.post(
142 154 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
143 155 assert response.status == '302 Found'
144 156 response = response.follow()
145 157 assert response.status == '200 OK'
146 158 assert response.request.path == '/'
147 159
148 160 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
149 161 @pytest.mark.parametrize("url_came_from", [
150 162 '/\r\nX-Forwarded-Host: \rhttp://example.org',
151 163 ], ids=no_newline_id_generator)
152 164 def test_login_bad_came_froms_404(self, url_came_from):
153 165 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
154 166 response = self.app.post(
155 167 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
156 168
157 169 response = response.follow()
158 170 assert response.status == '404 Not Found'
159 171
160 172 def test_login_short_password(self):
161 173 response = self.app.post(route_path('login'),
162 174 {'username': 'test_admin',
163 175 'password': 'as'})
164 176 assert response.status == '200 OK'
165 177
166 178 response.mustcontain('Enter 3 characters or more')
167 179
168 180 def test_login_wrong_non_ascii_password(self, user_regular):
169 181 response = self.app.post(
170 182 route_path('login'),
171 183 {'username': user_regular.username,
172 184 'password': 'invalid-non-asci\xe4'.encode('utf8')})
173 185
174 186 response.mustcontain('invalid user name')
175 187 response.mustcontain('invalid password')
176 188
177 189 def test_login_with_non_ascii_password(self, user_util):
178 190 password = u'valid-non-ascii\xe4'
179 191 user = user_util.create_user(password=password)
180 192 response = self.app.post(
181 193 route_path('login'),
182 194 {'username': user.username,
183 195 'password': password})
184 196 assert response.status_code == 302
185 197
186 198 def test_login_wrong_username_password(self):
187 199 response = self.app.post(route_path('login'),
188 200 {'username': 'error',
189 201 'password': 'test12'})
190 202
191 203 response.mustcontain('invalid user name')
192 204 response.mustcontain('invalid password')
193 205
194 206 def test_login_admin_ok_password_migration(self, real_crypto_backend):
195 207 from rhodecode.lib import auth
196 208
197 209 # create new user, with sha256 password
198 210 temp_user = 'test_admin_sha256'
199 211 user = fixture.create_user(temp_user)
200 212 user.password = auth._RhodeCodeCryptoSha256().hash_create(
201 213 b'test123')
202 214 Session().add(user)
203 215 Session().commit()
204 216 self.destroy_users.add(temp_user)
205 217 response = self.app.post(route_path('login'),
206 218 {'username': temp_user,
207 219 'password': 'test123'}, status=302)
208 220
209 221 response = response.follow()
210 222 session = response.get_session_from_response()
211 223 username = session['rhodecode_user'].get('username')
212 224 assert username == temp_user
213 225 response.mustcontain('logout')
214 226
215 227 # new password should be bcrypted, after log-in and transfer
216 228 user = User.get_by_username(temp_user)
217 229 assert user.password.startswith('$')
218 230
219 231 # REGISTRATIONS
220 232 def test_register(self):
221 233 response = self.app.get(route_path('register'))
222 234 response.mustcontain('Create an Account')
223 235
224 236 def test_register_err_same_username(self):
225 237 uname = 'test_admin'
226 238 response = self.app.post(
227 239 route_path('register'),
228 240 {
229 241 'username': uname,
230 242 'password': 'test12',
231 243 'password_confirmation': 'test12',
232 244 'email': 'goodmail@domain.com',
233 245 'firstname': 'test',
234 246 'lastname': 'test'
235 247 }
236 248 )
237 249
238 250 assertr = response.assert_response()
239 251 msg = 'Username "%(username)s" already exists'
240 252 msg = msg % {'username': uname}
241 253 assertr.element_contains('#username+.error-message', msg)
242 254
243 255 def test_register_err_same_email(self):
244 256 response = self.app.post(
245 257 route_path('register'),
246 258 {
247 259 'username': 'test_admin_0',
248 260 'password': 'test12',
249 261 'password_confirmation': 'test12',
250 262 'email': 'test_admin@mail.com',
251 263 'firstname': 'test',
252 264 'lastname': 'test'
253 265 }
254 266 )
255 267
256 268 assertr = response.assert_response()
257 269 msg = u'This e-mail address is already taken'
258 270 assertr.element_contains('#email+.error-message', msg)
259 271
260 272 def test_register_err_same_email_case_sensitive(self):
261 273 response = self.app.post(
262 274 route_path('register'),
263 275 {
264 276 'username': 'test_admin_1',
265 277 'password': 'test12',
266 278 'password_confirmation': 'test12',
267 279 'email': 'TesT_Admin@mail.COM',
268 280 'firstname': 'test',
269 281 'lastname': 'test'
270 282 }
271 283 )
272 284 assertr = response.assert_response()
273 285 msg = u'This e-mail address is already taken'
274 286 assertr.element_contains('#email+.error-message', msg)
275 287
276 288 def test_register_err_wrong_data(self):
277 289 response = self.app.post(
278 290 route_path('register'),
279 291 {
280 292 'username': 'xs',
281 293 'password': 'test',
282 294 'password_confirmation': 'test',
283 295 'email': 'goodmailm',
284 296 'firstname': 'test',
285 297 'lastname': 'test'
286 298 }
287 299 )
288 300 assert response.status == '200 OK'
289 301 response.mustcontain('An email address must contain a single @')
290 302 response.mustcontain('Enter a value 6 characters long or more')
291 303
292 304 def test_register_err_username(self):
293 305 response = self.app.post(
294 306 route_path('register'),
295 307 {
296 308 'username': 'error user',
297 309 'password': 'test12',
298 310 'password_confirmation': 'test12',
299 311 'email': 'goodmailm',
300 312 'firstname': 'test',
301 313 'lastname': 'test'
302 314 }
303 315 )
304 316
305 317 response.mustcontain('An email address must contain a single @')
306 318 response.mustcontain(
307 319 'Username may only contain '
308 320 'alphanumeric characters underscores, '
309 321 'periods or dashes and must begin with '
310 322 'alphanumeric character')
311 323
312 324 def test_register_err_case_sensitive(self):
313 325 usr = 'Test_Admin'
314 326 response = self.app.post(
315 327 route_path('register'),
316 328 {
317 329 'username': usr,
318 330 'password': 'test12',
319 331 'password_confirmation': 'test12',
320 332 'email': 'goodmailm',
321 333 'firstname': 'test',
322 334 'lastname': 'test'
323 335 }
324 336 )
325 337
326 338 assertr = response.assert_response()
327 339 msg = u'Username "%(username)s" already exists'
328 340 msg = msg % {'username': usr}
329 341 assertr.element_contains('#username+.error-message', msg)
330 342
331 343 def test_register_special_chars(self):
332 344 response = self.app.post(
333 345 route_path('register'),
334 346 {
335 347 'username': 'xxxaxn',
336 348 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
337 349 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
338 350 'email': 'goodmailm@test.plx',
339 351 'firstname': 'test',
340 352 'lastname': 'test'
341 353 }
342 354 )
343 355
344 356 msg = u'Invalid characters (non-ascii) in password'
345 357 response.mustcontain(msg)
346 358
347 359 def test_register_password_mismatch(self):
348 360 response = self.app.post(
349 361 route_path('register'),
350 362 {
351 363 'username': 'xs',
352 364 'password': '123qwe',
353 365 'password_confirmation': 'qwe123',
354 366 'email': 'goodmailm@test.plxa',
355 367 'firstname': 'test',
356 368 'lastname': 'test'
357 369 }
358 370 )
359 371 msg = u'Passwords do not match'
360 372 response.mustcontain(msg)
361 373
362 374 def test_register_ok(self):
363 375 username = 'test_regular4'
364 376 password = 'qweqwe'
365 377 email = 'marcin@test.com'
366 378 name = 'testname'
367 379 lastname = 'testlastname'
368 380
369 381 # this initializes a session
370 382 response = self.app.get(route_path('register'))
371 383 response.mustcontain('Create an Account')
372 384
373 385
374 386 response = self.app.post(
375 387 route_path('register'),
376 388 {
377 389 'username': username,
378 390 'password': password,
379 391 'password_confirmation': password,
380 392 'email': email,
381 393 'firstname': name,
382 394 'lastname': lastname,
383 395 'admin': True
384 396 },
385 397 status=302
386 398 ) # This should be overridden
387 399
388 400 assert_session_flash(
389 401 response, 'You have successfully registered with RhodeCode. You can log-in now.')
390 402
391 403 ret = Session().query(User).filter(
392 404 User.username == 'test_regular4').one()
393 405 assert ret.username == username
394 406 assert check_password(password, ret.password)
395 407 assert ret.email == email
396 408 assert ret.name == name
397 409 assert ret.lastname == lastname
398 410 assert ret.auth_tokens is not None
399 411 assert not ret.admin
400 412
401 413 def test_forgot_password_wrong_mail(self):
402 414 bad_email = 'marcin@wrongmail.org'
403 415 # this initializes a session
404 416 self.app.get(route_path('reset_password'))
405 417
406 418 response = self.app.post(
407 419 route_path('reset_password'), {'email': bad_email, }
408 420 )
409 421 assert_session_flash(response,
410 422 'If such email exists, a password reset link was sent to it.')
411 423
412 424 def test_forgot_password(self, user_util):
413 425 # this initializes a session
414 426 self.app.get(route_path('reset_password'))
415 427
416 428 user = user_util.create_user()
417 429 user_id = user.user_id
418 430 email = user.email
419 431
420 432 response = self.app.post(route_path('reset_password'), {'email': email, })
421 433
422 434 assert_session_flash(response,
423 435 'If such email exists, a password reset link was sent to it.')
424 436
425 437 # BAD KEY
426 438 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
427 439 response = self.app.get(confirm_url, status=302)
428 440 assert response.location.endswith(route_path('reset_password'))
429 441 assert_session_flash(response, 'Given reset token is invalid')
430 442
431 443 response.follow() # cleanup flash
432 444
433 445 # GOOD KEY
434 446 key = UserApiKeys.query()\
435 447 .filter(UserApiKeys.user_id == user_id)\
436 448 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
437 449 .first()
438 450
439 451 assert key
440 452
441 453 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
442 454 response = self.app.get(confirm_url)
443 455 assert response.status == '302 Found'
444 456 assert response.location.endswith(route_path('login'))
445 457
446 458 assert_session_flash(
447 459 response,
448 460 'Your password reset was successful, '
449 461 'a new password has been sent to your email')
450 462
451 463 response.follow()
452 464
453 465 def _get_api_whitelist(self, values=None):
454 466 config = {'api_access_controllers_whitelist': values or []}
455 467 return config
456 468
457 469 @pytest.mark.parametrize("test_name, auth_token", [
458 470 ('none', None),
459 471 ('empty_string', ''),
460 472 ('fake_number', '123456'),
461 473 ('proper_auth_token', None)
462 474 ])
463 475 def test_access_not_whitelisted_page_via_auth_token(
464 476 self, test_name, auth_token, user_admin):
465 477
466 478 whitelist = self._get_api_whitelist([])
467 479 with mock.patch.dict('rhodecode.CONFIG', whitelist):
468 480 assert [] == whitelist['api_access_controllers_whitelist']
469 481 if test_name == 'proper_auth_token':
470 482 # use builtin if api_key is None
471 483 auth_token = user_admin.api_key
472 484
473 485 with fixture.anon_access(False):
474 486 # webtest uses linter to check if response is bytes,
475 487 # and we use memoryview here as a wrapper, quick turn-off
476 488 self.app.lint = False
477 489
478 490 self.app.get(
479 491 route_path('repo_commit_raw',
480 492 repo_name=HG_REPO, commit_id='tip',
481 493 params=dict(api_key=auth_token)),
482 494 status=302)
483 495
484 496 @pytest.mark.parametrize("test_name, auth_token, code", [
485 497 ('none', None, 302),
486 498 ('empty_string', '', 302),
487 499 ('fake_number', '123456', 302),
488 500 ('proper_auth_token', None, 200)
489 501 ])
490 502 def test_access_whitelisted_page_via_auth_token(
491 503 self, test_name, auth_token, code, user_admin):
492 504
493 505 whitelist = self._get_api_whitelist(whitelist_view)
494 506
495 507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
496 508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
497 509
498 510 if test_name == 'proper_auth_token':
499 511 auth_token = user_admin.api_key
500 512 assert auth_token
501 513
502 514 with fixture.anon_access(False):
503 515 # webtest uses linter to check if response is bytes,
504 516 # and we use memoryview here as a wrapper, quick turn-off
505 517 self.app.lint = False
506 518 self.app.get(
507 519 route_path('repo_commit_raw',
508 520 repo_name=HG_REPO, commit_id='tip',
509 521 params=dict(api_key=auth_token)),
510 522 status=code)
511 523
512 524 @pytest.mark.parametrize("test_name, auth_token, code", [
513 525 ('proper_auth_token', None, 200),
514 526 ('wrong_auth_token', '123456', 302),
515 527 ])
516 528 def test_access_whitelisted_page_via_auth_token_bound_to_token(
517 529 self, test_name, auth_token, code, user_admin):
518 530
519 531 expected_token = auth_token
520 532 if test_name == 'proper_auth_token':
521 533 auth_token = user_admin.api_key
522 534 expected_token = auth_token
523 535 assert auth_token
524 536
525 537 whitelist = self._get_api_whitelist([
526 538 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
527 539
528 540 with mock.patch.dict('rhodecode.CONFIG', whitelist):
529 541
530 542 with fixture.anon_access(False):
531 543 # webtest uses linter to check if response is bytes,
532 544 # and we use memoryview here as a wrapper, quick turn-off
533 545 self.app.lint = False
534 546
535 547 self.app.get(
536 548 route_path('repo_commit_raw',
537 549 repo_name=HG_REPO, commit_id='tip',
538 550 params=dict(api_key=auth_token)),
539 551 status=code)
540 552
541 553 def test_access_page_via_extra_auth_token(self):
542 554 whitelist = self._get_api_whitelist(whitelist_view)
543 555 with mock.patch.dict('rhodecode.CONFIG', whitelist):
544 556 assert whitelist_view == \
545 557 whitelist['api_access_controllers_whitelist']
546 558
547 559 new_auth_token = AuthTokenModel().create(
548 560 TEST_USER_ADMIN_LOGIN, 'test')
549 561 Session().commit()
550 562 with fixture.anon_access(False):
551 563 # webtest uses linter to check if response is bytes,
552 564 # and we use memoryview here as a wrapper, quick turn-off
553 565 self.app.lint = False
554 566 self.app.get(
555 567 route_path('repo_commit_raw',
556 568 repo_name=HG_REPO, commit_id='tip',
557 569 params=dict(api_key=new_auth_token.api_key)),
558 570 status=200)
559 571
560 572 def test_access_page_via_expired_auth_token(self):
561 573 whitelist = self._get_api_whitelist(whitelist_view)
562 574 with mock.patch.dict('rhodecode.CONFIG', whitelist):
563 575 assert whitelist_view == \
564 576 whitelist['api_access_controllers_whitelist']
565 577
566 578 new_auth_token = AuthTokenModel().create(
567 579 TEST_USER_ADMIN_LOGIN, 'test')
568 580 Session().commit()
569 581 # patch the api key and make it expired
570 582 new_auth_token.expires = 0
571 583 Session().add(new_auth_token)
572 584 Session().commit()
573 585 with fixture.anon_access(False):
574 586 # webtest uses linter to check if response is bytes,
575 587 # and we use memoryview here as a wrapper, quick turn-off
576 588 self.app.lint = False
577 589 self.app.get(
578 590 route_path('repo_commit_raw',
579 591 repo_name=HG_REPO, commit_id='tip',
580 592 params=dict(api_key=new_auth_token.api_key)),
581 593 status=302)
@@ -1,469 +1,469 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import time
20 20 import collections
21 21 import datetime
22 22 import formencode
23 23 import formencode.htmlfill
24 24 import logging
25 25 import urllib.parse
26 26 import requests
27 27
28 28 from pyramid.httpexceptions import HTTPFound
29 29
30 30
31 31 from rhodecode.apps._base import BaseAppView
32 32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.events import UserRegistered, trigger
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.auth import (
38 38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 39 from rhodecode.lib.base import get_ip_addr
40 40 from rhodecode.lib.exceptions import UserCreationError
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User, UserApiKeys
43 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.auth_token import AuthTokenModel
46 46 from rhodecode.model.settings import SettingsModel
47 47 from rhodecode.model.user import UserModel
48 48 from rhodecode.translation import _
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 CaptchaData = collections.namedtuple(
54 54 'CaptchaData', 'active, private_key, public_key')
55 55
56 56
57 def store_user_in_session(session, username, remember=False):
58 user = User.get_by_username(username, case_insensitive=True)
57 def store_user_in_session(session, user_identifier, remember=False):
58 user = User.get_by_username_or_primary_email(user_identifier)
59 59 auth_user = AuthUser(user.user_id)
60 60 auth_user.set_authenticated()
61 61 cs = auth_user.get_cookie_store()
62 62 session['rhodecode_user'] = cs
63 63 user.update_lastlogin()
64 64 Session().commit()
65 65
66 66 # If they want to be remembered, update the cookie
67 67 if remember:
68 68 _year = (datetime.datetime.now() +
69 69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 70 session._set_cookie_expires(_year)
71 71
72 72 session.save()
73 73
74 74 safe_cs = cs.copy()
75 75 safe_cs['password'] = '****'
76 76 log.info('user %s is now authenticated and stored in '
77 'session, session attrs %s', username, safe_cs)
77 'session, session attrs %s', user_identifier, safe_cs)
78 78
79 79 # dumps session attrs back to cookie
80 80 session._update_cookie_out()
81 81 # we set new cookie
82 82 headers = None
83 83 if session.request['set_cookie']:
84 84 # send set-cookie headers back to response to update cookie
85 85 headers = [('Set-Cookie', session.request['cookie_out'])]
86 86 return headers
87 87
88 88
89 89 def get_came_from(request):
90 90 came_from = safe_str(request.GET.get('came_from', ''))
91 91 parsed = urllib.parse.urlparse(came_from)
92 92
93 93 allowed_schemes = ['http', 'https']
94 94 default_came_from = h.route_path('home')
95 95 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 96 log.error('Suspicious URL scheme detected %s for url %s',
97 97 parsed.scheme, parsed)
98 98 came_from = default_came_from
99 99 elif parsed.netloc and request.host != parsed.netloc:
100 100 log.error('Suspicious NETLOC detected %s for url %s server url '
101 101 'is: %s', parsed.netloc, parsed, request.host)
102 102 came_from = default_came_from
103 103 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
104 104 log.error('Header injection detected `%s` for url %s server url ',
105 105 parsed.path, parsed)
106 106 came_from = default_came_from
107 107
108 108 return came_from or default_came_from
109 109
110 110
111 111 class LoginView(BaseAppView):
112 112
113 113 def load_default_context(self):
114 114 c = self._get_local_tmpl_context()
115 115 c.came_from = get_came_from(self.request)
116 116 return c
117 117
118 118 def _get_captcha_data(self):
119 119 settings = SettingsModel().get_all_settings()
120 120 private_key = settings.get('rhodecode_captcha_private_key')
121 121 public_key = settings.get('rhodecode_captcha_public_key')
122 122 active = bool(private_key)
123 123 return CaptchaData(
124 124 active=active, private_key=private_key, public_key=public_key)
125 125
126 126 def validate_captcha(self, private_key):
127 127
128 128 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 129 url = "https://www.google.com/recaptcha/api/siteverify"
130 130 params = {
131 131 'secret': private_key,
132 132 'response': captcha_rs,
133 133 'remoteip': get_ip_addr(self.request.environ)
134 134 }
135 135 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 136 verify_rs = verify_rs.json()
137 137 captcha_status = verify_rs.get('success', False)
138 138 captcha_errors = verify_rs.get('error-codes', [])
139 139 if not isinstance(captcha_errors, list):
140 140 captcha_errors = [captcha_errors]
141 141 captcha_errors = ', '.join(captcha_errors)
142 142 captcha_message = ''
143 143 if captcha_status is False:
144 144 captcha_message = "Bad captcha. Errors: {}".format(
145 145 captcha_errors)
146 146
147 147 return captcha_status, captcha_message
148 148
149 149 def login(self):
150 150 c = self.load_default_context()
151 151 auth_user = self._rhodecode_user
152 152
153 153 # redirect if already logged in
154 154 if (auth_user.is_authenticated and
155 155 not auth_user.is_default and auth_user.ip_allowed):
156 156 raise HTTPFound(c.came_from)
157 157
158 158 # check if we use headers plugin, and try to login using it.
159 159 try:
160 160 log.debug('Running PRE-AUTH for headers based authentication')
161 161 auth_info = authenticate(
162 162 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
163 163 if auth_info:
164 164 headers = store_user_in_session(
165 165 self.session, auth_info.get('username'))
166 166 raise HTTPFound(c.came_from, headers=headers)
167 167 except UserCreationError as e:
168 168 log.error(e)
169 169 h.flash(e, category='error')
170 170
171 171 return self._get_template_context(c)
172 172
173 173 def login_post(self):
174 174 c = self.load_default_context()
175 175
176 176 login_form = LoginForm(self.request.translate)()
177 177
178 178 try:
179 179 self.session.invalidate()
180 180 form_result = login_form.to_python(self.request.POST)
181 181 # form checks for username/password, now we're authenticated
182 182 headers = store_user_in_session(
183 183 self.session,
184 username=form_result['username'],
184 user_identifier=form_result['username'],
185 185 remember=form_result['remember'])
186 186 log.debug('Redirecting to "%s" after login.', c.came_from)
187 187
188 188 audit_user = audit_logger.UserWrap(
189 189 username=self.request.POST.get('username'),
190 190 ip_addr=self.request.remote_addr)
191 191 action_data = {'user_agent': self.request.user_agent}
192 192 audit_logger.store_web(
193 193 'user.login.success', action_data=action_data,
194 194 user=audit_user, commit=True)
195 195
196 196 raise HTTPFound(c.came_from, headers=headers)
197 197 except formencode.Invalid as errors:
198 198 defaults = errors.value
199 199 # remove password from filling in form again
200 200 defaults.pop('password', None)
201 201 render_ctx = {
202 202 'errors': errors.error_dict,
203 203 'defaults': defaults,
204 204 }
205 205
206 206 audit_user = audit_logger.UserWrap(
207 207 username=self.request.POST.get('username'),
208 208 ip_addr=self.request.remote_addr)
209 209 action_data = {'user_agent': self.request.user_agent}
210 210 audit_logger.store_web(
211 211 'user.login.failure', action_data=action_data,
212 212 user=audit_user, commit=True)
213 213 return self._get_template_context(c, **render_ctx)
214 214
215 215 except UserCreationError as e:
216 216 # headers auth or other auth functions that create users on
217 217 # the fly can throw this exception signaling that there's issue
218 218 # with user creation, explanation should be provided in
219 219 # Exception itself
220 220 h.flash(e, category='error')
221 221 return self._get_template_context(c)
222 222
223 223 @CSRFRequired()
224 224 def logout(self):
225 225 auth_user = self._rhodecode_user
226 226 log.info('Deleting session for user: `%s`', auth_user)
227 227
228 228 action_data = {'user_agent': self.request.user_agent}
229 229 audit_logger.store_web(
230 230 'user.logout', action_data=action_data,
231 231 user=auth_user, commit=True)
232 232 self.session.delete()
233 233 return HTTPFound(h.route_path('home'))
234 234
235 235 @HasPermissionAnyDecorator(
236 236 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
237 237 def register(self, defaults=None, errors=None):
238 238 c = self.load_default_context()
239 239 defaults = defaults or {}
240 240 errors = errors or {}
241 241
242 242 settings = SettingsModel().get_all_settings()
243 243 register_message = settings.get('rhodecode_register_message') or ''
244 244 captcha = self._get_captcha_data()
245 245 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
246 246 .AuthUser().permissions['global']
247 247
248 248 render_ctx = self._get_template_context(c)
249 249 render_ctx.update({
250 250 'defaults': defaults,
251 251 'errors': errors,
252 252 'auto_active': auto_active,
253 253 'captcha_active': captcha.active,
254 254 'captcha_public_key': captcha.public_key,
255 255 'register_message': register_message,
256 256 })
257 257 return render_ctx
258 258
259 259 @HasPermissionAnyDecorator(
260 260 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
261 261 def register_post(self):
262 262 from rhodecode.authentication.plugins import auth_rhodecode
263 263
264 264 self.load_default_context()
265 265 captcha = self._get_captcha_data()
266 266 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
267 267 .AuthUser().permissions['global']
268 268
269 269 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 270 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
271 271
272 272 register_form = RegisterForm(self.request.translate)()
273 273 try:
274 274
275 275 form_result = register_form.to_python(self.request.POST)
276 276 form_result['active'] = auto_active
277 277 external_identity = self.request.POST.get('external_identity')
278 278
279 279 if external_identity:
280 280 extern_name = external_identity
281 281 extern_type = external_identity
282 282
283 283 if captcha.active:
284 284 captcha_status, captcha_message = self.validate_captcha(
285 285 captcha.private_key)
286 286
287 287 if not captcha_status:
288 288 _value = form_result
289 289 _msg = _('Bad captcha')
290 290 error_dict = {'recaptcha_field': captcha_message}
291 291 raise formencode.Invalid(
292 292 _msg, _value, None, error_dict=error_dict)
293 293
294 294 new_user = UserModel().create_registration(
295 295 form_result, extern_name=extern_name, extern_type=extern_type)
296 296
297 297 action_data = {'data': new_user.get_api_data(),
298 298 'user_agent': self.request.user_agent}
299 299
300 300 if external_identity:
301 301 action_data['external_identity'] = external_identity
302 302
303 303 audit_user = audit_logger.UserWrap(
304 304 username=new_user.username,
305 305 user_id=new_user.user_id,
306 306 ip_addr=self.request.remote_addr)
307 307
308 308 audit_logger.store_web(
309 309 'user.register', action_data=action_data,
310 310 user=audit_user)
311 311
312 312 event = UserRegistered(user=new_user, session=self.session)
313 313 trigger(event)
314 314 h.flash(
315 315 _('You have successfully registered with RhodeCode. You can log-in now.'),
316 316 category='success')
317 317 if external_identity:
318 318 h.flash(
319 319 _('Please use the {identity} button to log-in').format(
320 320 identity=external_identity),
321 321 category='success')
322 322 Session().commit()
323 323
324 324 redirect_ro = self.request.route_path('login')
325 325 raise HTTPFound(redirect_ro)
326 326
327 327 except formencode.Invalid as errors:
328 328 errors.value.pop('password', None)
329 329 errors.value.pop('password_confirmation', None)
330 330 return self.register(
331 331 defaults=errors.value, errors=errors.error_dict)
332 332
333 333 except UserCreationError as e:
334 334 # container auth or other auth functions that create users on
335 335 # the fly can throw this exception signaling that there's issue
336 336 # with user creation, explanation should be provided in
337 337 # Exception itself
338 338 h.flash(e, category='error')
339 339 return self.register()
340 340
341 341 def password_reset(self):
342 342 c = self.load_default_context()
343 343 captcha = self._get_captcha_data()
344 344
345 345 template_context = {
346 346 'captcha_active': captcha.active,
347 347 'captcha_public_key': captcha.public_key,
348 348 'defaults': {},
349 349 'errors': {},
350 350 }
351 351
352 352 # always send implicit message to prevent from discovery of
353 353 # matching emails
354 354 msg = _('If such email exists, a password reset link was sent to it.')
355 355
356 356 def default_response():
357 357 log.debug('faking response on invalid password reset')
358 358 # make this take 2s, to prevent brute forcing.
359 359 time.sleep(2)
360 360 h.flash(msg, category='success')
361 361 return HTTPFound(self.request.route_path('reset_password'))
362 362
363 363 if self.request.POST:
364 364 if h.HasPermissionAny('hg.password_reset.disabled')():
365 365 _email = self.request.POST.get('email', '')
366 366 log.error('Failed attempt to reset password for `%s`.', _email)
367 367 h.flash(_('Password reset has been disabled.'), category='error')
368 368 return HTTPFound(self.request.route_path('reset_password'))
369 369
370 370 password_reset_form = PasswordResetForm(self.request.translate)()
371 371 description = 'Generated token for password reset from {}'.format(
372 372 datetime.datetime.now().isoformat())
373 373
374 374 try:
375 375 form_result = password_reset_form.to_python(
376 376 self.request.POST)
377 377 user_email = form_result['email']
378 378
379 379 if captcha.active:
380 380 captcha_status, captcha_message = self.validate_captcha(
381 381 captcha.private_key)
382 382
383 383 if not captcha_status:
384 384 _value = form_result
385 385 _msg = _('Bad captcha')
386 386 error_dict = {'recaptcha_field': captcha_message}
387 387 raise formencode.Invalid(
388 388 _msg, _value, None, error_dict=error_dict)
389 389
390 390 # Generate reset URL and send mail.
391 391 user = User.get_by_email(user_email)
392 392
393 393 # only allow rhodecode based users to reset their password
394 394 # external auth shouldn't allow password reset
395 395 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
396 396 log.warning('User %s with external type `%s` tried a password reset. '
397 397 'This try was rejected', user, user.extern_type)
398 398 return default_response()
399 399
400 400 # generate password reset token that expires in 10 minutes
401 401 reset_token = UserModel().add_auth_token(
402 402 user=user, lifetime_minutes=10,
403 403 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
404 404 description=description)
405 405 Session().commit()
406 406
407 407 log.debug('Successfully created password recovery token')
408 408 password_reset_url = self.request.route_url(
409 409 'reset_password_confirmation',
410 410 _query={'key': reset_token.api_key})
411 411 UserModel().reset_password_link(
412 412 form_result, password_reset_url)
413 413
414 414 action_data = {'email': user_email,
415 415 'user_agent': self.request.user_agent}
416 416 audit_logger.store_web(
417 417 'user.password.reset_request', action_data=action_data,
418 418 user=self._rhodecode_user, commit=True)
419 419
420 420 return default_response()
421 421
422 422 except formencode.Invalid as errors:
423 423 template_context.update({
424 424 'defaults': errors.value,
425 425 'errors': errors.error_dict,
426 426 })
427 427 if not self.request.POST.get('email'):
428 428 # case of empty email, we want to report that
429 429 return self._get_template_context(c, **template_context)
430 430
431 431 if 'recaptcha_field' in errors.error_dict:
432 432 # case of failed captcha
433 433 return self._get_template_context(c, **template_context)
434 434
435 435 return default_response()
436 436
437 437 return self._get_template_context(c, **template_context)
438 438
439 439 def password_reset_confirmation(self):
440 440 self.load_default_context()
441 441 if self.request.GET and self.request.GET.get('key'):
442 442 # make this take 2s, to prevent brute forcing.
443 443 time.sleep(2)
444 444
445 445 token = AuthTokenModel().get_auth_token(
446 446 self.request.GET.get('key'))
447 447
448 448 # verify token is the correct role
449 449 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
450 450 log.debug('Got token with role:%s expected is %s',
451 451 getattr(token, 'role', 'EMPTY_TOKEN'),
452 452 UserApiKeys.ROLE_PASSWORD_RESET)
453 453 h.flash(
454 454 _('Given reset token is invalid'), category='error')
455 455 return HTTPFound(self.request.route_path('reset_password'))
456 456
457 457 try:
458 458 owner = token.user
459 459 data = {'email': owner.email, 'token': token.api_key}
460 460 UserModel().reset_password(data)
461 461 h.flash(
462 462 _('Your password reset was successful, '
463 463 'a new password has been sent to your email'),
464 464 category='success')
465 465 except Exception as e:
466 466 log.error(e)
467 467 return HTTPFound(self.request.route_path('reset_password'))
468 468
469 469 return HTTPFound(self.request.route_path('login'))
@@ -1,825 +1,821 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Authentication modules
21 21 """
22 22 import socket
23 23 import string
24 24 import colander
25 25 import copy
26 26 import logging
27 27 import time
28 28 import traceback
29 29 import warnings
30 30 import functools
31 31
32 32 from pyramid.threadlocal import get_current_registry
33 33
34 34 from rhodecode.authentication import AuthenticationPluginRegistry
35 35 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.lib import rc_cache
38 38 from rhodecode.lib.statsd_client import StatsdClient
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.str_utils import safe_bytes
41 41 from rhodecode.lib.utils2 import safe_int, safe_str
42 42 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, 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 153
154 154 def __str__(self):
155 155 return self.get_id()
156 156
157 157 def _get_setting_full_name(self, name):
158 158 """
159 159 Return the full setting name used for storing values in the database.
160 160 """
161 161 # TODO: johbo: Using the name here is problematic. It would be good to
162 162 # introduce either new models in the database to hold Plugin and
163 163 # PluginSetting or to use the plugin id here.
164 164 return f'auth_{self.name}_{name}'
165 165
166 166 def _get_setting_type(self, name):
167 167 """
168 168 Return the type of a setting. This type is defined by the SettingsModel
169 169 and determines how the setting is stored in DB. Optionally the suffix
170 170 `.encrypted` is appended to instruct SettingsModel to store it
171 171 encrypted.
172 172 """
173 173 schema_node = self.get_settings_schema().get(name)
174 174 db_type = self._settings_type_map.get(
175 175 type(schema_node.typ), 'unicode')
176 176 if name in self._settings_encrypted:
177 177 db_type = f'{db_type}.encrypted'
178 178 return db_type
179 179
180 180 @classmethod
181 181 def docs(cls):
182 182 """
183 183 Defines documentation url which helps with plugin setup
184 184 """
185 185 return ''
186 186
187 187 @classmethod
188 188 def icon(cls):
189 189 """
190 190 Defines ICON in SVG format for authentication method
191 191 """
192 192 return ''
193 193
194 194 def is_enabled(self):
195 195 """
196 196 Returns true if this plugin is enabled. An enabled plugin can be
197 197 configured in the admin interface but it is not consulted during
198 198 authentication.
199 199 """
200 200 auth_plugins = SettingsModel().get_auth_plugins()
201 201 return self.get_id() in auth_plugins
202 202
203 203 def is_active(self, plugin_cached_settings=None):
204 204 """
205 205 Returns true if the plugin is activated. An activated plugin is
206 206 consulted during authentication, assumed it is also enabled.
207 207 """
208 208 return self.get_setting_by_name(
209 209 'enabled', plugin_cached_settings=plugin_cached_settings)
210 210
211 211 def get_id(self):
212 212 """
213 213 Returns the plugin id.
214 214 """
215 215 return self._plugin_id
216 216
217 217 def get_display_name(self, load_from_settings=False):
218 218 """
219 219 Returns a translation string for displaying purposes.
220 220 if load_from_settings is set, plugin settings can override the display name
221 221 """
222 222 raise NotImplementedError('Not implemented in base class')
223 223
224 224 def get_settings_schema(self):
225 225 """
226 226 Returns a colander schema, representing the plugin settings.
227 227 """
228 228 return AuthnPluginSettingsSchemaBase()
229 229
230 230 def _propagate_settings(self, raw_settings):
231 231 settings = {}
232 232 for node in self.get_settings_schema():
233 233 settings[node.name] = self.get_setting_by_name(
234 234 node.name, plugin_cached_settings=raw_settings)
235 235 return settings
236 236
237 237 def get_settings(self, use_cache=True):
238 238 """
239 239 Returns the plugin settings as dictionary.
240 240 """
241 241
242 242 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
243 243 settings = self._propagate_settings(raw_settings)
244 244
245 245 return settings
246 246
247 247 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
248 248 """
249 249 Returns a plugin setting by name.
250 250 """
251 251 full_name = f'rhodecode_{self._get_setting_full_name(name)}'
252 252 if plugin_cached_settings:
253 253 plugin_settings = plugin_cached_settings
254 254 else:
255 255 plugin_settings = SettingsModel().get_all_settings()
256 256
257 257 if full_name in plugin_settings:
258 258 return plugin_settings[full_name]
259 259 else:
260 260 return default
261 261
262 262 def create_or_update_setting(self, name, value):
263 263 """
264 264 Create or update a setting for this plugin in the persistent storage.
265 265 """
266 266 full_name = self._get_setting_full_name(name)
267 267 type_ = self._get_setting_type(name)
268 268 db_setting = SettingsModel().create_or_update_setting(
269 269 full_name, value, type_)
270 270 return db_setting.app_settings_value
271 271
272 272 def log_safe_settings(self, settings):
273 273 """
274 274 returns a log safe representation of settings, without any secrets
275 275 """
276 276 settings_copy = copy.deepcopy(settings)
277 277 for k in self._settings_unsafe_keys:
278 278 if k in settings_copy:
279 279 del settings_copy[k]
280 280 return settings_copy
281 281
282 282 @hybrid_property
283 283 def name(self):
284 284 """
285 285 Returns the name of this authentication plugin.
286 286
287 287 :returns: string
288 288 """
289 289 raise NotImplementedError("Not implemented in base class")
290 290
291 291 def get_url_slug(self):
292 292 """
293 293 Returns a slug which should be used when constructing URLs which refer
294 294 to this plugin. By default it returns the plugin name. If the name is
295 295 not suitable for using it in an URL the plugin should override this
296 296 method.
297 297 """
298 298 return self.name
299 299
300 300 @property
301 301 def is_headers_auth(self):
302 302 """
303 303 Returns True if this authentication plugin uses HTTP headers as
304 304 authentication method.
305 305 """
306 306 return False
307 307
308 308 @hybrid_property
309 309 def is_container_auth(self):
310 310 """
311 311 Deprecated method that indicates if this authentication plugin uses
312 312 HTTP headers as authentication method.
313 313 """
314 314 warnings.warn(
315 315 'Use is_headers_auth instead.', category=DeprecationWarning)
316 316 return self.is_headers_auth
317 317
318 318 @hybrid_property
319 319 def allows_creating_users(self):
320 320 """
321 321 Defines if Plugin allows users to be created on-the-fly when
322 322 authentication is called. Controls how external plugins should behave
323 323 in terms if they are allowed to create new users, or not. Base plugins
324 324 should not be allowed to, but External ones should be !
325 325
326 326 :return: bool
327 327 """
328 328 return False
329 329
330 330 def set_auth_type(self, auth_type):
331 331 self.auth_type = auth_type
332 332
333 333 def set_calling_scope_repo(self, acl_repo_name):
334 334 self.acl_repo_name = acl_repo_name
335 335
336 336 def allows_authentication_from(
337 337 self, user, allows_non_existing_user=True,
338 338 allowed_auth_plugins=None, allowed_auth_sources=None):
339 339 """
340 340 Checks if this authentication module should accept a request for
341 341 the current user.
342 342
343 343 :param user: user object fetched using plugin's get_user() method.
344 344 :param allows_non_existing_user: if True, don't allow the
345 345 user to be empty, meaning not existing in our database
346 346 :param allowed_auth_plugins: if provided, users extern_type will be
347 347 checked against a list of provided extern types, which are plugin
348 348 auth_names in the end
349 349 :param allowed_auth_sources: authentication type allowed,
350 350 `http` or `vcs` default is both.
351 351 defines if plugin will accept only http authentication vcs
352 352 authentication(git/hg) or both
353 353 :returns: boolean
354 354 """
355 355 if not user and not allows_non_existing_user:
356 356 log.debug('User is empty but plugin does not allow empty users,'
357 357 'not allowed to authenticate')
358 358 return False
359 359
360 360 expected_auth_plugins = allowed_auth_plugins or [self.name]
361 361 if user and (user.extern_type and
362 362 user.extern_type not in expected_auth_plugins):
363 363 log.debug(
364 364 'User `%s` is bound to `%s` auth type. Plugin allows only '
365 365 '%s, skipping', user, user.extern_type, expected_auth_plugins)
366 366
367 367 return False
368 368
369 369 # by default accept both
370 370 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
371 371 if self.auth_type not in expected_auth_from:
372 372 log.debug('Current auth source is %s but plugin only allows %s',
373 373 self.auth_type, expected_auth_from)
374 374 return False
375 375
376 376 return True
377 377
378 378 def get_user(self, username=None, **kwargs):
379 379 """
380 380 Helper method for user fetching in plugins, by default it's using
381 381 simple fetch by username, but this method can be customized in plugins
382 382 eg. headers auth plugin to fetch user by environ params
383 383
384 384 :param username: username if given to fetch from database
385 385 :param kwargs: extra arguments needed for user fetching.
386 386 """
387 387
388 388 user = None
389 389 log.debug(
390 390 'Trying to fetch user `%s` from RhodeCode database', username)
391 391 if username:
392 user = User.get_by_username(username)
393 if not user:
394 log.debug('User not found, fallback to fetch user in '
395 'case insensitive mode')
396 user = User.get_by_username(username, case_insensitive=True)
392 user = User.get_by_username_or_primary_email(username)
397 393 else:
398 394 log.debug('provided username:`%s` is empty skipping...', username)
399 395 if not user:
400 396 log.debug('User `%s` not found in database', username)
401 397 else:
402 398 log.debug('Got DB user:%s', user)
403 399 return user
404 400
405 401 def user_activation_state(self):
406 402 """
407 403 Defines user activation state when creating new users
408 404
409 405 :returns: boolean
410 406 """
411 407 raise NotImplementedError("Not implemented in base class")
412 408
413 409 def auth(self, userobj, username, passwd, settings, **kwargs):
414 410 """
415 411 Given a user object (which may be null), username, a plaintext
416 412 password, and a settings object (containing all the keys needed as
417 413 listed in settings()), authenticate this user's login attempt.
418 414
419 415 Return None on failure. On success, return a dictionary of the form:
420 416
421 417 see: RhodeCodeAuthPluginBase.auth_func_attrs
422 418 This is later validated for correctness
423 419 """
424 420 raise NotImplementedError("not implemented in base class")
425 421
426 422 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
427 423 """
428 424 Wrapper to call self.auth() that validates call on it
429 425
430 426 :param userobj: userobj
431 427 :param username: username
432 428 :param passwd: plaintext password
433 429 :param settings: plugin settings
434 430 """
435 431 auth = self.auth(userobj, username, passwd, settings, **kwargs)
436 432 if auth:
437 433 auth['_plugin'] = self.name
438 434 auth['_ttl_cache'] = self.get_ttl_cache(settings)
439 435 # check if hash should be migrated ?
440 436 new_hash = auth.get('_hash_migrate')
441 437 if new_hash:
442 438 # new_hash is a newly encrypted destination hash
443 439 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
444 440 if 'user_group_sync' not in auth:
445 441 auth['user_group_sync'] = False
446 442 return self._validate_auth_return(auth)
447 443 return auth
448 444
449 445 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
450 446 new_hash_cypher = _RhodeCodeCryptoBCrypt()
451 447 # extra checks, so make sure new hash is correct.
452 448 password_as_bytes = safe_bytes(password)
453 449
454 450 if new_hash and new_hash_cypher.hash_check(password_as_bytes, new_hash):
455 451 cur_user = User.get_by_username(username)
456 452 cur_user.password = new_hash
457 453 Session().add(cur_user)
458 454 Session().flush()
459 455 log.info('Migrated user %s hash to bcrypt', cur_user)
460 456
461 457 def _validate_auth_return(self, ret):
462 458 if not isinstance(ret, dict):
463 459 raise Exception('returned value from auth must be a dict')
464 460 for k in self.auth_func_attrs:
465 461 if k not in ret:
466 462 raise Exception('Missing %s attribute from returned data' % k)
467 463 return ret
468 464
469 465 def get_ttl_cache(self, settings=None):
470 466 plugin_settings = settings or self.get_settings()
471 467 # we set default to 30, we make a compromise here,
472 468 # performance > security, mostly due to LDAP/SVN, majority
473 469 # of users pick cache_ttl to be enabled
474 470 from rhodecode.authentication import plugin_default_auth_ttl
475 471 cache_ttl = plugin_default_auth_ttl
476 472
477 473 if isinstance(self.AUTH_CACHE_TTL, int):
478 474 # plugin cache set inside is more important than the settings value
479 475 cache_ttl = self.AUTH_CACHE_TTL
480 476 elif 'cache_ttl' in plugin_settings:
481 477 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
482 478
483 479 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
484 480 return plugin_cache_active, cache_ttl
485 481
486 482
487 483 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
488 484
489 485 @hybrid_property
490 486 def allows_creating_users(self):
491 487 return True
492 488
493 489 def use_fake_password(self):
494 490 """
495 491 Return a boolean that indicates whether or not we should set the user's
496 492 password to a random value when it is authenticated by this plugin.
497 493 If your plugin provides authentication, then you will generally
498 494 want this.
499 495
500 496 :returns: boolean
501 497 """
502 498 raise NotImplementedError("Not implemented in base class")
503 499
504 500 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
505 501 # at this point _authenticate calls plugin's `auth()` function
506 502 auth = super()._authenticate(
507 503 userobj, username, passwd, settings, **kwargs)
508 504
509 505 if auth:
510 506 # maybe plugin will clean the username ?
511 507 # we should use the return value
512 508 username = auth['username']
513 509
514 510 # if external source tells us that user is not active, we should
515 511 # skip rest of the process. This can prevent from creating users in
516 512 # RhodeCode when using external authentication, but if it's
517 513 # inactive user we shouldn't create that user anyway
518 514 if auth['active_from_extern'] is False:
519 515 log.warning(
520 516 "User %s authenticated against %s, but is inactive",
521 517 username, self.__module__)
522 518 return None
523 519
524 520 cur_user = User.get_by_username(username, case_insensitive=True)
525 521 is_user_existing = cur_user is not None
526 522
527 523 if is_user_existing:
528 524 log.debug('Syncing user `%s` from '
529 525 '`%s` plugin', username, self.name)
530 526 else:
531 527 log.debug('Creating non existing user `%s` from '
532 528 '`%s` plugin', username, self.name)
533 529
534 530 if self.allows_creating_users:
535 531 log.debug('Plugin `%s` allows to '
536 532 'create new users', self.name)
537 533 else:
538 534 log.debug('Plugin `%s` does not allow to '
539 535 'create new users', self.name)
540 536
541 537 user_parameters = {
542 538 'username': username,
543 539 'email': auth["email"],
544 540 'firstname': auth["firstname"],
545 541 'lastname': auth["lastname"],
546 542 'active': auth["active"],
547 543 'admin': auth["admin"],
548 544 'extern_name': auth["extern_name"],
549 545 'extern_type': self.name,
550 546 'plugin': self,
551 547 'allow_to_create_user': self.allows_creating_users,
552 548 }
553 549
554 550 if not is_user_existing:
555 551 if self.use_fake_password():
556 552 # Randomize the PW because we don't need it, but don't want
557 553 # them blank either
558 554 passwd = PasswordGenerator().gen_password(length=16)
559 555 user_parameters['password'] = passwd
560 556 else:
561 557 # Since the password is required by create_or_update method of
562 558 # UserModel, we need to set it explicitly.
563 559 # The create_or_update method is smart and recognises the
564 560 # password hashes as well.
565 561 user_parameters['password'] = cur_user.password
566 562
567 563 # we either create or update users, we also pass the flag
568 564 # that controls if this method can actually do that.
569 565 # raises NotAllowedToCreateUserError if it cannot, and we try to.
570 566 user = UserModel().create_or_update(**user_parameters)
571 567 Session().flush()
572 568 # enforce user is just in given groups, all of them has to be ones
573 569 # created from plugins. We store this info in _group_data JSON
574 570 # field
575 571
576 572 if auth['user_group_sync']:
577 573 try:
578 574 groups = auth['groups'] or []
579 575 log.debug(
580 576 'Performing user_group sync based on set `%s` '
581 577 'returned by `%s` plugin', groups, self.name)
582 578 UserGroupModel().enforce_groups(user, groups, self.name)
583 579 except Exception:
584 580 # for any reason group syncing fails, we should
585 581 # proceed with login
586 582 log.error(traceback.format_exc())
587 583
588 584 Session().commit()
589 585 return auth
590 586
591 587
592 588 class AuthLdapBase(object):
593 589
594 590 @classmethod
595 591 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
596 592
597 593 def host_resolver(host, port, full_resolve=True):
598 594 """
599 595 Main work for this function is to prevent ldap connection issues,
600 596 and detect them early using a "greenified" sockets
601 597 """
602 598 host = host.strip()
603 599 if not full_resolve:
604 600 return f'{host}:{port}'
605 601
606 602 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
607 603 try:
608 604 ip = socket.gethostbyname(host)
609 605 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
610 606 except Exception:
611 607 raise LdapConnectionError(f'Failed to resolve host: `{host}`')
612 608
613 609 log.debug('LDAP: Checking if IP %s is accessible', ip)
614 610 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
615 611 try:
616 612 s.connect((ip, int(port)))
617 613 s.shutdown(socket.SHUT_RD)
618 614 log.debug('LDAP: connection to %s successful', ip)
619 615 except Exception:
620 616 raise LdapConnectionError(
621 617 f'Failed to connect to host: `{host}:{port}`')
622 618
623 619 return f'{host}:{port}'
624 620
625 621 if len(ldap_server) == 1:
626 622 # in case of single server use resolver to detect potential
627 623 # connection issues
628 624 full_resolve = True
629 625 else:
630 626 full_resolve = False
631 627
632 628 return ', '.join(
633 629 ["{}://{}".format(
634 630 ldap_server_type,
635 631 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
636 632 for host in ldap_server])
637 633
638 634 @classmethod
639 635 def _get_server_list(cls, servers):
640 636 return [s.strip() for s in servers.split(',')]
641 637
642 638 @classmethod
643 639 def get_uid(cls, username, server_addresses):
644 640 uid = username
645 641 for server_addr in server_addresses:
646 642 uid = chop_at(username, "@%s" % server_addr)
647 643 return uid
648 644
649 645 @classmethod
650 646 def validate_username(cls, username):
651 647 if "," in username:
652 648 raise LdapUsernameError(
653 649 f"invalid character `,` in username: `{username}`")
654 650
655 651 @classmethod
656 652 def validate_password(cls, username, password):
657 653 if not password:
658 654 msg = "Authenticating user %s with blank password not allowed"
659 655 log.warning(msg, username)
660 656 raise LdapPasswordError(msg)
661 657
662 658
663 659 def loadplugin(plugin_id):
664 660 """
665 661 Loads and returns an instantiated authentication plugin.
666 662 Returns the RhodeCodeAuthPluginBase subclass on success,
667 663 or None on failure.
668 664 """
669 665 # TODO: Disusing pyramids thread locals to retrieve the registry.
670 666 authn_registry = get_authn_registry()
671 667 plugin = authn_registry.get_plugin(plugin_id)
672 668 if plugin is None:
673 669 log.error('Authentication plugin not found: "%s"', plugin_id)
674 670 return plugin
675 671
676 672
677 673 def get_authn_registry(registry=None) -> AuthenticationPluginRegistry:
678 674 registry = registry or get_current_registry()
679 675 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
680 676 return authn_registry
681 677
682 678
683 679 def authenticate(username, password, environ=None, auth_type=None,
684 680 skip_missing=False, registry=None, acl_repo_name=None):
685 681 """
686 682 Authentication function used for access control,
687 683 It tries to authenticate based on enabled authentication modules.
688 684
689 685 :param username: username can be empty for headers auth
690 686 :param password: password can be empty for headers auth
691 687 :param environ: environ headers passed for headers auth
692 688 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
693 689 :param skip_missing: ignores plugins that are in db but not in environment
694 690 :param registry: pyramid registry
695 691 :param acl_repo_name: name of repo for ACL checks
696 692 :returns: None if auth failed, plugin_user dict if auth is correct
697 693 """
698 694 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
699 695 raise ValueError(f'auth type must be on of http, vcs got "{auth_type}" instead')
700 696
701 697 auth_credentials = (username and password)
702 698 headers_only = environ and not auth_credentials
703 699
704 700 authn_registry = get_authn_registry(registry)
705 701
706 702 plugins_to_check = authn_registry.get_plugins_for_authentication()
707 703 log.debug('authentication: headers=%s, username_and_passwd=%s', headers_only, bool(auth_credentials))
708 704 log.debug('Starting ordered authentication chain using %s plugins',
709 705 [x.name for x in plugins_to_check])
710 706
711 707 for plugin in plugins_to_check:
712 708 plugin.set_auth_type(auth_type)
713 709 plugin.set_calling_scope_repo(acl_repo_name)
714 710
715 711 if headers_only and not plugin.is_headers_auth:
716 712 log.debug('Auth type is for headers only and plugin `%s` is not '
717 713 'headers plugin, skipping...', plugin.get_id())
718 714 continue
719 715
720 716 log.debug('Trying authentication using ** %s **', plugin.get_id())
721 717
722 718 # load plugin settings from RhodeCode database
723 719 plugin_settings = plugin.get_settings()
724 720 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
725 721 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
726 722
727 723 # use plugin's method of user extraction.
728 724 user = plugin.get_user(username, environ=environ,
729 725 settings=plugin_settings)
730 726 display_user = user.username if user else username
731 727 log.debug(
732 728 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
733 729
734 730 if not plugin.allows_authentication_from(user):
735 731 log.debug('Plugin %s does not accept user `%s` for authentication',
736 732 plugin.get_id(), display_user)
737 733 continue
738 734 else:
739 735 log.debug('Plugin %s accepted user `%s` for authentication',
740 736 plugin.get_id(), display_user)
741 737
742 738 log.info('Authenticating user `%s` using %s plugin',
743 739 display_user, plugin.get_id())
744 740
745 741 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
746 742
747 743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
748 744 plugin.get_id(), plugin_cache_active, cache_ttl)
749 745
750 746 user_id = user.user_id if user else 'no-user'
751 747 # don't cache for empty users
752 748 plugin_cache_active = plugin_cache_active and user_id
753 749 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
754 750 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
755 751
756 752 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
757 753 expiration_time=cache_ttl,
758 754 condition=plugin_cache_active)
759 755 def compute_auth(
760 756 cache_name, plugin_name, username, password):
761 757
762 758 # _authenticate is a wrapper for .auth() method of plugin.
763 759 # it checks if .auth() sends proper data.
764 760 # For RhodeCodeExternalAuthPlugin it also maps users to
765 761 # Database and maps the attributes returned from .auth()
766 762 # to RhodeCode database. If this function returns data
767 763 # then auth is correct.
768 764 log.debug('Running plugin `%s` _authenticate method '
769 765 'using username and password', plugin.get_id())
770 766 return plugin._authenticate(
771 767 user, username, password, plugin_settings,
772 768 environ=environ or {})
773 769
774 770 start = time.time()
775 771 # for environ based auth, password can be empty, but then the validation is
776 772 # on the server that fills in the env data needed for authentication
777 773 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
778 774
779 775 auth_time = time.time() - start
780 776 log.debug('Authentication for plugin `%s` completed in %.4fs, '
781 777 'expiration time of fetched cache %.1fs.',
782 778 plugin.get_id(), auth_time, cache_ttl,
783 779 extra={"plugin": plugin.get_id(), "time": auth_time})
784 780
785 781 log.debug('PLUGIN USER DATA: %s', plugin_user)
786 782
787 783 statsd = StatsdClient.statsd
788 784
789 785 if plugin_user:
790 786 log.debug('Plugin returned proper authentication data')
791 787 if statsd:
792 788 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
793 789 statsd.incr('rhodecode_login_success_total')
794 790 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
795 791 tags=[f"plugin:{plugin.get_id()}"],
796 792 use_decimals=False
797 793 )
798 794 return plugin_user
799 795
800 796 # we failed to Auth because .auth() method didn't return proper user
801 797 log.debug("User `%s` failed to authenticate against %s",
802 798 display_user, plugin.get_id())
803 799 if statsd:
804 800 statsd.incr('rhodecode_login_fail_total')
805 801
806 802 # case when we failed to authenticate against all defined plugins
807 803 return None
808 804
809 805
810 806 def chop_at(s, sub, inclusive=False):
811 807 """Truncate string ``s`` at the first occurrence of ``sub``.
812 808
813 809 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
814 810
815 811 >>> chop_at("plutocratic brats", "rat")
816 812 'plutoc'
817 813 >>> chop_at("plutocratic brats", "rat", True)
818 814 'plutocrat'
819 815 """
820 816 pos = s.find(sub)
821 817 if pos == -1:
822 818 return s
823 819 if inclusive:
824 820 return s[:pos+len(sub)]
825 821 return s[:pos]
@@ -1,220 +1,220 b''
1 1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 RhodeCode authentication plugin for built in internal auth
21 21 """
22 22
23 23 import logging
24 24
25 25 import colander
26 26
27 27 from rhodecode.translation import _
28 28 from rhodecode.lib.utils2 import safe_bytes
29 29 from rhodecode.model.db import User
30 30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
31 31 from rhodecode.authentication.base import (
32 32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def plugin_factory(plugin_id, *args, **kwargs):
39 39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 40 return plugin
41 41
42 42
43 43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 44 pass
45 45
46 46
47 47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 48 uid = 'rhodecode'
49 49 AUTH_RESTRICTION_NONE = 'user_all'
50 50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
51 51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
52 52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
53 53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54 54
55 55 def includeme(self, config):
56 56 config.add_authn_plugin(self)
57 57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 58 config.add_view(
59 59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 60 attr='settings_get',
61 61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 62 request_method='GET',
63 63 route_name='auth_home',
64 64 context=RhodecodeAuthnResource)
65 65 config.add_view(
66 66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 67 attr='settings_post',
68 68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 69 request_method='POST',
70 70 route_name='auth_home',
71 71 context=RhodecodeAuthnResource)
72 72
73 73 def get_settings_schema(self):
74 74 return RhodeCodeSettingsSchema()
75 75
76 76 def get_display_name(self, load_from_settings=False):
77 77 return _('RhodeCode Internal')
78 78
79 79 @classmethod
80 80 def docs(cls):
81 81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
82 82
83 83 @hybrid_property
84 84 def name(self):
85 85 return "rhodecode"
86 86
87 87 def user_activation_state(self):
88 88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 89 return 'hg.register.auto_activate' in def_user_perms
90 90
91 91 def allows_authentication_from(
92 92 self, user, allows_non_existing_user=True,
93 93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 94 """
95 95 Custom method for this auth that doesn't accept non existing users.
96 96 We know that user exists in our database.
97 97 """
98 98 allows_non_existing_user = False
99 99 return super().allows_authentication_from(
100 100 user, allows_non_existing_user=allows_non_existing_user)
101 101
102 102 def auth(self, userobj, username, password, settings, **kwargs):
103 103 if not userobj:
104 104 log.debug('userobj was:%s skipping', userobj)
105 105 return None
106 106
107 107 if userobj.extern_type != self.name:
108 108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 109 userobj, userobj.extern_type, self.name)
110 110 return None
111 111
112 112 # check scope of auth
113 113 scope_restriction = settings.get('scope_restriction', '')
114 114
115 115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 116 and self.auth_type != HTTP_TYPE:
117 117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
118 118 userobj, self.auth_type, scope_restriction)
119 119 return None
120 120
121 121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
122 122 and self.auth_type != VCS_TYPE:
123 123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
124 124 userobj, self.auth_type, scope_restriction)
125 125 return None
126 126
127 127 # check super-admin restriction
128 128 auth_restriction = settings.get('auth_restriction', '')
129 129
130 130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
131 131 and userobj.admin is False:
132 132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
133 133 userobj, auth_restriction)
134 134 return None
135 135
136 136 user_attrs = {
137 137 "username": userobj.username,
138 138 "firstname": userobj.firstname,
139 139 "lastname": userobj.lastname,
140 140 "groups": [],
141 141 'user_group_sync': False,
142 142 "email": userobj.email,
143 143 "admin": userobj.admin,
144 144 "active": userobj.active,
145 145 "active_from_extern": userobj.active,
146 146 "extern_name": userobj.user_id,
147 147 "extern_type": userobj.extern_type,
148 148 }
149 149
150 150 log.debug("User attributes:%s", user_attrs)
151 151 if userobj.active:
152 152 from rhodecode.lib import auth
153 153 crypto_backend = auth.crypto_backend()
154 154 password_encoded = safe_bytes(password)
155 155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
156 156 password_encoded, userobj.password or '')
157 157
158 158 if password_match and new_hash:
159 159 log.debug('user %s properly authenticated, but '
160 160 'requires hash change to bcrypt', userobj)
161 161 # if password match, and we use OLD deprecated hash,
162 162 # we should migrate this user hash password to the new hash
163 163 # we store the new returned by hash_check_with_upgrade function
164 164 user_attrs['_hash_migrate'] = new_hash
165 165
166 166 if userobj.username == User.DEFAULT_USER and userobj.active:
167 167 log.info('user `%s` authenticated correctly as anonymous user',
168 168 userobj.username,
169 169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 170 return user_attrs
171 171
172 elif userobj.username == username and password_match:
172 elif (userobj.username == username or userobj.email == username) and password_match:
173 173 log.info('user `%s` authenticated correctly', userobj.username,
174 174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 175 return user_attrs
176 176 log.warning("user `%s` used a wrong password when "
177 177 "authenticating on this plugin", userobj.username)
178 178 return None
179 179 else:
180 180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 181 'active.', username, self.name)
182 182 return None
183 183
184 184
185 185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
186 186
187 187 auth_restriction_choices = [
188 188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 190 ]
191 191
192 192 auth_scope_choices = [
193 193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 195 ]
196 196
197 197 auth_restriction = colander.SchemaNode(
198 198 colander.String(),
199 199 default=auth_restriction_choices[0],
200 200 description=_('Allowed user types for authentication using this plugin.'),
201 201 title=_('User restriction'),
202 202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 203 widget='select_with_labels',
204 204 choices=auth_restriction_choices
205 205 )
206 206 scope_restriction = colander.SchemaNode(
207 207 colander.String(),
208 208 default=auth_scope_choices[0],
209 209 description=_('Allowed protocols for authentication using this plugin. '
210 210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 211 title=_('Scope restriction'),
212 212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 213 widget='select_with_labels',
214 214 choices=auth_scope_choices
215 215 )
216 216
217 217
218 218 def includeme(config):
219 219 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
220 220 plugin_factory(plugin_id).includeme(config)
@@ -1,5884 +1,5890 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 36 from sqlalchemy import (
37 37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 true, false, null,
38 true, false, null, union_all,
39 39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 41 Text, Float, PickleType, BigInteger)
42 42 from sqlalchemy.sql.expression import case
43 43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 44 from sqlalchemy.orm import (
45 45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 46 from sqlalchemy.ext.declarative import declared_attr
47 47 from sqlalchemy.ext.hybrid import hybrid_property
48 48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 51 from pyramid.threadlocal import get_current_request
52 52 from webhelpers2.text import remove_formatting
53 53
54 54 from rhodecode.lib.str_utils import safe_bytes
55 55 from rhodecode.translation import _
56 56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 57 from rhodecode.lib.vcs.backends.base import (
58 58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 59 from rhodecode.lib.utils2 import (
60 60 str2bool, safe_str, get_commit_safe, sha1_safe,
61 61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 63 from rhodecode.lib.jsonalchemy import (
64 64 MutationObj, MutationList, JsonType, JsonRaw)
65 65 from rhodecode.lib.hash_utils import sha1
66 66 from rhodecode.lib import ext_json
67 67 from rhodecode.lib import enc_utils
68 68 from rhodecode.lib.ext_json import json, str_json
69 69 from rhodecode.lib.caching_query import FromCache
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY: bytes = b''
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 extra_sort_num = '1' # default
107 107
108 108 # NOTE(dan): inactive duplicates goes last
109 109 if getattr(obj, 'duplicate_perm', None):
110 110 extra_sort_num = '9'
111 111 return prefix + extra_sort_num + obj.username
112 112
113 113
114 114 def display_user_group_sort(obj):
115 115 """
116 116 Sort function used to sort permissions in .permissions() function of
117 117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 118 of all other resources
119 119 """
120 120
121 121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 122 return prefix + obj.users_group_name
123 123
124 124
125 125 def _hash_key(k):
126 126 return sha1_safe(k)
127 127
128 128
129 129 def in_filter_generator(qry, items, limit=500):
130 130 """
131 131 Splits IN() into multiple with OR
132 132 e.g.::
133 133 cnt = Repository.query().filter(
134 134 or_(
135 135 *in_filter_generator(Repository.repo_id, range(100000))
136 136 )).count()
137 137 """
138 138 if not items:
139 139 # empty list will cause empty query which might cause security issues
140 140 # this can lead to hidden unpleasant results
141 141 items = [-1]
142 142
143 143 parts = []
144 144 for chunk in range(0, len(items), limit):
145 145 parts.append(
146 146 qry.in_(items[chunk: chunk + limit])
147 147 )
148 148
149 149 return parts
150 150
151 151
152 152 base_table_args = {
153 153 'extend_existing': True,
154 154 'mysql_engine': 'InnoDB',
155 155 'mysql_charset': 'utf8',
156 156 'sqlite_autoincrement': True
157 157 }
158 158
159 159
160 160 class EncryptedTextValue(TypeDecorator):
161 161 """
162 162 Special column for encrypted long text data, use like::
163 163
164 164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 165
166 166 This column is intelligent so if value is in unencrypted form it return
167 167 unencrypted form, but on save it always encrypts
168 168 """
169 169 cache_ok = True
170 170 impl = Text
171 171
172 172 def process_bind_param(self, value, dialect):
173 173 """
174 174 Setter for storing value
175 175 """
176 176 import rhodecode
177 177 if not value:
178 178 return value
179 179
180 180 # protect against double encrypting if values is already encrypted
181 181 if value.startswith('enc$aes$') \
182 182 or value.startswith('enc$aes_hmac$') \
183 183 or value.startswith('enc2$'):
184 184 raise ValueError('value needs to be in unencrypted format, '
185 185 'ie. not starting with enc$ or enc2$')
186 186
187 187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 189 return safe_str(bytes_val)
190 190
191 191 def process_result_value(self, value, dialect):
192 192 """
193 193 Getter for retrieving value
194 194 """
195 195
196 196 import rhodecode
197 197 if not value:
198 198 return value
199 199
200 200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201 201
202 202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203 203
204 204 return safe_str(bytes_val)
205 205
206 206
207 207 class BaseModel(object):
208 208 """
209 209 Base Model for all classes
210 210 """
211 211
212 212 @classmethod
213 213 def _get_keys(cls):
214 214 """return column names for this model """
215 215 return class_mapper(cls).c.keys()
216 216
217 217 def get_dict(self):
218 218 """
219 219 return dict with keys and values corresponding
220 220 to this model data """
221 221
222 222 d = {}
223 223 for k in self._get_keys():
224 224 d[k] = getattr(self, k)
225 225
226 226 # also use __json__() if present to get additional fields
227 227 _json_attr = getattr(self, '__json__', None)
228 228 if _json_attr:
229 229 # update with attributes from __json__
230 230 if callable(_json_attr):
231 231 _json_attr = _json_attr()
232 232 for k, val in _json_attr.items():
233 233 d[k] = val
234 234 return d
235 235
236 236 def get_appstruct(self):
237 237 """return list with keys and values tuples corresponding
238 238 to this model data """
239 239
240 240 lst = []
241 241 for k in self._get_keys():
242 242 lst.append((k, getattr(self, k),))
243 243 return lst
244 244
245 245 def populate_obj(self, populate_dict):
246 246 """populate model with data from given populate_dict"""
247 247
248 248 for k in self._get_keys():
249 249 if k in populate_dict:
250 250 setattr(self, k, populate_dict[k])
251 251
252 252 @classmethod
253 253 def query(cls):
254 254 return Session().query(cls)
255 255
256 256 @classmethod
257 257 def select(cls, custom_cls=None):
258 258 """
259 259 stmt = cls.select().where(cls.user_id==1)
260 260 # optionally
261 261 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 262 result = cls.execute(stmt) | cls.scalars(stmt)
263 263 """
264 264
265 265 if custom_cls:
266 266 stmt = select(custom_cls)
267 267 else:
268 268 stmt = select(cls)
269 269 return stmt
270 270
271 271 @classmethod
272 272 def execute(cls, stmt):
273 273 return Session().execute(stmt)
274 274
275 275 @classmethod
276 276 def scalars(cls, stmt):
277 277 return Session().scalars(stmt)
278 278
279 279 @classmethod
280 280 def get(cls, id_):
281 281 if id_:
282 282 return cls.query().get(id_)
283 283
284 284 @classmethod
285 285 def get_or_404(cls, id_):
286 286 from pyramid.httpexceptions import HTTPNotFound
287 287
288 288 try:
289 289 id_ = int(id_)
290 290 except (TypeError, ValueError):
291 291 raise HTTPNotFound()
292 292
293 293 res = cls.query().get(id_)
294 294 if not res:
295 295 raise HTTPNotFound()
296 296 return res
297 297
298 298 @classmethod
299 299 def getAll(cls):
300 300 # deprecated and left for backward compatibility
301 301 return cls.get_all()
302 302
303 303 @classmethod
304 304 def get_all(cls):
305 305 return cls.query().all()
306 306
307 307 @classmethod
308 308 def delete(cls, id_):
309 309 obj = cls.query().get(id_)
310 310 Session().delete(obj)
311 311
312 312 @classmethod
313 313 def identity_cache(cls, session, attr_name, value):
314 314 exist_in_session = []
315 315 for (item_cls, pkey), instance in session.identity_map.items():
316 316 if cls == item_cls and getattr(instance, attr_name) == value:
317 317 exist_in_session.append(instance)
318 318 if exist_in_session:
319 319 if len(exist_in_session) == 1:
320 320 return exist_in_session[0]
321 321 log.exception(
322 322 'multiple objects with attr %s and '
323 323 'value %s found with same name: %r',
324 324 attr_name, value, exist_in_session)
325 325
326 326 @property
327 327 def cls_name(self):
328 328 return self.__class__.__name__
329 329
330 330 def __repr__(self):
331 331 return f'<DB:{self.cls_name}>'
332 332
333 333
334 334 class RhodeCodeSetting(Base, BaseModel):
335 335 __tablename__ = 'rhodecode_settings'
336 336 __table_args__ = (
337 337 UniqueConstraint('app_settings_name'),
338 338 base_table_args
339 339 )
340 340
341 341 SETTINGS_TYPES = {
342 342 'str': safe_str,
343 343 'int': safe_int,
344 344 'unicode': safe_str,
345 345 'bool': str2bool,
346 346 'list': functools.partial(aslist, sep=',')
347 347 }
348 348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 349 GLOBAL_CONF_KEY = 'app_settings'
350 350
351 351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355 355
356 356 def __init__(self, key='', val='', type='unicode'):
357 357 self.app_settings_name = key
358 358 self.app_settings_type = type
359 359 self.app_settings_value = val
360 360
361 361 @validates('_app_settings_value')
362 362 def validate_settings_value(self, key, val):
363 363 assert type(val) == str
364 364 return val
365 365
366 366 @hybrid_property
367 367 def app_settings_value(self):
368 368 v = self._app_settings_value
369 369 _type = self.app_settings_type
370 370 if _type:
371 371 _type = self.app_settings_type.split('.')[0]
372 372 # decode the encrypted value
373 373 if 'encrypted' in self.app_settings_type:
374 374 cipher = EncryptedTextValue()
375 375 v = safe_str(cipher.process_result_value(v, None))
376 376
377 377 converter = self.SETTINGS_TYPES.get(_type) or \
378 378 self.SETTINGS_TYPES['unicode']
379 379 return converter(v)
380 380
381 381 @app_settings_value.setter
382 382 def app_settings_value(self, val):
383 383 """
384 384 Setter that will always make sure we use unicode in app_settings_value
385 385
386 386 :param val:
387 387 """
388 388 val = safe_str(val)
389 389 # encode the encrypted value
390 390 if 'encrypted' in self.app_settings_type:
391 391 cipher = EncryptedTextValue()
392 392 val = safe_str(cipher.process_bind_param(val, None))
393 393 self._app_settings_value = val
394 394
395 395 @hybrid_property
396 396 def app_settings_type(self):
397 397 return self._app_settings_type
398 398
399 399 @app_settings_type.setter
400 400 def app_settings_type(self, val):
401 401 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 402 raise Exception('type must be one of %s got %s'
403 403 % (self.SETTINGS_TYPES.keys(), val))
404 404 self._app_settings_type = val
405 405
406 406 @classmethod
407 407 def get_by_prefix(cls, prefix):
408 408 return RhodeCodeSetting.query()\
409 409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 410 .all()
411 411
412 412 def __repr__(self):
413 413 return "<%s('%s:%s[%s]')>" % (
414 414 self.cls_name,
415 415 self.app_settings_name, self.app_settings_value,
416 416 self.app_settings_type
417 417 )
418 418
419 419
420 420 class RhodeCodeUi(Base, BaseModel):
421 421 __tablename__ = 'rhodecode_ui'
422 422 __table_args__ = (
423 423 UniqueConstraint('ui_key'),
424 424 base_table_args
425 425 )
426 426 # Sync those values with vcsserver.config.hooks
427 427
428 428 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 429 # HG
430 430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 431 HOOK_PULL = 'outgoing.pull_logger'
432 432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 434 HOOK_PUSH = 'changegroup.push_logger'
435 435 HOOK_PUSH_KEY = 'pushkey.key_push'
436 436
437 437 HOOKS_BUILTIN = [
438 438 HOOK_PRE_PULL,
439 439 HOOK_PULL,
440 440 HOOK_PRE_PUSH,
441 441 HOOK_PRETX_PUSH,
442 442 HOOK_PUSH,
443 443 HOOK_PUSH_KEY,
444 444 ]
445 445
446 446 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 447 # git part is currently hardcoded.
448 448
449 449 # SVN PATTERNS
450 450 SVN_BRANCH_ID = 'vcs_svn_branch'
451 451 SVN_TAG_ID = 'vcs_svn_tag'
452 452
453 453 ui_id = Column(
454 454 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 455 primary_key=True)
456 456 ui_section = Column(
457 457 "ui_section", String(255), nullable=True, unique=None, default=None)
458 458 ui_key = Column(
459 459 "ui_key", String(255), nullable=True, unique=None, default=None)
460 460 ui_value = Column(
461 461 "ui_value", String(255), nullable=True, unique=None, default=None)
462 462 ui_active = Column(
463 463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464 464
465 465 def __repr__(self):
466 466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 467 self.ui_key, self.ui_value)
468 468
469 469
470 470 class RepoRhodeCodeSetting(Base, BaseModel):
471 471 __tablename__ = 'repo_rhodecode_settings'
472 472 __table_args__ = (
473 473 UniqueConstraint(
474 474 'app_settings_name', 'repository_id',
475 475 name='uq_repo_rhodecode_setting_name_repo_id'),
476 476 base_table_args
477 477 )
478 478
479 479 repository_id = Column(
480 480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 481 nullable=False)
482 482 app_settings_id = Column(
483 483 "app_settings_id", Integer(), nullable=False, unique=True,
484 484 default=None, primary_key=True)
485 485 app_settings_name = Column(
486 486 "app_settings_name", String(255), nullable=True, unique=None,
487 487 default=None)
488 488 _app_settings_value = Column(
489 489 "app_settings_value", String(4096), nullable=True, unique=None,
490 490 default=None)
491 491 _app_settings_type = Column(
492 492 "app_settings_type", String(255), nullable=True, unique=None,
493 493 default=None)
494 494
495 495 repository = relationship('Repository', viewonly=True)
496 496
497 497 def __init__(self, repository_id, key='', val='', type='unicode'):
498 498 self.repository_id = repository_id
499 499 self.app_settings_name = key
500 500 self.app_settings_type = type
501 501 self.app_settings_value = val
502 502
503 503 @validates('_app_settings_value')
504 504 def validate_settings_value(self, key, val):
505 505 assert type(val) == str
506 506 return val
507 507
508 508 @hybrid_property
509 509 def app_settings_value(self):
510 510 v = self._app_settings_value
511 511 type_ = self.app_settings_type
512 512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 514 return converter(v)
515 515
516 516 @app_settings_value.setter
517 517 def app_settings_value(self, val):
518 518 """
519 519 Setter that will always make sure we use unicode in app_settings_value
520 520
521 521 :param val:
522 522 """
523 523 self._app_settings_value = safe_str(val)
524 524
525 525 @hybrid_property
526 526 def app_settings_type(self):
527 527 return self._app_settings_type
528 528
529 529 @app_settings_type.setter
530 530 def app_settings_type(self, val):
531 531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 532 if val not in SETTINGS_TYPES:
533 533 raise Exception('type must be one of %s got %s'
534 534 % (SETTINGS_TYPES.keys(), val))
535 535 self._app_settings_type = val
536 536
537 537 def __repr__(self):
538 538 return "<%s('%s:%s:%s[%s]')>" % (
539 539 self.cls_name, self.repository.repo_name,
540 540 self.app_settings_name, self.app_settings_value,
541 541 self.app_settings_type
542 542 )
543 543
544 544
545 545 class RepoRhodeCodeUi(Base, BaseModel):
546 546 __tablename__ = 'repo_rhodecode_ui'
547 547 __table_args__ = (
548 548 UniqueConstraint(
549 549 'repository_id', 'ui_section', 'ui_key',
550 550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 551 base_table_args
552 552 )
553 553
554 554 repository_id = Column(
555 555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 556 nullable=False)
557 557 ui_id = Column(
558 558 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 559 primary_key=True)
560 560 ui_section = Column(
561 561 "ui_section", String(255), nullable=True, unique=None, default=None)
562 562 ui_key = Column(
563 563 "ui_key", String(255), nullable=True, unique=None, default=None)
564 564 ui_value = Column(
565 565 "ui_value", String(255), nullable=True, unique=None, default=None)
566 566 ui_active = Column(
567 567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568 568
569 569 repository = relationship('Repository', viewonly=True)
570 570
571 571 def __repr__(self):
572 572 return '<%s[%s:%s]%s=>%s]>' % (
573 573 self.cls_name, self.repository.repo_name,
574 574 self.ui_section, self.ui_key, self.ui_value)
575 575
576 576
577 577 class User(Base, BaseModel):
578 578 __tablename__ = 'users'
579 579 __table_args__ = (
580 580 UniqueConstraint('username'), UniqueConstraint('email'),
581 581 Index('u_username_idx', 'username'),
582 582 Index('u_email_idx', 'email'),
583 583 base_table_args
584 584 )
585 585
586 586 DEFAULT_USER = 'default'
587 587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
589 589
590 590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 591 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 592 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601 601
602 602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608 608
609 609 user_log = relationship('UserLog', back_populates='user')
610 610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611 611
612 612 repositories = relationship('Repository', back_populates='user')
613 613 repository_groups = relationship('RepoGroup', back_populates='user')
614 614 user_groups = relationship('UserGroup', back_populates='user')
615 615
616 616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618 618
619 619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622 622
623 623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624 624
625 625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 626 # notifications assigned to this user
627 627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 628 # comments created by this user
629 629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 630 # user profile extra info
631 631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635 635
636 636 # gists
637 637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 638 # user pull requests
639 639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640 640
641 641 # external identities
642 642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 643 # review rules
644 644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645 645
646 646 # artifacts owned
647 647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648 648
649 649 # no cascade, set NULL
650 650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651 651
652 652 def __repr__(self):
653 653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654 654
655 655 @hybrid_property
656 656 def email(self):
657 657 return self._email
658 658
659 659 @email.setter
660 660 def email(self, val):
661 661 self._email = val.lower() if val else None
662 662
663 663 @hybrid_property
664 664 def first_name(self):
665 665 from rhodecode.lib import helpers as h
666 666 if self.name:
667 667 return h.escape(self.name)
668 668 return self.name
669 669
670 670 @hybrid_property
671 671 def last_name(self):
672 672 from rhodecode.lib import helpers as h
673 673 if self.lastname:
674 674 return h.escape(self.lastname)
675 675 return self.lastname
676 676
677 677 @hybrid_property
678 678 def api_key(self):
679 679 """
680 680 Fetch if exist an auth-token with role ALL connected to this user
681 681 """
682 682 user_auth_token = UserApiKeys.query()\
683 683 .filter(UserApiKeys.user_id == self.user_id)\
684 684 .filter(or_(UserApiKeys.expires == -1,
685 685 UserApiKeys.expires >= time.time()))\
686 686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 687 if user_auth_token:
688 688 user_auth_token = user_auth_token.api_key
689 689
690 690 return user_auth_token
691 691
692 692 @api_key.setter
693 693 def api_key(self, val):
694 694 # don't allow to set API key this is deprecated for now
695 695 self._api_key = None
696 696
697 697 @property
698 698 def reviewer_pull_requests(self):
699 699 return PullRequestReviewers.query() \
700 700 .options(joinedload(PullRequestReviewers.pull_request)) \
701 701 .filter(PullRequestReviewers.user_id == self.user_id) \
702 702 .all()
703 703
704 704 @property
705 705 def firstname(self):
706 706 # alias for future
707 707 return self.name
708 708
709 709 @property
710 710 def emails(self):
711 711 other = UserEmailMap.query()\
712 712 .filter(UserEmailMap.user == self) \
713 713 .order_by(UserEmailMap.email_id.asc()) \
714 714 .all()
715 715 return [self.email] + [x.email for x in other]
716 716
717 717 def emails_cached(self):
718 718 emails = []
719 719 if self.user_id != self.get_default_user_id():
720 720 emails = UserEmailMap.query()\
721 721 .filter(UserEmailMap.user == self) \
722 722 .order_by(UserEmailMap.email_id.asc())
723 723
724 724 emails = emails.options(
725 725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 726 )
727 727
728 728 return [self.email] + [x.email for x in emails]
729 729
730 730 @property
731 731 def auth_tokens(self):
732 732 auth_tokens = self.get_auth_tokens()
733 733 return [x.api_key for x in auth_tokens]
734 734
735 735 def get_auth_tokens(self):
736 736 return UserApiKeys.query()\
737 737 .filter(UserApiKeys.user == self)\
738 738 .order_by(UserApiKeys.user_api_key_id.asc())\
739 739 .all()
740 740
741 741 @LazyProperty
742 742 def feed_token(self):
743 743 return self.get_feed_token()
744 744
745 745 def get_feed_token(self, cache=True):
746 746 feed_tokens = UserApiKeys.query()\
747 747 .filter(UserApiKeys.user == self)\
748 748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 749 if cache:
750 750 feed_tokens = feed_tokens.options(
751 751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752 752
753 753 feed_tokens = feed_tokens.all()
754 754 if feed_tokens:
755 755 return feed_tokens[0].api_key
756 756 return 'NO_FEED_TOKEN_AVAILABLE'
757 757
758 758 @LazyProperty
759 759 def artifact_token(self):
760 760 return self.get_artifact_token()
761 761
762 762 def get_artifact_token(self, cache=True):
763 763 artifacts_tokens = UserApiKeys.query()\
764 764 .filter(UserApiKeys.user == self) \
765 765 .filter(or_(UserApiKeys.expires == -1,
766 766 UserApiKeys.expires >= time.time())) \
767 767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768 768
769 769 if cache:
770 770 artifacts_tokens = artifacts_tokens.options(
771 771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772 772
773 773 artifacts_tokens = artifacts_tokens.all()
774 774 if artifacts_tokens:
775 775 return artifacts_tokens[0].api_key
776 776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777 777
778 778 def get_or_create_artifact_token(self):
779 779 artifacts_tokens = UserApiKeys.query()\
780 780 .filter(UserApiKeys.user == self) \
781 781 .filter(or_(UserApiKeys.expires == -1,
782 782 UserApiKeys.expires >= time.time())) \
783 783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784 784
785 785 artifacts_tokens = artifacts_tokens.all()
786 786 if artifacts_tokens:
787 787 return artifacts_tokens[0].api_key
788 788 else:
789 789 from rhodecode.model.auth_token import AuthTokenModel
790 790 artifact_token = AuthTokenModel().create(
791 791 self, 'auto-generated-artifact-token',
792 792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 793 Session.commit()
794 794 return artifact_token.api_key
795 795
796 796 @classmethod
797 797 def get(cls, user_id, cache=False):
798 798 if not user_id:
799 799 return
800 800
801 801 user = cls.query()
802 802 if cache:
803 803 user = user.options(
804 804 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 805 return user.get(user_id)
806 806
807 807 @classmethod
808 808 def extra_valid_auth_tokens(cls, user, role=None):
809 809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 810 .filter(or_(UserApiKeys.expires == -1,
811 811 UserApiKeys.expires >= time.time()))
812 812 if role:
813 813 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 815 return tokens.all()
816 816
817 817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 818 from rhodecode.lib import auth
819 819
820 820 log.debug('Trying to authenticate user: %s via auth-token, '
821 821 'and roles: %s', self, roles)
822 822
823 823 if not auth_token:
824 824 return False
825 825
826 826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 827 tokens_q = UserApiKeys.query()\
828 828 .filter(UserApiKeys.user_id == self.user_id)\
829 829 .filter(or_(UserApiKeys.expires == -1,
830 830 UserApiKeys.expires >= time.time()))
831 831
832 832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833 833
834 834 crypto_backend = auth.crypto_backend()
835 835 enc_token_map = {}
836 836 plain_token_map = {}
837 837 for token in tokens_q:
838 838 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 839 enc_token_map[token.api_key] = token
840 840 else:
841 841 plain_token_map[token.api_key] = token
842 842 log.debug(
843 843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 844 len(plain_token_map), len(enc_token_map))
845 845
846 846 # plain token match comes first
847 847 match = plain_token_map.get(auth_token)
848 848
849 849 # check encrypted tokens now
850 850 if not match:
851 851 for token_hash, token in enc_token_map.items():
852 852 # NOTE(marcink): this is expensive to calculate, but most secure
853 853 if crypto_backend.hash_check(auth_token, token_hash):
854 854 match = token
855 855 break
856 856
857 857 if match:
858 858 log.debug('Found matching token %s', match)
859 859 if match.repo_id:
860 860 log.debug('Found scope, checking for scope match of token %s', match)
861 861 if match.repo_id == scope_repo_id:
862 862 return True
863 863 else:
864 864 log.debug(
865 865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 866 'and calling scope is:%s, skipping further checks',
867 867 match.repo, scope_repo_id)
868 868 return False
869 869 else:
870 870 return True
871 871
872 872 return False
873 873
874 874 @property
875 875 def ip_addresses(self):
876 876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 877 return [x.ip_addr for x in ret]
878 878
879 879 @property
880 880 def username_and_name(self):
881 881 return f'{self.username} ({self.first_name} {self.last_name})'
882 882
883 883 @property
884 884 def username_or_name_or_email(self):
885 885 full_name = self.full_name if self.full_name != ' ' else None
886 886 return self.username or full_name or self.email
887 887
888 888 @property
889 889 def full_name(self):
890 890 return f'{self.first_name} {self.last_name}'
891 891
892 892 @property
893 893 def full_name_or_username(self):
894 894 return (f'{self.first_name} {self.last_name}'
895 895 if (self.first_name and self.last_name) else self.username)
896 896
897 897 @property
898 898 def full_contact(self):
899 899 return f'{self.first_name} {self.last_name} <{self.email}>'
900 900
901 901 @property
902 902 def short_contact(self):
903 903 return f'{self.first_name} {self.last_name}'
904 904
905 905 @property
906 906 def is_admin(self):
907 907 return self.admin
908 908
909 909 @property
910 910 def language(self):
911 911 return self.user_data.get('language')
912 912
913 913 def AuthUser(self, **kwargs):
914 914 """
915 915 Returns instance of AuthUser for this user
916 916 """
917 917 from rhodecode.lib.auth import AuthUser
918 918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919 919
920 920 @hybrid_property
921 921 def user_data(self):
922 922 if not self._user_data:
923 923 return {}
924 924
925 925 try:
926 926 return json.loads(self._user_data) or {}
927 927 except TypeError:
928 928 return {}
929 929
930 930 @user_data.setter
931 931 def user_data(self, val):
932 932 if not isinstance(val, dict):
933 933 raise Exception('user_data must be dict, got %s' % type(val))
934 934 try:
935 935 self._user_data = safe_bytes(json.dumps(val))
936 936 except Exception:
937 937 log.error(traceback.format_exc())
938 938
939 939 @classmethod
940 940 def get_by_username(cls, username, case_insensitive=False,
941 941 cache=False):
942 942
943 943 if case_insensitive:
944 944 q = cls.select().where(
945 945 func.lower(cls.username) == func.lower(username))
946 946 else:
947 947 q = cls.select().where(cls.username == username)
948 948
949 949 if cache:
950 950 hash_key = _hash_key(username)
951 951 q = q.options(
952 952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953 953
954 954 return cls.execute(q).scalar_one_or_none()
955 955
956 956 @classmethod
957 def get_by_username_or_primary_email(cls, user_identifier):
958 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
959 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
960 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
961
962 @classmethod
957 963 def get_by_auth_token(cls, auth_token, cache=False):
958 964
959 965 q = cls.select(User)\
960 966 .join(UserApiKeys)\
961 967 .where(UserApiKeys.api_key == auth_token)\
962 968 .where(or_(UserApiKeys.expires == -1,
963 969 UserApiKeys.expires >= time.time()))
964 970
965 971 if cache:
966 972 q = q.options(
967 973 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
968 974
969 975 matched_user = cls.execute(q).scalar_one_or_none()
970 976
971 977 return matched_user
972 978
973 979 @classmethod
974 980 def get_by_email(cls, email, case_insensitive=False, cache=False):
975 981
976 982 if case_insensitive:
977 983 q = cls.select().where(func.lower(cls.email) == func.lower(email))
978 984 else:
979 985 q = cls.select().where(cls.email == email)
980 986
981 987 if cache:
982 988 email_key = _hash_key(email)
983 989 q = q.options(
984 990 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
985 991
986 992 ret = cls.execute(q).scalar_one_or_none()
987 993
988 994 if ret is None:
989 995 q = cls.select(UserEmailMap)
990 996 # try fetching in alternate email map
991 997 if case_insensitive:
992 998 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
993 999 else:
994 1000 q = q.where(UserEmailMap.email == email)
995 1001 q = q.options(joinedload(UserEmailMap.user))
996 1002 if cache:
997 1003 q = q.options(
998 1004 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
999 1005
1000 1006 result = cls.execute(q).scalar_one_or_none()
1001 1007 ret = getattr(result, 'user', None)
1002 1008
1003 1009 return ret
1004 1010
1005 1011 @classmethod
1006 1012 def get_from_cs_author(cls, author):
1007 1013 """
1008 1014 Tries to get User objects out of commit author string
1009 1015
1010 1016 :param author:
1011 1017 """
1012 1018 from rhodecode.lib.helpers import email, author_name
1013 1019 # Valid email in the attribute passed, see if they're in the system
1014 1020 _email = email(author)
1015 1021 if _email:
1016 1022 user = cls.get_by_email(_email, case_insensitive=True)
1017 1023 if user:
1018 1024 return user
1019 1025 # Maybe we can match by username?
1020 1026 _author = author_name(author)
1021 1027 user = cls.get_by_username(_author, case_insensitive=True)
1022 1028 if user:
1023 1029 return user
1024 1030
1025 1031 def update_userdata(self, **kwargs):
1026 1032 usr = self
1027 1033 old = usr.user_data
1028 1034 old.update(**kwargs)
1029 1035 usr.user_data = old
1030 1036 Session().add(usr)
1031 1037 log.debug('updated userdata with %s', kwargs)
1032 1038
1033 1039 def update_lastlogin(self):
1034 1040 """Update user lastlogin"""
1035 1041 self.last_login = datetime.datetime.now()
1036 1042 Session().add(self)
1037 1043 log.debug('updated user %s lastlogin', self.username)
1038 1044
1039 1045 def update_password(self, new_password):
1040 1046 from rhodecode.lib.auth import get_crypt_password
1041 1047
1042 1048 self.password = get_crypt_password(new_password)
1043 1049 Session().add(self)
1044 1050
1045 1051 @classmethod
1046 1052 def get_first_super_admin(cls):
1047 1053 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1048 1054 user = cls.scalars(stmt).first()
1049 1055
1050 1056 if user is None:
1051 1057 raise Exception('FATAL: Missing administrative account!')
1052 1058 return user
1053 1059
1054 1060 @classmethod
1055 1061 def get_all_super_admins(cls, only_active=False):
1056 1062 """
1057 1063 Returns all admin accounts sorted by username
1058 1064 """
1059 1065 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1060 1066 if only_active:
1061 1067 qry = qry.filter(User.active == true())
1062 1068 return qry.all()
1063 1069
1064 1070 @classmethod
1065 1071 def get_all_user_ids(cls, only_active=True):
1066 1072 """
1067 1073 Returns all users IDs
1068 1074 """
1069 1075 qry = Session().query(User.user_id)
1070 1076
1071 1077 if only_active:
1072 1078 qry = qry.filter(User.active == true())
1073 1079 return [x.user_id for x in qry]
1074 1080
1075 1081 @classmethod
1076 1082 def get_default_user(cls, cache=False, refresh=False):
1077 1083 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1078 1084 if user is None:
1079 1085 raise Exception('FATAL: Missing default account!')
1080 1086 if refresh:
1081 1087 # The default user might be based on outdated state which
1082 1088 # has been loaded from the cache.
1083 1089 # A call to refresh() ensures that the
1084 1090 # latest state from the database is used.
1085 1091 Session().refresh(user)
1086 1092
1087 1093 return user
1088 1094
1089 1095 @classmethod
1090 1096 def get_default_user_id(cls):
1091 1097 import rhodecode
1092 1098 return rhodecode.CONFIG['default_user_id']
1093 1099
1094 1100 def _get_default_perms(self, user, suffix=''):
1095 1101 from rhodecode.model.permission import PermissionModel
1096 1102 return PermissionModel().get_default_perms(user.user_perms, suffix)
1097 1103
1098 1104 def get_default_perms(self, suffix=''):
1099 1105 return self._get_default_perms(self, suffix)
1100 1106
1101 1107 def get_api_data(self, include_secrets=False, details='full'):
1102 1108 """
1103 1109 Common function for generating user related data for API
1104 1110
1105 1111 :param include_secrets: By default secrets in the API data will be replaced
1106 1112 by a placeholder value to prevent exposing this data by accident. In case
1107 1113 this data shall be exposed, set this flag to ``True``.
1108 1114
1109 1115 :param details: details can be 'basic|full' basic gives only a subset of
1110 1116 the available user information that includes user_id, name and emails.
1111 1117 """
1112 1118 user = self
1113 1119 user_data = self.user_data
1114 1120 data = {
1115 1121 'user_id': user.user_id,
1116 1122 'username': user.username,
1117 1123 'firstname': user.name,
1118 1124 'lastname': user.lastname,
1119 1125 'description': user.description,
1120 1126 'email': user.email,
1121 1127 'emails': user.emails,
1122 1128 }
1123 1129 if details == 'basic':
1124 1130 return data
1125 1131
1126 1132 auth_token_length = 40
1127 1133 auth_token_replacement = '*' * auth_token_length
1128 1134
1129 1135 extras = {
1130 1136 'auth_tokens': [auth_token_replacement],
1131 1137 'active': user.active,
1132 1138 'admin': user.admin,
1133 1139 'extern_type': user.extern_type,
1134 1140 'extern_name': user.extern_name,
1135 1141 'last_login': user.last_login,
1136 1142 'last_activity': user.last_activity,
1137 1143 'ip_addresses': user.ip_addresses,
1138 1144 'language': user_data.get('language')
1139 1145 }
1140 1146 data.update(extras)
1141 1147
1142 1148 if include_secrets:
1143 1149 data['auth_tokens'] = user.auth_tokens
1144 1150 return data
1145 1151
1146 1152 def __json__(self):
1147 1153 data = {
1148 1154 'full_name': self.full_name,
1149 1155 'full_name_or_username': self.full_name_or_username,
1150 1156 'short_contact': self.short_contact,
1151 1157 'full_contact': self.full_contact,
1152 1158 }
1153 1159 data.update(self.get_api_data())
1154 1160 return data
1155 1161
1156 1162
1157 1163 class UserApiKeys(Base, BaseModel):
1158 1164 __tablename__ = 'user_api_keys'
1159 1165 __table_args__ = (
1160 1166 Index('uak_api_key_idx', 'api_key'),
1161 1167 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1162 1168 base_table_args
1163 1169 )
1164 1170
1165 1171 # ApiKey role
1166 1172 ROLE_ALL = 'token_role_all'
1167 1173 ROLE_VCS = 'token_role_vcs'
1168 1174 ROLE_API = 'token_role_api'
1169 1175 ROLE_HTTP = 'token_role_http'
1170 1176 ROLE_FEED = 'token_role_feed'
1171 1177 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1172 1178 # The last one is ignored in the list as we only
1173 1179 # use it for one action, and cannot be created by users
1174 1180 ROLE_PASSWORD_RESET = 'token_password_reset'
1175 1181
1176 1182 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1177 1183
1178 1184 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1179 1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1180 1186 api_key = Column("api_key", String(255), nullable=False, unique=True)
1181 1187 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1182 1188 expires = Column('expires', Float(53), nullable=False)
1183 1189 role = Column('role', String(255), nullable=True)
1184 1190 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1185 1191
1186 1192 # scope columns
1187 1193 repo_id = Column(
1188 1194 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1189 1195 nullable=True, unique=None, default=None)
1190 1196 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1191 1197
1192 1198 repo_group_id = Column(
1193 1199 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1194 1200 nullable=True, unique=None, default=None)
1195 1201 repo_group = relationship('RepoGroup', lazy='joined')
1196 1202
1197 1203 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1198 1204
1199 1205 def __repr__(self):
1200 1206 return f"<{self.cls_name}('{self.role}')>"
1201 1207
1202 1208 def __json__(self):
1203 1209 data = {
1204 1210 'auth_token': self.api_key,
1205 1211 'role': self.role,
1206 1212 'scope': self.scope_humanized,
1207 1213 'expired': self.expired
1208 1214 }
1209 1215 return data
1210 1216
1211 1217 def get_api_data(self, include_secrets=False):
1212 1218 data = self.__json__()
1213 1219 if include_secrets:
1214 1220 return data
1215 1221 else:
1216 1222 data['auth_token'] = self.token_obfuscated
1217 1223 return data
1218 1224
1219 1225 @hybrid_property
1220 1226 def description_safe(self):
1221 1227 from rhodecode.lib import helpers as h
1222 1228 return h.escape(self.description)
1223 1229
1224 1230 @property
1225 1231 def expired(self):
1226 1232 if self.expires == -1:
1227 1233 return False
1228 1234 return time.time() > self.expires
1229 1235
1230 1236 @classmethod
1231 1237 def _get_role_name(cls, role):
1232 1238 return {
1233 1239 cls.ROLE_ALL: _('all'),
1234 1240 cls.ROLE_HTTP: _('http/web interface'),
1235 1241 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1236 1242 cls.ROLE_API: _('api calls'),
1237 1243 cls.ROLE_FEED: _('feed access'),
1238 1244 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1239 1245 }.get(role, role)
1240 1246
1241 1247 @classmethod
1242 1248 def _get_role_description(cls, role):
1243 1249 return {
1244 1250 cls.ROLE_ALL: _('Token for all actions.'),
1245 1251 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1246 1252 'login using `api_access_controllers_whitelist` functionality.'),
1247 1253 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1248 1254 'Requires auth_token authentication plugin to be active. <br/>'
1249 1255 'Such Token should be used then instead of a password to '
1250 1256 'interact with a repository, and additionally can be '
1251 1257 'limited to single repository using repo scope.'),
1252 1258 cls.ROLE_API: _('Token limited to api calls.'),
1253 1259 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1254 1260 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1255 1261 }.get(role, role)
1256 1262
1257 1263 @property
1258 1264 def role_humanized(self):
1259 1265 return self._get_role_name(self.role)
1260 1266
1261 1267 def _get_scope(self):
1262 1268 if self.repo:
1263 1269 return 'Repository: {}'.format(self.repo.repo_name)
1264 1270 if self.repo_group:
1265 1271 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1266 1272 return 'Global'
1267 1273
1268 1274 @property
1269 1275 def scope_humanized(self):
1270 1276 return self._get_scope()
1271 1277
1272 1278 @property
1273 1279 def token_obfuscated(self):
1274 1280 if self.api_key:
1275 1281 return self.api_key[:4] + "****"
1276 1282
1277 1283
1278 1284 class UserEmailMap(Base, BaseModel):
1279 1285 __tablename__ = 'user_email_map'
1280 1286 __table_args__ = (
1281 1287 Index('uem_email_idx', 'email'),
1282 1288 Index('uem_user_id_idx', 'user_id'),
1283 1289 UniqueConstraint('email'),
1284 1290 base_table_args
1285 1291 )
1286 1292
1287 1293 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1288 1294 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1289 1295 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1290 1296 user = relationship('User', lazy='joined', back_populates='user_emails')
1291 1297
1292 1298 @validates('_email')
1293 1299 def validate_email(self, key, email):
1294 1300 # check if this email is not main one
1295 1301 main_email = Session().query(User).filter(User.email == email).scalar()
1296 1302 if main_email is not None:
1297 1303 raise AttributeError('email %s is present is user table' % email)
1298 1304 return email
1299 1305
1300 1306 @hybrid_property
1301 1307 def email(self):
1302 1308 return self._email
1303 1309
1304 1310 @email.setter
1305 1311 def email(self, val):
1306 1312 self._email = val.lower() if val else None
1307 1313
1308 1314
1309 1315 class UserIpMap(Base, BaseModel):
1310 1316 __tablename__ = 'user_ip_map'
1311 1317 __table_args__ = (
1312 1318 UniqueConstraint('user_id', 'ip_addr'),
1313 1319 base_table_args
1314 1320 )
1315 1321
1316 1322 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 1323 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1318 1324 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1319 1325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1320 1326 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1321 1327 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1322 1328
1323 1329 @hybrid_property
1324 1330 def description_safe(self):
1325 1331 from rhodecode.lib import helpers as h
1326 1332 return h.escape(self.description)
1327 1333
1328 1334 @classmethod
1329 1335 def _get_ip_range(cls, ip_addr):
1330 1336 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1331 1337 return [str(net.network_address), str(net.broadcast_address)]
1332 1338
1333 1339 def __json__(self):
1334 1340 return {
1335 1341 'ip_addr': self.ip_addr,
1336 1342 'ip_range': self._get_ip_range(self.ip_addr),
1337 1343 }
1338 1344
1339 1345 def __repr__(self):
1340 1346 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1341 1347
1342 1348
1343 1349 class UserSshKeys(Base, BaseModel):
1344 1350 __tablename__ = 'user_ssh_keys'
1345 1351 __table_args__ = (
1346 1352 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1347 1353
1348 1354 UniqueConstraint('ssh_key_fingerprint'),
1349 1355
1350 1356 base_table_args
1351 1357 )
1352 1358
1353 1359 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1354 1360 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1355 1361 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1356 1362
1357 1363 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1358 1364
1359 1365 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1360 1366 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1361 1367 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1362 1368
1363 1369 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1364 1370
1365 1371 def __json__(self):
1366 1372 data = {
1367 1373 'ssh_fingerprint': self.ssh_key_fingerprint,
1368 1374 'description': self.description,
1369 1375 'created_on': self.created_on
1370 1376 }
1371 1377 return data
1372 1378
1373 1379 def get_api_data(self):
1374 1380 data = self.__json__()
1375 1381 return data
1376 1382
1377 1383
1378 1384 class UserLog(Base, BaseModel):
1379 1385 __tablename__ = 'user_logs'
1380 1386 __table_args__ = (
1381 1387 base_table_args,
1382 1388 )
1383 1389
1384 1390 VERSION_1 = 'v1'
1385 1391 VERSION_2 = 'v2'
1386 1392 VERSIONS = [VERSION_1, VERSION_2]
1387 1393
1388 1394 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1389 1395 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1390 1396 username = Column("username", String(255), nullable=True, unique=None, default=None)
1391 1397 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1392 1398 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1393 1399 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1394 1400 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1395 1401 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1396 1402
1397 1403 version = Column("version", String(255), nullable=True, default=VERSION_1)
1398 1404 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1399 1405 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1400 1406 user = relationship('User', cascade='', back_populates='user_log')
1401 1407 repository = relationship('Repository', cascade='', back_populates='logs')
1402 1408
1403 1409 def __repr__(self):
1404 1410 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1405 1411
1406 1412 def __json__(self):
1407 1413 return {
1408 1414 'user_id': self.user_id,
1409 1415 'username': self.username,
1410 1416 'repository_id': self.repository_id,
1411 1417 'repository_name': self.repository_name,
1412 1418 'user_ip': self.user_ip,
1413 1419 'action_date': self.action_date,
1414 1420 'action': self.action,
1415 1421 }
1416 1422
1417 1423 @hybrid_property
1418 1424 def entry_id(self):
1419 1425 return self.user_log_id
1420 1426
1421 1427 @property
1422 1428 def action_as_day(self):
1423 1429 return datetime.date(*self.action_date.timetuple()[:3])
1424 1430
1425 1431
1426 1432 class UserGroup(Base, BaseModel):
1427 1433 __tablename__ = 'users_groups'
1428 1434 __table_args__ = (
1429 1435 base_table_args,
1430 1436 )
1431 1437
1432 1438 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1433 1439 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1434 1440 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1435 1441 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1436 1442 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1437 1443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1438 1444 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1439 1445 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1440 1446
1441 1447 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1442 1448 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1443 1449 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1444 1450 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1445 1451 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1446 1452
1447 1453 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1448 1454
1449 1455 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1450 1456 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1451 1457
1452 1458 @classmethod
1453 1459 def _load_group_data(cls, column):
1454 1460 if not column:
1455 1461 return {}
1456 1462
1457 1463 try:
1458 1464 return json.loads(column) or {}
1459 1465 except TypeError:
1460 1466 return {}
1461 1467
1462 1468 @hybrid_property
1463 1469 def description_safe(self):
1464 1470 from rhodecode.lib import helpers as h
1465 1471 return h.escape(self.user_group_description)
1466 1472
1467 1473 @hybrid_property
1468 1474 def group_data(self):
1469 1475 return self._load_group_data(self._group_data)
1470 1476
1471 1477 @group_data.expression
1472 1478 def group_data(self, **kwargs):
1473 1479 return self._group_data
1474 1480
1475 1481 @group_data.setter
1476 1482 def group_data(self, val):
1477 1483 try:
1478 1484 self._group_data = json.dumps(val)
1479 1485 except Exception:
1480 1486 log.error(traceback.format_exc())
1481 1487
1482 1488 @classmethod
1483 1489 def _load_sync(cls, group_data):
1484 1490 if group_data:
1485 1491 return group_data.get('extern_type')
1486 1492
1487 1493 @property
1488 1494 def sync(self):
1489 1495 return self._load_sync(self.group_data)
1490 1496
1491 1497 def __repr__(self):
1492 1498 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1493 1499
1494 1500 @classmethod
1495 1501 def get_by_group_name(cls, group_name, cache=False,
1496 1502 case_insensitive=False):
1497 1503 if case_insensitive:
1498 1504 q = cls.query().filter(func.lower(cls.users_group_name) ==
1499 1505 func.lower(group_name))
1500 1506
1501 1507 else:
1502 1508 q = cls.query().filter(cls.users_group_name == group_name)
1503 1509 if cache:
1504 1510 name_key = _hash_key(group_name)
1505 1511 q = q.options(
1506 1512 FromCache("sql_cache_short", f"get_group_{name_key}"))
1507 1513 return q.scalar()
1508 1514
1509 1515 @classmethod
1510 1516 def get(cls, user_group_id, cache=False):
1511 1517 if not user_group_id:
1512 1518 return
1513 1519
1514 1520 user_group = cls.query()
1515 1521 if cache:
1516 1522 user_group = user_group.options(
1517 1523 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1518 1524 return user_group.get(user_group_id)
1519 1525
1520 1526 def permissions(self, with_admins=True, with_owner=True,
1521 1527 expand_from_user_groups=False):
1522 1528 """
1523 1529 Permissions for user groups
1524 1530 """
1525 1531 _admin_perm = 'usergroup.admin'
1526 1532
1527 1533 owner_row = []
1528 1534 if with_owner:
1529 1535 usr = AttributeDict(self.user.get_dict())
1530 1536 usr.owner_row = True
1531 1537 usr.permission = _admin_perm
1532 1538 owner_row.append(usr)
1533 1539
1534 1540 super_admin_ids = []
1535 1541 super_admin_rows = []
1536 1542 if with_admins:
1537 1543 for usr in User.get_all_super_admins():
1538 1544 super_admin_ids.append(usr.user_id)
1539 1545 # if this admin is also owner, don't double the record
1540 1546 if usr.user_id == owner_row[0].user_id:
1541 1547 owner_row[0].admin_row = True
1542 1548 else:
1543 1549 usr = AttributeDict(usr.get_dict())
1544 1550 usr.admin_row = True
1545 1551 usr.permission = _admin_perm
1546 1552 super_admin_rows.append(usr)
1547 1553
1548 1554 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1549 1555 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1550 1556 joinedload(UserUserGroupToPerm.user),
1551 1557 joinedload(UserUserGroupToPerm.permission),)
1552 1558
1553 1559 # get owners and admins and permissions. We do a trick of re-writing
1554 1560 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1555 1561 # has a global reference and changing one object propagates to all
1556 1562 # others. This means if admin is also an owner admin_row that change
1557 1563 # would propagate to both objects
1558 1564 perm_rows = []
1559 1565 for _usr in q.all():
1560 1566 usr = AttributeDict(_usr.user.get_dict())
1561 1567 # if this user is also owner/admin, mark as duplicate record
1562 1568 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1563 1569 usr.duplicate_perm = True
1564 1570 usr.permission = _usr.permission.permission_name
1565 1571 perm_rows.append(usr)
1566 1572
1567 1573 # filter the perm rows by 'default' first and then sort them by
1568 1574 # admin,write,read,none permissions sorted again alphabetically in
1569 1575 # each group
1570 1576 perm_rows = sorted(perm_rows, key=display_user_sort)
1571 1577
1572 1578 user_groups_rows = []
1573 1579 if expand_from_user_groups:
1574 1580 for ug in self.permission_user_groups(with_members=True):
1575 1581 for user_data in ug.members:
1576 1582 user_groups_rows.append(user_data)
1577 1583
1578 1584 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1579 1585
1580 1586 def permission_user_groups(self, with_members=False):
1581 1587 q = UserGroupUserGroupToPerm.query()\
1582 1588 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1583 1589 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1584 1590 joinedload(UserGroupUserGroupToPerm.target_user_group),
1585 1591 joinedload(UserGroupUserGroupToPerm.permission),)
1586 1592
1587 1593 perm_rows = []
1588 1594 for _user_group in q.all():
1589 1595 entry = AttributeDict(_user_group.user_group.get_dict())
1590 1596 entry.permission = _user_group.permission.permission_name
1591 1597 if with_members:
1592 1598 entry.members = [x.user.get_dict()
1593 1599 for x in _user_group.user_group.members]
1594 1600 perm_rows.append(entry)
1595 1601
1596 1602 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1597 1603 return perm_rows
1598 1604
1599 1605 def _get_default_perms(self, user_group, suffix=''):
1600 1606 from rhodecode.model.permission import PermissionModel
1601 1607 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1602 1608
1603 1609 def get_default_perms(self, suffix=''):
1604 1610 return self._get_default_perms(self, suffix)
1605 1611
1606 1612 def get_api_data(self, with_group_members=True, include_secrets=False):
1607 1613 """
1608 1614 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1609 1615 basically forwarded.
1610 1616
1611 1617 """
1612 1618 user_group = self
1613 1619 data = {
1614 1620 'users_group_id': user_group.users_group_id,
1615 1621 'group_name': user_group.users_group_name,
1616 1622 'group_description': user_group.user_group_description,
1617 1623 'active': user_group.users_group_active,
1618 1624 'owner': user_group.user.username,
1619 1625 'sync': user_group.sync,
1620 1626 'owner_email': user_group.user.email,
1621 1627 }
1622 1628
1623 1629 if with_group_members:
1624 1630 users = []
1625 1631 for user in user_group.members:
1626 1632 user = user.user
1627 1633 users.append(user.get_api_data(include_secrets=include_secrets))
1628 1634 data['users'] = users
1629 1635
1630 1636 return data
1631 1637
1632 1638
1633 1639 class UserGroupMember(Base, BaseModel):
1634 1640 __tablename__ = 'users_groups_members'
1635 1641 __table_args__ = (
1636 1642 base_table_args,
1637 1643 )
1638 1644
1639 1645 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1640 1646 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1641 1647 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1642 1648
1643 1649 user = relationship('User', lazy='joined', back_populates='group_member')
1644 1650 users_group = relationship('UserGroup', back_populates='members')
1645 1651
1646 1652 def __init__(self, gr_id='', u_id=''):
1647 1653 self.users_group_id = gr_id
1648 1654 self.user_id = u_id
1649 1655
1650 1656
1651 1657 class RepositoryField(Base, BaseModel):
1652 1658 __tablename__ = 'repositories_fields'
1653 1659 __table_args__ = (
1654 1660 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1655 1661 base_table_args,
1656 1662 )
1657 1663
1658 1664 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1659 1665
1660 1666 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1661 1667 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1662 1668 field_key = Column("field_key", String(250))
1663 1669 field_label = Column("field_label", String(1024), nullable=False)
1664 1670 field_value = Column("field_value", String(10000), nullable=False)
1665 1671 field_desc = Column("field_desc", String(1024), nullable=False)
1666 1672 field_type = Column("field_type", String(255), nullable=False, unique=None)
1667 1673 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1668 1674
1669 1675 repository = relationship('Repository', back_populates='extra_fields')
1670 1676
1671 1677 @property
1672 1678 def field_key_prefixed(self):
1673 1679 return 'ex_%s' % self.field_key
1674 1680
1675 1681 @classmethod
1676 1682 def un_prefix_key(cls, key):
1677 1683 if key.startswith(cls.PREFIX):
1678 1684 return key[len(cls.PREFIX):]
1679 1685 return key
1680 1686
1681 1687 @classmethod
1682 1688 def get_by_key_name(cls, key, repo):
1683 1689 row = cls.query()\
1684 1690 .filter(cls.repository == repo)\
1685 1691 .filter(cls.field_key == key).scalar()
1686 1692 return row
1687 1693
1688 1694
1689 1695 class Repository(Base, BaseModel):
1690 1696 __tablename__ = 'repositories'
1691 1697 __table_args__ = (
1692 1698 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1693 1699 base_table_args,
1694 1700 )
1695 1701 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1696 1702 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1697 1703 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1698 1704
1699 1705 STATE_CREATED = 'repo_state_created'
1700 1706 STATE_PENDING = 'repo_state_pending'
1701 1707 STATE_ERROR = 'repo_state_error'
1702 1708
1703 1709 LOCK_AUTOMATIC = 'lock_auto'
1704 1710 LOCK_API = 'lock_api'
1705 1711 LOCK_WEB = 'lock_web'
1706 1712 LOCK_PULL = 'lock_pull'
1707 1713
1708 1714 NAME_SEP = URL_SEP
1709 1715
1710 1716 repo_id = Column(
1711 1717 "repo_id", Integer(), nullable=False, unique=True, default=None,
1712 1718 primary_key=True)
1713 1719 _repo_name = Column(
1714 1720 "repo_name", Text(), nullable=False, default=None)
1715 1721 repo_name_hash = Column(
1716 1722 "repo_name_hash", String(255), nullable=False, unique=True)
1717 1723 repo_state = Column("repo_state", String(255), nullable=True)
1718 1724
1719 1725 clone_uri = Column(
1720 1726 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1721 1727 default=None)
1722 1728 push_uri = Column(
1723 1729 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1724 1730 default=None)
1725 1731 repo_type = Column(
1726 1732 "repo_type", String(255), nullable=False, unique=False, default=None)
1727 1733 user_id = Column(
1728 1734 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1729 1735 unique=False, default=None)
1730 1736 private = Column(
1731 1737 "private", Boolean(), nullable=True, unique=None, default=None)
1732 1738 archived = Column(
1733 1739 "archived", Boolean(), nullable=True, unique=None, default=None)
1734 1740 enable_statistics = Column(
1735 1741 "statistics", Boolean(), nullable=True, unique=None, default=True)
1736 1742 enable_downloads = Column(
1737 1743 "downloads", Boolean(), nullable=True, unique=None, default=True)
1738 1744 description = Column(
1739 1745 "description", String(10000), nullable=True, unique=None, default=None)
1740 1746 created_on = Column(
1741 1747 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1742 1748 default=datetime.datetime.now)
1743 1749 updated_on = Column(
1744 1750 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1745 1751 default=datetime.datetime.now)
1746 1752 _landing_revision = Column(
1747 1753 "landing_revision", String(255), nullable=False, unique=False,
1748 1754 default=None)
1749 1755 enable_locking = Column(
1750 1756 "enable_locking", Boolean(), nullable=False, unique=None,
1751 1757 default=False)
1752 1758 _locked = Column(
1753 1759 "locked", String(255), nullable=True, unique=False, default=None)
1754 1760 _changeset_cache = Column(
1755 1761 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1756 1762
1757 1763 fork_id = Column(
1758 1764 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1759 1765 nullable=True, unique=False, default=None)
1760 1766 group_id = Column(
1761 1767 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1762 1768 unique=False, default=None)
1763 1769
1764 1770 user = relationship('User', lazy='joined', back_populates='repositories')
1765 1771 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1766 1772 group = relationship('RepoGroup', lazy='joined')
1767 1773 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1768 1774 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1769 1775 stats = relationship('Statistics', cascade='all', uselist=False)
1770 1776
1771 1777 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1772 1778 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1773 1779
1774 1780 logs = relationship('UserLog', back_populates='repository')
1775 1781
1776 1782 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1777 1783
1778 1784 pull_requests_source = relationship(
1779 1785 'PullRequest',
1780 1786 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 1787 cascade="all, delete-orphan",
1782 1788 overlaps="source_repo"
1783 1789 )
1784 1790 pull_requests_target = relationship(
1785 1791 'PullRequest',
1786 1792 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1787 1793 cascade="all, delete-orphan",
1788 1794 overlaps="target_repo"
1789 1795 )
1790 1796
1791 1797 ui = relationship('RepoRhodeCodeUi', cascade="all")
1792 1798 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1793 1799 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1794 1800
1795 1801 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1796 1802
1797 1803 # no cascade, set NULL
1798 1804 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1799 1805
1800 1806 review_rules = relationship('RepoReviewRule')
1801 1807 user_branch_perms = relationship('UserToRepoBranchPermission')
1802 1808 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1803 1809
1804 1810 def __repr__(self):
1805 1811 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1806 1812
1807 1813 @hybrid_property
1808 1814 def description_safe(self):
1809 1815 from rhodecode.lib import helpers as h
1810 1816 return h.escape(self.description)
1811 1817
1812 1818 @hybrid_property
1813 1819 def landing_rev(self):
1814 1820 # always should return [rev_type, rev], e.g ['branch', 'master']
1815 1821 if self._landing_revision:
1816 1822 _rev_info = self._landing_revision.split(':')
1817 1823 if len(_rev_info) < 2:
1818 1824 _rev_info.insert(0, 'rev')
1819 1825 return [_rev_info[0], _rev_info[1]]
1820 1826 return [None, None]
1821 1827
1822 1828 @property
1823 1829 def landing_ref_type(self):
1824 1830 return self.landing_rev[0]
1825 1831
1826 1832 @property
1827 1833 def landing_ref_name(self):
1828 1834 return self.landing_rev[1]
1829 1835
1830 1836 @landing_rev.setter
1831 1837 def landing_rev(self, val):
1832 1838 if ':' not in val:
1833 1839 raise ValueError('value must be delimited with `:` and consist '
1834 1840 'of <rev_type>:<rev>, got %s instead' % val)
1835 1841 self._landing_revision = val
1836 1842
1837 1843 @hybrid_property
1838 1844 def locked(self):
1839 1845 if self._locked:
1840 1846 user_id, timelocked, reason = self._locked.split(':')
1841 1847 lock_values = int(user_id), timelocked, reason
1842 1848 else:
1843 1849 lock_values = [None, None, None]
1844 1850 return lock_values
1845 1851
1846 1852 @locked.setter
1847 1853 def locked(self, val):
1848 1854 if val and isinstance(val, (list, tuple)):
1849 1855 self._locked = ':'.join(map(str, val))
1850 1856 else:
1851 1857 self._locked = None
1852 1858
1853 1859 @classmethod
1854 1860 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1855 1861 from rhodecode.lib.vcs.backends.base import EmptyCommit
1856 1862 dummy = EmptyCommit().__json__()
1857 1863 if not changeset_cache_raw:
1858 1864 dummy['source_repo_id'] = repo_id
1859 1865 return json.loads(json.dumps(dummy))
1860 1866
1861 1867 try:
1862 1868 return json.loads(changeset_cache_raw)
1863 1869 except TypeError:
1864 1870 return dummy
1865 1871 except Exception:
1866 1872 log.error(traceback.format_exc())
1867 1873 return dummy
1868 1874
1869 1875 @hybrid_property
1870 1876 def changeset_cache(self):
1871 1877 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1872 1878
1873 1879 @changeset_cache.setter
1874 1880 def changeset_cache(self, val):
1875 1881 try:
1876 1882 self._changeset_cache = json.dumps(val)
1877 1883 except Exception:
1878 1884 log.error(traceback.format_exc())
1879 1885
1880 1886 @hybrid_property
1881 1887 def repo_name(self):
1882 1888 return self._repo_name
1883 1889
1884 1890 @repo_name.setter
1885 1891 def repo_name(self, value):
1886 1892 self._repo_name = value
1887 1893 self.repo_name_hash = sha1(safe_bytes(value))
1888 1894
1889 1895 @classmethod
1890 1896 def normalize_repo_name(cls, repo_name):
1891 1897 """
1892 1898 Normalizes os specific repo_name to the format internally stored inside
1893 1899 database using URL_SEP
1894 1900
1895 1901 :param cls:
1896 1902 :param repo_name:
1897 1903 """
1898 1904 return cls.NAME_SEP.join(repo_name.split(os.sep))
1899 1905
1900 1906 @classmethod
1901 1907 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1902 1908 session = Session()
1903 1909 q = session.query(cls).filter(cls.repo_name == repo_name)
1904 1910
1905 1911 if cache:
1906 1912 if identity_cache:
1907 1913 val = cls.identity_cache(session, 'repo_name', repo_name)
1908 1914 if val:
1909 1915 return val
1910 1916 else:
1911 1917 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1912 1918 q = q.options(
1913 1919 FromCache("sql_cache_short", cache_key))
1914 1920
1915 1921 return q.scalar()
1916 1922
1917 1923 @classmethod
1918 1924 def get_by_id_or_repo_name(cls, repoid):
1919 1925 if isinstance(repoid, int):
1920 1926 try:
1921 1927 repo = cls.get(repoid)
1922 1928 except ValueError:
1923 1929 repo = None
1924 1930 else:
1925 1931 repo = cls.get_by_repo_name(repoid)
1926 1932 return repo
1927 1933
1928 1934 @classmethod
1929 1935 def get_by_full_path(cls, repo_full_path):
1930 1936 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1931 1937 repo_name = cls.normalize_repo_name(repo_name)
1932 1938 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1933 1939
1934 1940 @classmethod
1935 1941 def get_repo_forks(cls, repo_id):
1936 1942 return cls.query().filter(Repository.fork_id == repo_id)
1937 1943
1938 1944 @classmethod
1939 1945 def base_path(cls):
1940 1946 """
1941 1947 Returns base path when all repos are stored
1942 1948
1943 1949 :param cls:
1944 1950 """
1945 1951 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1946 1952 return get_rhodecode_repo_store_path()
1947 1953
1948 1954 @classmethod
1949 1955 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1950 1956 case_insensitive=True, archived=False):
1951 1957 q = Repository.query()
1952 1958
1953 1959 if not archived:
1954 1960 q = q.filter(Repository.archived.isnot(true()))
1955 1961
1956 1962 if not isinstance(user_id, Optional):
1957 1963 q = q.filter(Repository.user_id == user_id)
1958 1964
1959 1965 if not isinstance(group_id, Optional):
1960 1966 q = q.filter(Repository.group_id == group_id)
1961 1967
1962 1968 if case_insensitive:
1963 1969 q = q.order_by(func.lower(Repository.repo_name))
1964 1970 else:
1965 1971 q = q.order_by(Repository.repo_name)
1966 1972
1967 1973 return q.all()
1968 1974
1969 1975 @property
1970 1976 def repo_uid(self):
1971 1977 return '_{}'.format(self.repo_id)
1972 1978
1973 1979 @property
1974 1980 def forks(self):
1975 1981 """
1976 1982 Return forks of this repo
1977 1983 """
1978 1984 return Repository.get_repo_forks(self.repo_id)
1979 1985
1980 1986 @property
1981 1987 def parent(self):
1982 1988 """
1983 1989 Returns fork parent
1984 1990 """
1985 1991 return self.fork
1986 1992
1987 1993 @property
1988 1994 def just_name(self):
1989 1995 return self.repo_name.split(self.NAME_SEP)[-1]
1990 1996
1991 1997 @property
1992 1998 def groups_with_parents(self):
1993 1999 groups = []
1994 2000 if self.group is None:
1995 2001 return groups
1996 2002
1997 2003 cur_gr = self.group
1998 2004 groups.insert(0, cur_gr)
1999 2005 while 1:
2000 2006 gr = getattr(cur_gr, 'parent_group', None)
2001 2007 cur_gr = cur_gr.parent_group
2002 2008 if gr is None:
2003 2009 break
2004 2010 groups.insert(0, gr)
2005 2011
2006 2012 return groups
2007 2013
2008 2014 @property
2009 2015 def groups_and_repo(self):
2010 2016 return self.groups_with_parents, self
2011 2017
2012 2018 @property
2013 2019 def repo_path(self):
2014 2020 """
2015 2021 Returns base full path for that repository means where it actually
2016 2022 exists on a filesystem
2017 2023 """
2018 2024 return self.base_path()
2019 2025
2020 2026 @property
2021 2027 def repo_full_path(self):
2022 2028 p = [self.repo_path]
2023 2029 # we need to split the name by / since this is how we store the
2024 2030 # names in the database, but that eventually needs to be converted
2025 2031 # into a valid system path
2026 2032 p += self.repo_name.split(self.NAME_SEP)
2027 2033 return os.path.join(*map(safe_str, p))
2028 2034
2029 2035 @property
2030 2036 def cache_keys(self):
2031 2037 """
2032 2038 Returns associated cache keys for that repo
2033 2039 """
2034 2040 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2035 2041 return CacheKey.query()\
2036 2042 .filter(CacheKey.cache_key == repo_namespace_key)\
2037 2043 .order_by(CacheKey.cache_key)\
2038 2044 .all()
2039 2045
2040 2046 @property
2041 2047 def cached_diffs_relative_dir(self):
2042 2048 """
2043 2049 Return a relative to the repository store path of cached diffs
2044 2050 used for safe display for users, who shouldn't know the absolute store
2045 2051 path
2046 2052 """
2047 2053 return os.path.join(
2048 2054 os.path.dirname(self.repo_name),
2049 2055 self.cached_diffs_dir.split(os.path.sep)[-1])
2050 2056
2051 2057 @property
2052 2058 def cached_diffs_dir(self):
2053 2059 path = self.repo_full_path
2054 2060 return os.path.join(
2055 2061 os.path.dirname(path),
2056 2062 f'.__shadow_diff_cache_repo_{self.repo_id}')
2057 2063
2058 2064 def cached_diffs(self):
2059 2065 diff_cache_dir = self.cached_diffs_dir
2060 2066 if os.path.isdir(diff_cache_dir):
2061 2067 return os.listdir(diff_cache_dir)
2062 2068 return []
2063 2069
2064 2070 def shadow_repos(self):
2065 2071 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2066 2072 return [
2067 2073 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2068 2074 if x.startswith(shadow_repos_pattern)
2069 2075 ]
2070 2076
2071 2077 def get_new_name(self, repo_name):
2072 2078 """
2073 2079 returns new full repository name based on assigned group and new new
2074 2080
2075 2081 :param repo_name:
2076 2082 """
2077 2083 path_prefix = self.group.full_path_splitted if self.group else []
2078 2084 return self.NAME_SEP.join(path_prefix + [repo_name])
2079 2085
2080 2086 @property
2081 2087 def _config(self):
2082 2088 """
2083 2089 Returns db based config object.
2084 2090 """
2085 2091 from rhodecode.lib.utils import make_db_config
2086 2092 return make_db_config(clear_session=False, repo=self)
2087 2093
2088 2094 def permissions(self, with_admins=True, with_owner=True,
2089 2095 expand_from_user_groups=False):
2090 2096 """
2091 2097 Permissions for repositories
2092 2098 """
2093 2099 _admin_perm = 'repository.admin'
2094 2100
2095 2101 owner_row = []
2096 2102 if with_owner:
2097 2103 usr = AttributeDict(self.user.get_dict())
2098 2104 usr.owner_row = True
2099 2105 usr.permission = _admin_perm
2100 2106 usr.permission_id = None
2101 2107 owner_row.append(usr)
2102 2108
2103 2109 super_admin_ids = []
2104 2110 super_admin_rows = []
2105 2111 if with_admins:
2106 2112 for usr in User.get_all_super_admins():
2107 2113 super_admin_ids.append(usr.user_id)
2108 2114 # if this admin is also owner, don't double the record
2109 2115 if usr.user_id == owner_row[0].user_id:
2110 2116 owner_row[0].admin_row = True
2111 2117 else:
2112 2118 usr = AttributeDict(usr.get_dict())
2113 2119 usr.admin_row = True
2114 2120 usr.permission = _admin_perm
2115 2121 usr.permission_id = None
2116 2122 super_admin_rows.append(usr)
2117 2123
2118 2124 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2119 2125 q = q.options(joinedload(UserRepoToPerm.repository),
2120 2126 joinedload(UserRepoToPerm.user),
2121 2127 joinedload(UserRepoToPerm.permission),)
2122 2128
2123 2129 # get owners and admins and permissions. We do a trick of re-writing
2124 2130 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2125 2131 # has a global reference and changing one object propagates to all
2126 2132 # others. This means if admin is also an owner admin_row that change
2127 2133 # would propagate to both objects
2128 2134 perm_rows = []
2129 2135 for _usr in q.all():
2130 2136 usr = AttributeDict(_usr.user.get_dict())
2131 2137 # if this user is also owner/admin, mark as duplicate record
2132 2138 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2133 2139 usr.duplicate_perm = True
2134 2140 # also check if this permission is maybe used by branch_permissions
2135 2141 if _usr.branch_perm_entry:
2136 2142 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2137 2143
2138 2144 usr.permission = _usr.permission.permission_name
2139 2145 usr.permission_id = _usr.repo_to_perm_id
2140 2146 perm_rows.append(usr)
2141 2147
2142 2148 # filter the perm rows by 'default' first and then sort them by
2143 2149 # admin,write,read,none permissions sorted again alphabetically in
2144 2150 # each group
2145 2151 perm_rows = sorted(perm_rows, key=display_user_sort)
2146 2152
2147 2153 user_groups_rows = []
2148 2154 if expand_from_user_groups:
2149 2155 for ug in self.permission_user_groups(with_members=True):
2150 2156 for user_data in ug.members:
2151 2157 user_groups_rows.append(user_data)
2152 2158
2153 2159 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2154 2160
2155 2161 def permission_user_groups(self, with_members=True):
2156 2162 q = UserGroupRepoToPerm.query()\
2157 2163 .filter(UserGroupRepoToPerm.repository == self)
2158 2164 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2159 2165 joinedload(UserGroupRepoToPerm.users_group),
2160 2166 joinedload(UserGroupRepoToPerm.permission),)
2161 2167
2162 2168 perm_rows = []
2163 2169 for _user_group in q.all():
2164 2170 entry = AttributeDict(_user_group.users_group.get_dict())
2165 2171 entry.permission = _user_group.permission.permission_name
2166 2172 if with_members:
2167 2173 entry.members = [x.user.get_dict()
2168 2174 for x in _user_group.users_group.members]
2169 2175 perm_rows.append(entry)
2170 2176
2171 2177 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2172 2178 return perm_rows
2173 2179
2174 2180 def get_api_data(self, include_secrets=False):
2175 2181 """
2176 2182 Common function for generating repo api data
2177 2183
2178 2184 :param include_secrets: See :meth:`User.get_api_data`.
2179 2185
2180 2186 """
2181 2187 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2182 2188 # move this methods on models level.
2183 2189 from rhodecode.model.settings import SettingsModel
2184 2190 from rhodecode.model.repo import RepoModel
2185 2191
2186 2192 repo = self
2187 2193 _user_id, _time, _reason = self.locked
2188 2194
2189 2195 data = {
2190 2196 'repo_id': repo.repo_id,
2191 2197 'repo_name': repo.repo_name,
2192 2198 'repo_type': repo.repo_type,
2193 2199 'clone_uri': repo.clone_uri or '',
2194 2200 'push_uri': repo.push_uri or '',
2195 2201 'url': RepoModel().get_url(self),
2196 2202 'private': repo.private,
2197 2203 'created_on': repo.created_on,
2198 2204 'description': repo.description_safe,
2199 2205 'landing_rev': repo.landing_rev,
2200 2206 'owner': repo.user.username,
2201 2207 'fork_of': repo.fork.repo_name if repo.fork else None,
2202 2208 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2203 2209 'enable_statistics': repo.enable_statistics,
2204 2210 'enable_locking': repo.enable_locking,
2205 2211 'enable_downloads': repo.enable_downloads,
2206 2212 'last_changeset': repo.changeset_cache,
2207 2213 'locked_by': User.get(_user_id).get_api_data(
2208 2214 include_secrets=include_secrets) if _user_id else None,
2209 2215 'locked_date': time_to_datetime(_time) if _time else None,
2210 2216 'lock_reason': _reason if _reason else None,
2211 2217 }
2212 2218
2213 2219 # TODO: mikhail: should be per-repo settings here
2214 2220 rc_config = SettingsModel().get_all_settings()
2215 2221 repository_fields = str2bool(
2216 2222 rc_config.get('rhodecode_repository_fields'))
2217 2223 if repository_fields:
2218 2224 for f in self.extra_fields:
2219 2225 data[f.field_key_prefixed] = f.field_value
2220 2226
2221 2227 return data
2222 2228
2223 2229 @classmethod
2224 2230 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2225 2231 if not lock_time:
2226 2232 lock_time = time.time()
2227 2233 if not lock_reason:
2228 2234 lock_reason = cls.LOCK_AUTOMATIC
2229 2235 repo.locked = [user_id, lock_time, lock_reason]
2230 2236 Session().add(repo)
2231 2237 Session().commit()
2232 2238
2233 2239 @classmethod
2234 2240 def unlock(cls, repo):
2235 2241 repo.locked = None
2236 2242 Session().add(repo)
2237 2243 Session().commit()
2238 2244
2239 2245 @classmethod
2240 2246 def getlock(cls, repo):
2241 2247 return repo.locked
2242 2248
2243 2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2244 2250 """
2245 2251 Checks locking on this repository, if locking is enabled and lock is
2246 2252 present returns a tuple of make_lock, locked, locked_by.
2247 2253 make_lock can have 3 states None (do nothing) True, make lock
2248 2254 False release lock, This value is later propagated to hooks, which
2249 2255 do the locking. Think about this as signals passed to hooks what to do.
2250 2256
2251 2257 """
2252 2258 # TODO: johbo: This is part of the business logic and should be moved
2253 2259 # into the RepositoryModel.
2254 2260
2255 2261 if action not in ('push', 'pull'):
2256 2262 raise ValueError("Invalid action value: %s" % repr(action))
2257 2263
2258 2264 # defines if locked error should be thrown to user
2259 2265 currently_locked = False
2260 2266 # defines if new lock should be made, tri-state
2261 2267 make_lock = None
2262 2268 repo = self
2263 2269 user = User.get(user_id)
2264 2270
2265 2271 lock_info = repo.locked
2266 2272
2267 2273 if repo and (repo.enable_locking or not only_when_enabled):
2268 2274 if action == 'push':
2269 2275 # check if it's already locked !, if it is compare users
2270 2276 locked_by_user_id = lock_info[0]
2271 2277 if user.user_id == locked_by_user_id:
2272 2278 log.debug(
2273 2279 'Got `push` action from user %s, now unlocking', user)
2274 2280 # unlock if we have push from user who locked
2275 2281 make_lock = False
2276 2282 else:
2277 2283 # we're not the same user who locked, ban with
2278 2284 # code defined in settings (default is 423 HTTP Locked) !
2279 2285 log.debug('Repo %s is currently locked by %s', repo, user)
2280 2286 currently_locked = True
2281 2287 elif action == 'pull':
2282 2288 # [0] user [1] date
2283 2289 if lock_info[0] and lock_info[1]:
2284 2290 log.debug('Repo %s is currently locked by %s', repo, user)
2285 2291 currently_locked = True
2286 2292 else:
2287 2293 log.debug('Setting lock on repo %s by %s', repo, user)
2288 2294 make_lock = True
2289 2295
2290 2296 else:
2291 2297 log.debug('Repository %s do not have locking enabled', repo)
2292 2298
2293 2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2294 2300 make_lock, currently_locked, lock_info)
2295 2301
2296 2302 from rhodecode.lib.auth import HasRepoPermissionAny
2297 2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2298 2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2299 2305 # if we don't have at least write permission we cannot make a lock
2300 2306 log.debug('lock state reset back to FALSE due to lack '
2301 2307 'of at least read permission')
2302 2308 make_lock = False
2303 2309
2304 2310 return make_lock, currently_locked, lock_info
2305 2311
2306 2312 @property
2307 2313 def last_commit_cache_update_diff(self):
2308 2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2309 2315
2310 2316 @classmethod
2311 2317 def _load_commit_change(cls, last_commit_cache):
2312 2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2313 2319 empty_date = datetime.datetime.fromtimestamp(0)
2314 2320 date_latest = last_commit_cache.get('date', empty_date)
2315 2321 try:
2316 2322 return parse_datetime(date_latest)
2317 2323 except Exception:
2318 2324 return empty_date
2319 2325
2320 2326 @property
2321 2327 def last_commit_change(self):
2322 2328 return self._load_commit_change(self.changeset_cache)
2323 2329
2324 2330 @property
2325 2331 def last_db_change(self):
2326 2332 return self.updated_on
2327 2333
2328 2334 @property
2329 2335 def clone_uri_hidden(self):
2330 2336 clone_uri = self.clone_uri
2331 2337 if clone_uri:
2332 2338 import urlobject
2333 2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2334 2340 if url_obj.password:
2335 2341 clone_uri = url_obj.with_password('*****')
2336 2342 return clone_uri
2337 2343
2338 2344 @property
2339 2345 def push_uri_hidden(self):
2340 2346 push_uri = self.push_uri
2341 2347 if push_uri:
2342 2348 import urlobject
2343 2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2344 2350 if url_obj.password:
2345 2351 push_uri = url_obj.with_password('*****')
2346 2352 return push_uri
2347 2353
2348 2354 def clone_url(self, **override):
2349 2355 from rhodecode.model.settings import SettingsModel
2350 2356
2351 2357 uri_tmpl = None
2352 2358 if 'with_id' in override:
2353 2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2354 2360 del override['with_id']
2355 2361
2356 2362 if 'uri_tmpl' in override:
2357 2363 uri_tmpl = override['uri_tmpl']
2358 2364 del override['uri_tmpl']
2359 2365
2360 2366 ssh = False
2361 2367 if 'ssh' in override:
2362 2368 ssh = True
2363 2369 del override['ssh']
2364 2370
2365 2371 # we didn't override our tmpl from **overrides
2366 2372 request = get_current_request()
2367 2373 if not uri_tmpl:
2368 2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2369 2375 rc_config = request.call_context.rc_config
2370 2376 else:
2371 2377 rc_config = SettingsModel().get_all_settings(cache=True)
2372 2378
2373 2379 if ssh:
2374 2380 uri_tmpl = rc_config.get(
2375 2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2376 2382
2377 2383 else:
2378 2384 uri_tmpl = rc_config.get(
2379 2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2380 2386
2381 2387 return get_clone_url(request=request,
2382 2388 uri_tmpl=uri_tmpl,
2383 2389 repo_name=self.repo_name,
2384 2390 repo_id=self.repo_id,
2385 2391 repo_type=self.repo_type,
2386 2392 **override)
2387 2393
2388 2394 def set_state(self, state):
2389 2395 self.repo_state = state
2390 2396 Session().add(self)
2391 2397 #==========================================================================
2392 2398 # SCM PROPERTIES
2393 2399 #==========================================================================
2394 2400
2395 2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2396 2402 return get_commit_safe(
2397 2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2398 2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2399 2405
2400 2406 def get_changeset(self, rev=None, pre_load=None):
2401 2407 warnings.warn("Use get_commit", DeprecationWarning)
2402 2408 commit_id = None
2403 2409 commit_idx = None
2404 2410 if isinstance(rev, str):
2405 2411 commit_id = rev
2406 2412 else:
2407 2413 commit_idx = rev
2408 2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2409 2415 pre_load=pre_load)
2410 2416
2411 2417 def get_landing_commit(self):
2412 2418 """
2413 2419 Returns landing commit, or if that doesn't exist returns the tip
2414 2420 """
2415 2421 _rev_type, _rev = self.landing_rev
2416 2422 commit = self.get_commit(_rev)
2417 2423 if isinstance(commit, EmptyCommit):
2418 2424 return self.get_commit()
2419 2425 return commit
2420 2426
2421 2427 def flush_commit_cache(self):
2422 2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2423 2429 self.update_commit_cache()
2424 2430
2425 2431 def update_commit_cache(self, cs_cache=None, config=None):
2426 2432 """
2427 2433 Update cache of last commit for repository
2428 2434 cache_keys should be::
2429 2435
2430 2436 source_repo_id
2431 2437 short_id
2432 2438 raw_id
2433 2439 revision
2434 2440 parents
2435 2441 message
2436 2442 date
2437 2443 author
2438 2444 updated_on
2439 2445
2440 2446 """
2441 2447 from rhodecode.lib.vcs.backends.base import BaseCommit
2442 2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2443 2449 empty_date = datetime.datetime.fromtimestamp(0)
2444 2450 repo_commit_count = 0
2445 2451
2446 2452 if cs_cache is None:
2447 2453 # use no-cache version here
2448 2454 try:
2449 2455 scm_repo = self.scm_instance(cache=False, config=config)
2450 2456 except VCSError:
2451 2457 scm_repo = None
2452 2458 empty = scm_repo is None or scm_repo.is_empty()
2453 2459
2454 2460 if not empty:
2455 2461 cs_cache = scm_repo.get_commit(
2456 2462 pre_load=["author", "date", "message", "parents", "branch"])
2457 2463 repo_commit_count = scm_repo.count()
2458 2464 else:
2459 2465 cs_cache = EmptyCommit()
2460 2466
2461 2467 if isinstance(cs_cache, BaseCommit):
2462 2468 cs_cache = cs_cache.__json__()
2463 2469
2464 2470 def is_outdated(new_cs_cache):
2465 2471 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2466 2472 new_cs_cache['revision'] != self.changeset_cache['revision']):
2467 2473 return True
2468 2474 return False
2469 2475
2470 2476 # check if we have maybe already latest cached revision
2471 2477 if is_outdated(cs_cache) or not self.changeset_cache:
2472 2478 _current_datetime = datetime.datetime.utcnow()
2473 2479 last_change = cs_cache.get('date') or _current_datetime
2474 2480 # we check if last update is newer than the new value
2475 2481 # if yes, we use the current timestamp instead. Imagine you get
2476 2482 # old commit pushed 1y ago, we'd set last update 1y to ago.
2477 2483 last_change_timestamp = datetime_to_time(last_change)
2478 2484 current_timestamp = datetime_to_time(last_change)
2479 2485 if last_change_timestamp > current_timestamp and not empty:
2480 2486 cs_cache['date'] = _current_datetime
2481 2487
2482 2488 # also store size of repo
2483 2489 cs_cache['repo_commit_count'] = repo_commit_count
2484 2490
2485 2491 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2486 2492 cs_cache['updated_on'] = time.time()
2487 2493 self.changeset_cache = cs_cache
2488 2494 self.updated_on = last_change
2489 2495 Session().add(self)
2490 2496 Session().commit()
2491 2497
2492 2498 else:
2493 2499 if empty:
2494 2500 cs_cache = EmptyCommit().__json__()
2495 2501 else:
2496 2502 cs_cache = self.changeset_cache
2497 2503
2498 2504 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2499 2505
2500 2506 cs_cache['updated_on'] = time.time()
2501 2507 self.changeset_cache = cs_cache
2502 2508 self.updated_on = _date_latest
2503 2509 Session().add(self)
2504 2510 Session().commit()
2505 2511
2506 2512 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2507 2513 self.repo_name, cs_cache, _date_latest)
2508 2514
2509 2515 @property
2510 2516 def tip(self):
2511 2517 return self.get_commit('tip')
2512 2518
2513 2519 @property
2514 2520 def author(self):
2515 2521 return self.tip.author
2516 2522
2517 2523 @property
2518 2524 def last_change(self):
2519 2525 return self.scm_instance().last_change
2520 2526
2521 2527 def get_comments(self, revisions=None):
2522 2528 """
2523 2529 Returns comments for this repository grouped by revisions
2524 2530
2525 2531 :param revisions: filter query by revisions only
2526 2532 """
2527 2533 cmts = ChangesetComment.query()\
2528 2534 .filter(ChangesetComment.repo == self)
2529 2535 if revisions:
2530 2536 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2531 2537 grouped = collections.defaultdict(list)
2532 2538 for cmt in cmts.all():
2533 2539 grouped[cmt.revision].append(cmt)
2534 2540 return grouped
2535 2541
2536 2542 def statuses(self, revisions=None):
2537 2543 """
2538 2544 Returns statuses for this repository
2539 2545
2540 2546 :param revisions: list of revisions to get statuses for
2541 2547 """
2542 2548 statuses = ChangesetStatus.query()\
2543 2549 .filter(ChangesetStatus.repo == self)\
2544 2550 .filter(ChangesetStatus.version == 0)
2545 2551
2546 2552 if revisions:
2547 2553 # Try doing the filtering in chunks to avoid hitting limits
2548 2554 size = 500
2549 2555 status_results = []
2550 2556 for chunk in range(0, len(revisions), size):
2551 2557 status_results += statuses.filter(
2552 2558 ChangesetStatus.revision.in_(
2553 2559 revisions[chunk: chunk+size])
2554 2560 ).all()
2555 2561 else:
2556 2562 status_results = statuses.all()
2557 2563
2558 2564 grouped = {}
2559 2565
2560 2566 # maybe we have open new pullrequest without a status?
2561 2567 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2562 2568 status_lbl = ChangesetStatus.get_status_lbl(stat)
2563 2569 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2564 2570 for rev in pr.revisions:
2565 2571 pr_id = pr.pull_request_id
2566 2572 pr_repo = pr.target_repo.repo_name
2567 2573 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2568 2574
2569 2575 for stat in status_results:
2570 2576 pr_id = pr_repo = None
2571 2577 if stat.pull_request:
2572 2578 pr_id = stat.pull_request.pull_request_id
2573 2579 pr_repo = stat.pull_request.target_repo.repo_name
2574 2580 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2575 2581 pr_id, pr_repo]
2576 2582 return grouped
2577 2583
2578 2584 # ==========================================================================
2579 2585 # SCM CACHE INSTANCE
2580 2586 # ==========================================================================
2581 2587
2582 2588 def scm_instance(self, **kwargs):
2583 2589 import rhodecode
2584 2590
2585 2591 # Passing a config will not hit the cache currently only used
2586 2592 # for repo2dbmapper
2587 2593 config = kwargs.pop('config', None)
2588 2594 cache = kwargs.pop('cache', None)
2589 2595 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2590 2596 if vcs_full_cache is not None:
2591 2597 # allows override global config
2592 2598 full_cache = vcs_full_cache
2593 2599 else:
2594 2600 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2595 2601 # if cache is NOT defined use default global, else we have a full
2596 2602 # control over cache behaviour
2597 2603 if cache is None and full_cache and not config:
2598 2604 log.debug('Initializing pure cached instance for %s', self.repo_path)
2599 2605 return self._get_instance_cached()
2600 2606
2601 2607 # cache here is sent to the "vcs server"
2602 2608 return self._get_instance(cache=bool(cache), config=config)
2603 2609
2604 2610 def _get_instance_cached(self):
2605 2611 from rhodecode.lib import rc_cache
2606 2612
2607 2613 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2608 2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2609 2615
2610 2616 # we must use thread scoped cache here,
2611 2617 # because each thread of gevent needs it's own not shared connection and cache
2612 2618 # we also alter `args` so the cache key is individual for every green thread.
2613 2619 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2614 2620 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2615 2621
2616 2622 # our wrapped caching function that takes state_uid to save the previous state in
2617 2623 def cache_generator(_state_uid):
2618 2624
2619 2625 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2620 2626 def get_instance_cached(_repo_id, _process_context_id):
2621 2627 # we save in cached func the generation state so we can detect a change and invalidate caches
2622 2628 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2623 2629
2624 2630 return get_instance_cached
2625 2631
2626 2632 with inv_context_manager as invalidation_context:
2627 2633 cache_state_uid = invalidation_context.state_uid
2628 2634 cache_func = cache_generator(cache_state_uid)
2629 2635
2630 2636 args = self.repo_id, inv_context_manager.proc_key
2631 2637
2632 2638 previous_state_uid, instance = cache_func(*args)
2633 2639
2634 2640 # now compare keys, the "cache" state vs expected state.
2635 2641 if previous_state_uid != cache_state_uid:
2636 2642 log.warning('Cached state uid %s is different than current state uid %s',
2637 2643 previous_state_uid, cache_state_uid)
2638 2644 _, instance = cache_func.refresh(*args)
2639 2645
2640 2646 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2641 2647 return instance
2642 2648
2643 2649 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2644 2650 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2645 2651 self.repo_type, self.repo_path, cache)
2646 2652 config = config or self._config
2647 2653 custom_wire = {
2648 2654 'cache': cache, # controls the vcs.remote cache
2649 2655 'repo_state_uid': repo_state_uid
2650 2656 }
2651 2657
2652 2658 repo = get_vcs_instance(
2653 2659 repo_path=safe_str(self.repo_full_path),
2654 2660 config=config,
2655 2661 with_wire=custom_wire,
2656 2662 create=False,
2657 2663 _vcs_alias=self.repo_type)
2658 2664 if repo is not None:
2659 2665 repo.count() # cache rebuild
2660 2666
2661 2667 return repo
2662 2668
2663 2669 def get_shadow_repository_path(self, workspace_id):
2664 2670 from rhodecode.lib.vcs.backends.base import BaseRepository
2665 2671 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2666 2672 self.repo_full_path, self.repo_id, workspace_id)
2667 2673 return shadow_repo_path
2668 2674
2669 2675 def __json__(self):
2670 2676 return {'landing_rev': self.landing_rev}
2671 2677
2672 2678 def get_dict(self):
2673 2679
2674 2680 # Since we transformed `repo_name` to a hybrid property, we need to
2675 2681 # keep compatibility with the code which uses `repo_name` field.
2676 2682
2677 2683 result = super(Repository, self).get_dict()
2678 2684 result['repo_name'] = result.pop('_repo_name', None)
2679 2685 result.pop('_changeset_cache', '')
2680 2686 return result
2681 2687
2682 2688
2683 2689 class RepoGroup(Base, BaseModel):
2684 2690 __tablename__ = 'groups'
2685 2691 __table_args__ = (
2686 2692 UniqueConstraint('group_name', 'group_parent_id'),
2687 2693 base_table_args,
2688 2694 )
2689 2695
2690 2696 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2691 2697
2692 2698 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2693 2699 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2694 2700 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2695 2701 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2696 2702 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2697 2703 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2698 2704 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2699 2705 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2700 2706 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2701 2707 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2702 2708 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2703 2709
2704 2710 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2705 2711 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2706 2712 parent_group = relationship('RepoGroup', remote_side=group_id)
2707 2713 user = relationship('User', back_populates='repository_groups')
2708 2714 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2709 2715
2710 2716 # no cascade, set NULL
2711 2717 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2712 2718
2713 2719 def __init__(self, group_name='', parent_group=None):
2714 2720 self.group_name = group_name
2715 2721 self.parent_group = parent_group
2716 2722
2717 2723 def __repr__(self):
2718 2724 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2719 2725
2720 2726 @hybrid_property
2721 2727 def group_name(self):
2722 2728 return self._group_name
2723 2729
2724 2730 @group_name.setter
2725 2731 def group_name(self, value):
2726 2732 self._group_name = value
2727 2733 self.group_name_hash = self.hash_repo_group_name(value)
2728 2734
2729 2735 @classmethod
2730 2736 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2731 2737 from rhodecode.lib.vcs.backends.base import EmptyCommit
2732 2738 dummy = EmptyCommit().__json__()
2733 2739 if not changeset_cache_raw:
2734 2740 dummy['source_repo_id'] = repo_id
2735 2741 return json.loads(json.dumps(dummy))
2736 2742
2737 2743 try:
2738 2744 return json.loads(changeset_cache_raw)
2739 2745 except TypeError:
2740 2746 return dummy
2741 2747 except Exception:
2742 2748 log.error(traceback.format_exc())
2743 2749 return dummy
2744 2750
2745 2751 @hybrid_property
2746 2752 def changeset_cache(self):
2747 2753 return self._load_changeset_cache('', self._changeset_cache)
2748 2754
2749 2755 @changeset_cache.setter
2750 2756 def changeset_cache(self, val):
2751 2757 try:
2752 2758 self._changeset_cache = json.dumps(val)
2753 2759 except Exception:
2754 2760 log.error(traceback.format_exc())
2755 2761
2756 2762 @validates('group_parent_id')
2757 2763 def validate_group_parent_id(self, key, val):
2758 2764 """
2759 2765 Check cycle references for a parent group to self
2760 2766 """
2761 2767 if self.group_id and val:
2762 2768 assert val != self.group_id
2763 2769
2764 2770 return val
2765 2771
2766 2772 @hybrid_property
2767 2773 def description_safe(self):
2768 2774 from rhodecode.lib import helpers as h
2769 2775 return h.escape(self.group_description)
2770 2776
2771 2777 @classmethod
2772 2778 def hash_repo_group_name(cls, repo_group_name):
2773 2779 val = remove_formatting(repo_group_name)
2774 2780 val = safe_str(val).lower()
2775 2781 chars = []
2776 2782 for c in val:
2777 2783 if c not in string.ascii_letters:
2778 2784 c = str(ord(c))
2779 2785 chars.append(c)
2780 2786
2781 2787 return ''.join(chars)
2782 2788
2783 2789 @classmethod
2784 2790 def _generate_choice(cls, repo_group):
2785 2791 from webhelpers2.html import literal as _literal
2786 2792
2787 2793 def _name(k):
2788 2794 return _literal(cls.CHOICES_SEPARATOR.join(k))
2789 2795
2790 2796 return repo_group.group_id, _name(repo_group.full_path_splitted)
2791 2797
2792 2798 @classmethod
2793 2799 def groups_choices(cls, groups=None, show_empty_group=True):
2794 2800 if not groups:
2795 2801 groups = cls.query().all()
2796 2802
2797 2803 repo_groups = []
2798 2804 if show_empty_group:
2799 2805 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2800 2806
2801 2807 repo_groups.extend([cls._generate_choice(x) for x in groups])
2802 2808
2803 2809 repo_groups = sorted(
2804 2810 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2805 2811 return repo_groups
2806 2812
2807 2813 @classmethod
2808 2814 def url_sep(cls):
2809 2815 return URL_SEP
2810 2816
2811 2817 @classmethod
2812 2818 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2813 2819 if case_insensitive:
2814 2820 gr = cls.query().filter(func.lower(cls.group_name)
2815 2821 == func.lower(group_name))
2816 2822 else:
2817 2823 gr = cls.query().filter(cls.group_name == group_name)
2818 2824 if cache:
2819 2825 name_key = _hash_key(group_name)
2820 2826 gr = gr.options(
2821 2827 FromCache("sql_cache_short", f"get_group_{name_key}"))
2822 2828 return gr.scalar()
2823 2829
2824 2830 @classmethod
2825 2831 def get_user_personal_repo_group(cls, user_id):
2826 2832 user = User.get(user_id)
2827 2833 if user.username == User.DEFAULT_USER:
2828 2834 return None
2829 2835
2830 2836 return cls.query()\
2831 2837 .filter(cls.personal == true()) \
2832 2838 .filter(cls.user == user) \
2833 2839 .order_by(cls.group_id.asc()) \
2834 2840 .first()
2835 2841
2836 2842 @classmethod
2837 2843 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2838 2844 case_insensitive=True):
2839 2845 q = RepoGroup.query()
2840 2846
2841 2847 if not isinstance(user_id, Optional):
2842 2848 q = q.filter(RepoGroup.user_id == user_id)
2843 2849
2844 2850 if not isinstance(group_id, Optional):
2845 2851 q = q.filter(RepoGroup.group_parent_id == group_id)
2846 2852
2847 2853 if case_insensitive:
2848 2854 q = q.order_by(func.lower(RepoGroup.group_name))
2849 2855 else:
2850 2856 q = q.order_by(RepoGroup.group_name)
2851 2857 return q.all()
2852 2858
2853 2859 @property
2854 2860 def parents(self, parents_recursion_limit=10):
2855 2861 groups = []
2856 2862 if self.parent_group is None:
2857 2863 return groups
2858 2864 cur_gr = self.parent_group
2859 2865 groups.insert(0, cur_gr)
2860 2866 cnt = 0
2861 2867 while 1:
2862 2868 cnt += 1
2863 2869 gr = getattr(cur_gr, 'parent_group', None)
2864 2870 cur_gr = cur_gr.parent_group
2865 2871 if gr is None:
2866 2872 break
2867 2873 if cnt == parents_recursion_limit:
2868 2874 # this will prevent accidental infinit loops
2869 2875 log.error('more than %s parents found for group %s, stopping '
2870 2876 'recursive parent fetching', parents_recursion_limit, self)
2871 2877 break
2872 2878
2873 2879 groups.insert(0, gr)
2874 2880 return groups
2875 2881
2876 2882 @property
2877 2883 def last_commit_cache_update_diff(self):
2878 2884 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2879 2885
2880 2886 @classmethod
2881 2887 def _load_commit_change(cls, last_commit_cache):
2882 2888 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2883 2889 empty_date = datetime.datetime.fromtimestamp(0)
2884 2890 date_latest = last_commit_cache.get('date', empty_date)
2885 2891 try:
2886 2892 return parse_datetime(date_latest)
2887 2893 except Exception:
2888 2894 return empty_date
2889 2895
2890 2896 @property
2891 2897 def last_commit_change(self):
2892 2898 return self._load_commit_change(self.changeset_cache)
2893 2899
2894 2900 @property
2895 2901 def last_db_change(self):
2896 2902 return self.updated_on
2897 2903
2898 2904 @property
2899 2905 def children(self):
2900 2906 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2901 2907
2902 2908 @property
2903 2909 def name(self):
2904 2910 return self.group_name.split(RepoGroup.url_sep())[-1]
2905 2911
2906 2912 @property
2907 2913 def full_path(self):
2908 2914 return self.group_name
2909 2915
2910 2916 @property
2911 2917 def full_path_splitted(self):
2912 2918 return self.group_name.split(RepoGroup.url_sep())
2913 2919
2914 2920 @property
2915 2921 def repositories(self):
2916 2922 return Repository.query()\
2917 2923 .filter(Repository.group == self)\
2918 2924 .order_by(Repository.repo_name)
2919 2925
2920 2926 @property
2921 2927 def repositories_recursive_count(self):
2922 2928 cnt = self.repositories.count()
2923 2929
2924 2930 def children_count(group):
2925 2931 cnt = 0
2926 2932 for child in group.children:
2927 2933 cnt += child.repositories.count()
2928 2934 cnt += children_count(child)
2929 2935 return cnt
2930 2936
2931 2937 return cnt + children_count(self)
2932 2938
2933 2939 def _recursive_objects(self, include_repos=True, include_groups=True):
2934 2940 all_ = []
2935 2941
2936 2942 def _get_members(root_gr):
2937 2943 if include_repos:
2938 2944 for r in root_gr.repositories:
2939 2945 all_.append(r)
2940 2946 childs = root_gr.children.all()
2941 2947 if childs:
2942 2948 for gr in childs:
2943 2949 if include_groups:
2944 2950 all_.append(gr)
2945 2951 _get_members(gr)
2946 2952
2947 2953 root_group = []
2948 2954 if include_groups:
2949 2955 root_group = [self]
2950 2956
2951 2957 _get_members(self)
2952 2958 return root_group + all_
2953 2959
2954 2960 def recursive_groups_and_repos(self):
2955 2961 """
2956 2962 Recursive return all groups, with repositories in those groups
2957 2963 """
2958 2964 return self._recursive_objects()
2959 2965
2960 2966 def recursive_groups(self):
2961 2967 """
2962 2968 Returns all children groups for this group including children of children
2963 2969 """
2964 2970 return self._recursive_objects(include_repos=False)
2965 2971
2966 2972 def recursive_repos(self):
2967 2973 """
2968 2974 Returns all children repositories for this group
2969 2975 """
2970 2976 return self._recursive_objects(include_groups=False)
2971 2977
2972 2978 def get_new_name(self, group_name):
2973 2979 """
2974 2980 returns new full group name based on parent and new name
2975 2981
2976 2982 :param group_name:
2977 2983 """
2978 2984 path_prefix = (self.parent_group.full_path_splitted if
2979 2985 self.parent_group else [])
2980 2986 return RepoGroup.url_sep().join(path_prefix + [group_name])
2981 2987
2982 2988 def update_commit_cache(self, config=None):
2983 2989 """
2984 2990 Update cache of last commit for newest repository inside this repository group.
2985 2991 cache_keys should be::
2986 2992
2987 2993 source_repo_id
2988 2994 short_id
2989 2995 raw_id
2990 2996 revision
2991 2997 parents
2992 2998 message
2993 2999 date
2994 3000 author
2995 3001
2996 3002 """
2997 3003 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2998 3004 empty_date = datetime.datetime.fromtimestamp(0)
2999 3005
3000 3006 def repo_groups_and_repos(root_gr):
3001 3007 for _repo in root_gr.repositories:
3002 3008 yield _repo
3003 3009 for child_group in root_gr.children.all():
3004 3010 yield child_group
3005 3011
3006 3012 latest_repo_cs_cache = {}
3007 3013 for obj in repo_groups_and_repos(self):
3008 3014 repo_cs_cache = obj.changeset_cache
3009 3015 date_latest = latest_repo_cs_cache.get('date', empty_date)
3010 3016 date_current = repo_cs_cache.get('date', empty_date)
3011 3017 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3012 3018 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3013 3019 latest_repo_cs_cache = repo_cs_cache
3014 3020 if hasattr(obj, 'repo_id'):
3015 3021 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3016 3022 else:
3017 3023 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3018 3024
3019 3025 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3020 3026
3021 3027 latest_repo_cs_cache['updated_on'] = time.time()
3022 3028 self.changeset_cache = latest_repo_cs_cache
3023 3029 self.updated_on = _date_latest
3024 3030 Session().add(self)
3025 3031 Session().commit()
3026 3032
3027 3033 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3028 3034 self.group_name, latest_repo_cs_cache, _date_latest)
3029 3035
3030 3036 def permissions(self, with_admins=True, with_owner=True,
3031 3037 expand_from_user_groups=False):
3032 3038 """
3033 3039 Permissions for repository groups
3034 3040 """
3035 3041 _admin_perm = 'group.admin'
3036 3042
3037 3043 owner_row = []
3038 3044 if with_owner:
3039 3045 usr = AttributeDict(self.user.get_dict())
3040 3046 usr.owner_row = True
3041 3047 usr.permission = _admin_perm
3042 3048 owner_row.append(usr)
3043 3049
3044 3050 super_admin_ids = []
3045 3051 super_admin_rows = []
3046 3052 if with_admins:
3047 3053 for usr in User.get_all_super_admins():
3048 3054 super_admin_ids.append(usr.user_id)
3049 3055 # if this admin is also owner, don't double the record
3050 3056 if usr.user_id == owner_row[0].user_id:
3051 3057 owner_row[0].admin_row = True
3052 3058 else:
3053 3059 usr = AttributeDict(usr.get_dict())
3054 3060 usr.admin_row = True
3055 3061 usr.permission = _admin_perm
3056 3062 super_admin_rows.append(usr)
3057 3063
3058 3064 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3059 3065 q = q.options(joinedload(UserRepoGroupToPerm.group),
3060 3066 joinedload(UserRepoGroupToPerm.user),
3061 3067 joinedload(UserRepoGroupToPerm.permission),)
3062 3068
3063 3069 # get owners and admins and permissions. We do a trick of re-writing
3064 3070 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3065 3071 # has a global reference and changing one object propagates to all
3066 3072 # others. This means if admin is also an owner admin_row that change
3067 3073 # would propagate to both objects
3068 3074 perm_rows = []
3069 3075 for _usr in q.all():
3070 3076 usr = AttributeDict(_usr.user.get_dict())
3071 3077 # if this user is also owner/admin, mark as duplicate record
3072 3078 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3073 3079 usr.duplicate_perm = True
3074 3080 usr.permission = _usr.permission.permission_name
3075 3081 perm_rows.append(usr)
3076 3082
3077 3083 # filter the perm rows by 'default' first and then sort them by
3078 3084 # admin,write,read,none permissions sorted again alphabetically in
3079 3085 # each group
3080 3086 perm_rows = sorted(perm_rows, key=display_user_sort)
3081 3087
3082 3088 user_groups_rows = []
3083 3089 if expand_from_user_groups:
3084 3090 for ug in self.permission_user_groups(with_members=True):
3085 3091 for user_data in ug.members:
3086 3092 user_groups_rows.append(user_data)
3087 3093
3088 3094 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3089 3095
3090 3096 def permission_user_groups(self, with_members=False):
3091 3097 q = UserGroupRepoGroupToPerm.query()\
3092 3098 .filter(UserGroupRepoGroupToPerm.group == self)
3093 3099 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3094 3100 joinedload(UserGroupRepoGroupToPerm.users_group),
3095 3101 joinedload(UserGroupRepoGroupToPerm.permission),)
3096 3102
3097 3103 perm_rows = []
3098 3104 for _user_group in q.all():
3099 3105 entry = AttributeDict(_user_group.users_group.get_dict())
3100 3106 entry.permission = _user_group.permission.permission_name
3101 3107 if with_members:
3102 3108 entry.members = [x.user.get_dict()
3103 3109 for x in _user_group.users_group.members]
3104 3110 perm_rows.append(entry)
3105 3111
3106 3112 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3107 3113 return perm_rows
3108 3114
3109 3115 def get_api_data(self):
3110 3116 """
3111 3117 Common function for generating api data
3112 3118
3113 3119 """
3114 3120 group = self
3115 3121 data = {
3116 3122 'group_id': group.group_id,
3117 3123 'group_name': group.group_name,
3118 3124 'group_description': group.description_safe,
3119 3125 'parent_group': group.parent_group.group_name if group.parent_group else None,
3120 3126 'repositories': [x.repo_name for x in group.repositories],
3121 3127 'owner': group.user.username,
3122 3128 }
3123 3129 return data
3124 3130
3125 3131 def get_dict(self):
3126 3132 # Since we transformed `group_name` to a hybrid property, we need to
3127 3133 # keep compatibility with the code which uses `group_name` field.
3128 3134 result = super(RepoGroup, self).get_dict()
3129 3135 result['group_name'] = result.pop('_group_name', None)
3130 3136 result.pop('_changeset_cache', '')
3131 3137 return result
3132 3138
3133 3139
3134 3140 class Permission(Base, BaseModel):
3135 3141 __tablename__ = 'permissions'
3136 3142 __table_args__ = (
3137 3143 Index('p_perm_name_idx', 'permission_name'),
3138 3144 base_table_args,
3139 3145 )
3140 3146
3141 3147 PERMS = [
3142 3148 ('hg.admin', _('RhodeCode Super Administrator')),
3143 3149
3144 3150 ('repository.none', _('Repository no access')),
3145 3151 ('repository.read', _('Repository read access')),
3146 3152 ('repository.write', _('Repository write access')),
3147 3153 ('repository.admin', _('Repository admin access')),
3148 3154
3149 3155 ('group.none', _('Repository group no access')),
3150 3156 ('group.read', _('Repository group read access')),
3151 3157 ('group.write', _('Repository group write access')),
3152 3158 ('group.admin', _('Repository group admin access')),
3153 3159
3154 3160 ('usergroup.none', _('User group no access')),
3155 3161 ('usergroup.read', _('User group read access')),
3156 3162 ('usergroup.write', _('User group write access')),
3157 3163 ('usergroup.admin', _('User group admin access')),
3158 3164
3159 3165 ('branch.none', _('Branch no permissions')),
3160 3166 ('branch.merge', _('Branch access by web merge')),
3161 3167 ('branch.push', _('Branch access by push')),
3162 3168 ('branch.push_force', _('Branch access by push with force')),
3163 3169
3164 3170 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3165 3171 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3166 3172
3167 3173 ('hg.usergroup.create.false', _('User Group creation disabled')),
3168 3174 ('hg.usergroup.create.true', _('User Group creation enabled')),
3169 3175
3170 3176 ('hg.create.none', _('Repository creation disabled')),
3171 3177 ('hg.create.repository', _('Repository creation enabled')),
3172 3178 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3173 3179 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3174 3180
3175 3181 ('hg.fork.none', _('Repository forking disabled')),
3176 3182 ('hg.fork.repository', _('Repository forking enabled')),
3177 3183
3178 3184 ('hg.register.none', _('Registration disabled')),
3179 3185 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3180 3186 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3181 3187
3182 3188 ('hg.password_reset.enabled', _('Password reset enabled')),
3183 3189 ('hg.password_reset.hidden', _('Password reset hidden')),
3184 3190 ('hg.password_reset.disabled', _('Password reset disabled')),
3185 3191
3186 3192 ('hg.extern_activate.manual', _('Manual activation of external account')),
3187 3193 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3188 3194
3189 3195 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3190 3196 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3191 3197 ]
3192 3198
3193 3199 # definition of system default permissions for DEFAULT user, created on
3194 3200 # system setup
3195 3201 DEFAULT_USER_PERMISSIONS = [
3196 3202 # object perms
3197 3203 'repository.read',
3198 3204 'group.read',
3199 3205 'usergroup.read',
3200 3206 # branch, for backward compat we need same value as before so forced pushed
3201 3207 'branch.push_force',
3202 3208 # global
3203 3209 'hg.create.repository',
3204 3210 'hg.repogroup.create.false',
3205 3211 'hg.usergroup.create.false',
3206 3212 'hg.create.write_on_repogroup.true',
3207 3213 'hg.fork.repository',
3208 3214 'hg.register.manual_activate',
3209 3215 'hg.password_reset.enabled',
3210 3216 'hg.extern_activate.auto',
3211 3217 'hg.inherit_default_perms.true',
3212 3218 ]
3213 3219
3214 3220 # defines which permissions are more important higher the more important
3215 3221 # Weight defines which permissions are more important.
3216 3222 # The higher number the more important.
3217 3223 PERM_WEIGHTS = {
3218 3224 'repository.none': 0,
3219 3225 'repository.read': 1,
3220 3226 'repository.write': 3,
3221 3227 'repository.admin': 4,
3222 3228
3223 3229 'group.none': 0,
3224 3230 'group.read': 1,
3225 3231 'group.write': 3,
3226 3232 'group.admin': 4,
3227 3233
3228 3234 'usergroup.none': 0,
3229 3235 'usergroup.read': 1,
3230 3236 'usergroup.write': 3,
3231 3237 'usergroup.admin': 4,
3232 3238
3233 3239 'branch.none': 0,
3234 3240 'branch.merge': 1,
3235 3241 'branch.push': 3,
3236 3242 'branch.push_force': 4,
3237 3243
3238 3244 'hg.repogroup.create.false': 0,
3239 3245 'hg.repogroup.create.true': 1,
3240 3246
3241 3247 'hg.usergroup.create.false': 0,
3242 3248 'hg.usergroup.create.true': 1,
3243 3249
3244 3250 'hg.fork.none': 0,
3245 3251 'hg.fork.repository': 1,
3246 3252 'hg.create.none': 0,
3247 3253 'hg.create.repository': 1
3248 3254 }
3249 3255
3250 3256 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3251 3257 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3252 3258 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3253 3259
3254 3260 def __repr__(self):
3255 3261 return "<%s('%s:%s')>" % (
3256 3262 self.cls_name, self.permission_id, self.permission_name
3257 3263 )
3258 3264
3259 3265 @classmethod
3260 3266 def get_by_key(cls, key):
3261 3267 return cls.query().filter(cls.permission_name == key).scalar()
3262 3268
3263 3269 @classmethod
3264 3270 def get_default_repo_perms(cls, user_id, repo_id=None):
3265 3271 q = Session().query(UserRepoToPerm, Repository, Permission)\
3266 3272 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3267 3273 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3268 3274 .filter(UserRepoToPerm.user_id == user_id)
3269 3275 if repo_id:
3270 3276 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3271 3277 return q.all()
3272 3278
3273 3279 @classmethod
3274 3280 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3275 3281 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3276 3282 .join(
3277 3283 Permission,
3278 3284 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3279 3285 .join(
3280 3286 UserRepoToPerm,
3281 3287 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3282 3288 .filter(UserRepoToPerm.user_id == user_id)
3283 3289
3284 3290 if repo_id:
3285 3291 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3286 3292 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3287 3293
3288 3294 @classmethod
3289 3295 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3290 3296 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3291 3297 .join(
3292 3298 Permission,
3293 3299 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3294 3300 .join(
3295 3301 Repository,
3296 3302 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3297 3303 .join(
3298 3304 UserGroup,
3299 3305 UserGroupRepoToPerm.users_group_id ==
3300 3306 UserGroup.users_group_id)\
3301 3307 .join(
3302 3308 UserGroupMember,
3303 3309 UserGroupRepoToPerm.users_group_id ==
3304 3310 UserGroupMember.users_group_id)\
3305 3311 .filter(
3306 3312 UserGroupMember.user_id == user_id,
3307 3313 UserGroup.users_group_active == true())
3308 3314 if repo_id:
3309 3315 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3310 3316 return q.all()
3311 3317
3312 3318 @classmethod
3313 3319 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3314 3320 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3315 3321 .join(
3316 3322 Permission,
3317 3323 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3318 3324 .join(
3319 3325 UserGroupRepoToPerm,
3320 3326 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3321 3327 .join(
3322 3328 UserGroup,
3323 3329 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3324 3330 .join(
3325 3331 UserGroupMember,
3326 3332 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3327 3333 .filter(
3328 3334 UserGroupMember.user_id == user_id,
3329 3335 UserGroup.users_group_active == true())
3330 3336
3331 3337 if repo_id:
3332 3338 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3333 3339 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3334 3340
3335 3341 @classmethod
3336 3342 def get_default_group_perms(cls, user_id, repo_group_id=None):
3337 3343 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3338 3344 .join(
3339 3345 Permission,
3340 3346 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3341 3347 .join(
3342 3348 RepoGroup,
3343 3349 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3344 3350 .filter(UserRepoGroupToPerm.user_id == user_id)
3345 3351 if repo_group_id:
3346 3352 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3347 3353 return q.all()
3348 3354
3349 3355 @classmethod
3350 3356 def get_default_group_perms_from_user_group(
3351 3357 cls, user_id, repo_group_id=None):
3352 3358 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3353 3359 .join(
3354 3360 Permission,
3355 3361 UserGroupRepoGroupToPerm.permission_id ==
3356 3362 Permission.permission_id)\
3357 3363 .join(
3358 3364 RepoGroup,
3359 3365 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3360 3366 .join(
3361 3367 UserGroup,
3362 3368 UserGroupRepoGroupToPerm.users_group_id ==
3363 3369 UserGroup.users_group_id)\
3364 3370 .join(
3365 3371 UserGroupMember,
3366 3372 UserGroupRepoGroupToPerm.users_group_id ==
3367 3373 UserGroupMember.users_group_id)\
3368 3374 .filter(
3369 3375 UserGroupMember.user_id == user_id,
3370 3376 UserGroup.users_group_active == true())
3371 3377 if repo_group_id:
3372 3378 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3373 3379 return q.all()
3374 3380
3375 3381 @classmethod
3376 3382 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3377 3383 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3378 3384 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3379 3385 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3380 3386 .filter(UserUserGroupToPerm.user_id == user_id)
3381 3387 if user_group_id:
3382 3388 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3383 3389 return q.all()
3384 3390
3385 3391 @classmethod
3386 3392 def get_default_user_group_perms_from_user_group(
3387 3393 cls, user_id, user_group_id=None):
3388 3394 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3389 3395 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3390 3396 .join(
3391 3397 Permission,
3392 3398 UserGroupUserGroupToPerm.permission_id ==
3393 3399 Permission.permission_id)\
3394 3400 .join(
3395 3401 TargetUserGroup,
3396 3402 UserGroupUserGroupToPerm.target_user_group_id ==
3397 3403 TargetUserGroup.users_group_id)\
3398 3404 .join(
3399 3405 UserGroup,
3400 3406 UserGroupUserGroupToPerm.user_group_id ==
3401 3407 UserGroup.users_group_id)\
3402 3408 .join(
3403 3409 UserGroupMember,
3404 3410 UserGroupUserGroupToPerm.user_group_id ==
3405 3411 UserGroupMember.users_group_id)\
3406 3412 .filter(
3407 3413 UserGroupMember.user_id == user_id,
3408 3414 UserGroup.users_group_active == true())
3409 3415 if user_group_id:
3410 3416 q = q.filter(
3411 3417 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3412 3418
3413 3419 return q.all()
3414 3420
3415 3421
3416 3422 class UserRepoToPerm(Base, BaseModel):
3417 3423 __tablename__ = 'repo_to_perm'
3418 3424 __table_args__ = (
3419 3425 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3420 3426 base_table_args
3421 3427 )
3422 3428
3423 3429 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3424 3430 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3425 3431 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3426 3432 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3427 3433
3428 3434 user = relationship('User', back_populates="repo_to_perm")
3429 3435 repository = relationship('Repository', back_populates="repo_to_perm")
3430 3436 permission = relationship('Permission')
3431 3437
3432 3438 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3433 3439
3434 3440 @classmethod
3435 3441 def create(cls, user, repository, permission):
3436 3442 n = cls()
3437 3443 n.user = user
3438 3444 n.repository = repository
3439 3445 n.permission = permission
3440 3446 Session().add(n)
3441 3447 return n
3442 3448
3443 3449 def __repr__(self):
3444 3450 return f'<{self.user} => {self.repository} >'
3445 3451
3446 3452
3447 3453 class UserUserGroupToPerm(Base, BaseModel):
3448 3454 __tablename__ = 'user_user_group_to_perm'
3449 3455 __table_args__ = (
3450 3456 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3451 3457 base_table_args
3452 3458 )
3453 3459
3454 3460 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3455 3461 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3456 3462 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3457 3463 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3458 3464
3459 3465 user = relationship('User', back_populates='user_group_to_perm')
3460 3466 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3461 3467 permission = relationship('Permission')
3462 3468
3463 3469 @classmethod
3464 3470 def create(cls, user, user_group, permission):
3465 3471 n = cls()
3466 3472 n.user = user
3467 3473 n.user_group = user_group
3468 3474 n.permission = permission
3469 3475 Session().add(n)
3470 3476 return n
3471 3477
3472 3478 def __repr__(self):
3473 3479 return f'<{self.user} => {self.user_group} >'
3474 3480
3475 3481
3476 3482 class UserToPerm(Base, BaseModel):
3477 3483 __tablename__ = 'user_to_perm'
3478 3484 __table_args__ = (
3479 3485 UniqueConstraint('user_id', 'permission_id'),
3480 3486 base_table_args
3481 3487 )
3482 3488
3483 3489 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3484 3490 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3485 3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3486 3492
3487 3493 user = relationship('User', back_populates='user_perms')
3488 3494 permission = relationship('Permission', lazy='joined')
3489 3495
3490 3496 def __repr__(self):
3491 3497 return f'<{self.user} => {self.permission} >'
3492 3498
3493 3499
3494 3500 class UserGroupRepoToPerm(Base, BaseModel):
3495 3501 __tablename__ = 'users_group_repo_to_perm'
3496 3502 __table_args__ = (
3497 3503 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3498 3504 base_table_args
3499 3505 )
3500 3506
3501 3507 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3502 3508 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3503 3509 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3504 3510 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3505 3511
3506 3512 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3507 3513 permission = relationship('Permission')
3508 3514 repository = relationship('Repository', back_populates='users_group_to_perm')
3509 3515 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3510 3516
3511 3517 @classmethod
3512 3518 def create(cls, users_group, repository, permission):
3513 3519 n = cls()
3514 3520 n.users_group = users_group
3515 3521 n.repository = repository
3516 3522 n.permission = permission
3517 3523 Session().add(n)
3518 3524 return n
3519 3525
3520 3526 def __repr__(self):
3521 3527 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3522 3528
3523 3529
3524 3530 class UserGroupUserGroupToPerm(Base, BaseModel):
3525 3531 __tablename__ = 'user_group_user_group_to_perm'
3526 3532 __table_args__ = (
3527 3533 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3528 3534 CheckConstraint('target_user_group_id != user_group_id'),
3529 3535 base_table_args
3530 3536 )
3531 3537
3532 3538 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3533 3539 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3534 3540 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3535 3541 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3536 3542
3537 3543 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3538 3544 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3539 3545 permission = relationship('Permission')
3540 3546
3541 3547 @classmethod
3542 3548 def create(cls, target_user_group, user_group, permission):
3543 3549 n = cls()
3544 3550 n.target_user_group = target_user_group
3545 3551 n.user_group = user_group
3546 3552 n.permission = permission
3547 3553 Session().add(n)
3548 3554 return n
3549 3555
3550 3556 def __repr__(self):
3551 3557 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3552 3558
3553 3559
3554 3560 class UserGroupToPerm(Base, BaseModel):
3555 3561 __tablename__ = 'users_group_to_perm'
3556 3562 __table_args__ = (
3557 3563 UniqueConstraint('users_group_id', 'permission_id',),
3558 3564 base_table_args
3559 3565 )
3560 3566
3561 3567 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3562 3568 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3563 3569 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3564 3570
3565 3571 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3566 3572 permission = relationship('Permission')
3567 3573
3568 3574
3569 3575 class UserRepoGroupToPerm(Base, BaseModel):
3570 3576 __tablename__ = 'user_repo_group_to_perm'
3571 3577 __table_args__ = (
3572 3578 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3573 3579 base_table_args
3574 3580 )
3575 3581
3576 3582 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3577 3583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3578 3584 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3579 3585 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3580 3586
3581 3587 user = relationship('User', back_populates='repo_group_to_perm')
3582 3588 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3583 3589 permission = relationship('Permission')
3584 3590
3585 3591 @classmethod
3586 3592 def create(cls, user, repository_group, permission):
3587 3593 n = cls()
3588 3594 n.user = user
3589 3595 n.group = repository_group
3590 3596 n.permission = permission
3591 3597 Session().add(n)
3592 3598 return n
3593 3599
3594 3600
3595 3601 class UserGroupRepoGroupToPerm(Base, BaseModel):
3596 3602 __tablename__ = 'users_group_repo_group_to_perm'
3597 3603 __table_args__ = (
3598 3604 UniqueConstraint('users_group_id', 'group_id'),
3599 3605 base_table_args
3600 3606 )
3601 3607
3602 3608 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3603 3609 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3604 3610 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3605 3611 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3606 3612
3607 3613 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3608 3614 permission = relationship('Permission')
3609 3615 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3610 3616
3611 3617 @classmethod
3612 3618 def create(cls, user_group, repository_group, permission):
3613 3619 n = cls()
3614 3620 n.users_group = user_group
3615 3621 n.group = repository_group
3616 3622 n.permission = permission
3617 3623 Session().add(n)
3618 3624 return n
3619 3625
3620 3626 def __repr__(self):
3621 3627 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3622 3628
3623 3629
3624 3630 class Statistics(Base, BaseModel):
3625 3631 __tablename__ = 'statistics'
3626 3632 __table_args__ = (
3627 3633 base_table_args
3628 3634 )
3629 3635
3630 3636 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3631 3637 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3632 3638 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3633 3639 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3634 3640 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3635 3641 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3636 3642
3637 3643 repository = relationship('Repository', single_parent=True, viewonly=True)
3638 3644
3639 3645
3640 3646 class UserFollowing(Base, BaseModel):
3641 3647 __tablename__ = 'user_followings'
3642 3648 __table_args__ = (
3643 3649 UniqueConstraint('user_id', 'follows_repository_id'),
3644 3650 UniqueConstraint('user_id', 'follows_user_id'),
3645 3651 base_table_args
3646 3652 )
3647 3653
3648 3654 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3649 3655 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3650 3656 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3651 3657 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3652 3658 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3653 3659
3654 3660 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3655 3661
3656 3662 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3657 3663 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3658 3664
3659 3665 @classmethod
3660 3666 def get_repo_followers(cls, repo_id):
3661 3667 return cls.query().filter(cls.follows_repo_id == repo_id)
3662 3668
3663 3669
3664 3670 class CacheKey(Base, BaseModel):
3665 3671 __tablename__ = 'cache_invalidation'
3666 3672 __table_args__ = (
3667 3673 UniqueConstraint('cache_key'),
3668 3674 Index('key_idx', 'cache_key'),
3669 3675 Index('cache_args_idx', 'cache_args'),
3670 3676 base_table_args,
3671 3677 )
3672 3678
3673 3679 CACHE_TYPE_FEED = 'FEED'
3674 3680
3675 3681 # namespaces used to register process/thread aware caches
3676 3682 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3677 3683
3678 3684 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3679 3685 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3680 3686 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3681 3687 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3682 3688 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3683 3689
3684 3690 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3685 3691 self.cache_key = cache_key
3686 3692 self.cache_args = cache_args
3687 3693 self.cache_active = cache_active
3688 3694 # first key should be same for all entries, since all workers should share it
3689 3695 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3690 3696
3691 3697 def __repr__(self):
3692 3698 return "<%s('%s:%s[%s]')>" % (
3693 3699 self.cls_name,
3694 3700 self.cache_id, self.cache_key, self.cache_active)
3695 3701
3696 3702 def _cache_key_partition(self):
3697 3703 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3698 3704 return prefix, repo_name, suffix
3699 3705
3700 3706 def get_prefix(self):
3701 3707 """
3702 3708 Try to extract prefix from existing cache key. The key could consist
3703 3709 of prefix, repo_name, suffix
3704 3710 """
3705 3711 # this returns prefix, repo_name, suffix
3706 3712 return self._cache_key_partition()[0]
3707 3713
3708 3714 def get_suffix(self):
3709 3715 """
3710 3716 get suffix that might have been used in _get_cache_key to
3711 3717 generate self.cache_key. Only used for informational purposes
3712 3718 in repo_edit.mako.
3713 3719 """
3714 3720 # prefix, repo_name, suffix
3715 3721 return self._cache_key_partition()[2]
3716 3722
3717 3723 @classmethod
3718 3724 def generate_new_state_uid(cls, based_on=None):
3719 3725 if based_on:
3720 3726 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3721 3727 else:
3722 3728 return str(uuid.uuid4())
3723 3729
3724 3730 @classmethod
3725 3731 def delete_all_cache(cls):
3726 3732 """
3727 3733 Delete all cache keys from database.
3728 3734 Should only be run when all instances are down and all entries
3729 3735 thus stale.
3730 3736 """
3731 3737 cls.query().delete()
3732 3738 Session().commit()
3733 3739
3734 3740 @classmethod
3735 3741 def set_invalidate(cls, cache_uid, delete=False):
3736 3742 """
3737 3743 Mark all caches of a repo as invalid in the database.
3738 3744 """
3739 3745 try:
3740 3746 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3741 3747 if delete:
3742 3748 qry.delete()
3743 3749 log.debug('cache objects deleted for cache args %s',
3744 3750 safe_str(cache_uid))
3745 3751 else:
3746 3752 new_uid = cls.generate_new_state_uid()
3747 3753 qry.update({"cache_state_uid": new_uid,
3748 3754 "cache_args": f"repo_state:{time.time()}"})
3749 3755 log.debug('cache object %s set new UID %s',
3750 3756 safe_str(cache_uid), new_uid)
3751 3757
3752 3758 Session().commit()
3753 3759 except Exception:
3754 3760 log.exception(
3755 3761 'Cache key invalidation failed for cache args %s',
3756 3762 safe_str(cache_uid))
3757 3763 Session().rollback()
3758 3764
3759 3765 @classmethod
3760 3766 def get_active_cache(cls, cache_key):
3761 3767 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3762 3768 if inv_obj:
3763 3769 return inv_obj
3764 3770 return None
3765 3771
3766 3772 @classmethod
3767 3773 def get_namespace_map(cls, namespace):
3768 3774 return {
3769 3775 x.cache_key: x
3770 3776 for x in cls.query().filter(cls.cache_args == namespace)}
3771 3777
3772 3778
3773 3779 class ChangesetComment(Base, BaseModel):
3774 3780 __tablename__ = 'changeset_comments'
3775 3781 __table_args__ = (
3776 3782 Index('cc_revision_idx', 'revision'),
3777 3783 base_table_args,
3778 3784 )
3779 3785
3780 3786 COMMENT_OUTDATED = 'comment_outdated'
3781 3787 COMMENT_TYPE_NOTE = 'note'
3782 3788 COMMENT_TYPE_TODO = 'todo'
3783 3789 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3784 3790
3785 3791 OP_IMMUTABLE = 'immutable'
3786 3792 OP_CHANGEABLE = 'changeable'
3787 3793
3788 3794 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3789 3795 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3790 3796 revision = Column('revision', String(40), nullable=True)
3791 3797 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3792 3798 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3793 3799 line_no = Column('line_no', Unicode(10), nullable=True)
3794 3800 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3795 3801 f_path = Column('f_path', Unicode(1000), nullable=True)
3796 3802 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3797 3803 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3798 3804 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3799 3805 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3800 3806 renderer = Column('renderer', Unicode(64), nullable=True)
3801 3807 display_state = Column('display_state', Unicode(128), nullable=True)
3802 3808 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3803 3809 draft = Column('draft', Boolean(), nullable=True, default=False)
3804 3810
3805 3811 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3806 3812 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3807 3813
3808 3814 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3809 3815 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3810 3816
3811 3817 author = relationship('User', lazy='select', back_populates='user_comments')
3812 3818 repo = relationship('Repository', back_populates='comments')
3813 3819 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3814 3820 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3815 3821 pull_request_version = relationship('PullRequestVersion', lazy='select')
3816 3822 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3817 3823
3818 3824 @classmethod
3819 3825 def get_users(cls, revision=None, pull_request_id=None):
3820 3826 """
3821 3827 Returns user associated with this ChangesetComment. ie those
3822 3828 who actually commented
3823 3829
3824 3830 :param cls:
3825 3831 :param revision:
3826 3832 """
3827 3833 q = Session().query(User).join(ChangesetComment.author)
3828 3834 if revision:
3829 3835 q = q.filter(cls.revision == revision)
3830 3836 elif pull_request_id:
3831 3837 q = q.filter(cls.pull_request_id == pull_request_id)
3832 3838 return q.all()
3833 3839
3834 3840 @classmethod
3835 3841 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3836 3842 if pr_version is None:
3837 3843 return 0
3838 3844
3839 3845 if versions is not None:
3840 3846 num_versions = [x.pull_request_version_id for x in versions]
3841 3847
3842 3848 num_versions = num_versions or []
3843 3849 try:
3844 3850 return num_versions.index(pr_version) + 1
3845 3851 except (IndexError, ValueError):
3846 3852 return 0
3847 3853
3848 3854 @property
3849 3855 def outdated(self):
3850 3856 return self.display_state == self.COMMENT_OUTDATED
3851 3857
3852 3858 @property
3853 3859 def outdated_js(self):
3854 3860 return str_json(self.display_state == self.COMMENT_OUTDATED)
3855 3861
3856 3862 @property
3857 3863 def immutable(self):
3858 3864 return self.immutable_state == self.OP_IMMUTABLE
3859 3865
3860 3866 def outdated_at_version(self, version: int) -> bool:
3861 3867 """
3862 3868 Checks if comment is outdated for given pull request version
3863 3869 """
3864 3870
3865 3871 def version_check():
3866 3872 return self.pull_request_version_id and self.pull_request_version_id != version
3867 3873
3868 3874 if self.is_inline:
3869 3875 return self.outdated and version_check()
3870 3876 else:
3871 3877 # general comments don't have .outdated set, also latest don't have a version
3872 3878 return version_check()
3873 3879
3874 3880 def outdated_at_version_js(self, version):
3875 3881 """
3876 3882 Checks if comment is outdated for given pull request version
3877 3883 """
3878 3884 return str_json(self.outdated_at_version(version))
3879 3885
3880 3886 def older_than_version(self, version: int) -> bool:
3881 3887 """
3882 3888 Checks if comment is made from a previous version than given.
3883 3889 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3884 3890 """
3885 3891
3886 3892 # If version is None, return False as the current version cannot be less than None
3887 3893 if version is None:
3888 3894 return False
3889 3895
3890 3896 # Ensure that the version is an integer to prevent TypeError on comparison
3891 3897 if not isinstance(version, int):
3892 3898 raise ValueError("The provided version must be an integer.")
3893 3899
3894 3900 # Initialize current version to 0 or pull_request_version_id if it's available
3895 3901 cur_ver = 0
3896 3902 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3897 3903 cur_ver = self.pull_request_version.pull_request_version_id
3898 3904
3899 3905 # Return True if the current version is less than the given version
3900 3906 return cur_ver < version
3901 3907
3902 3908 def older_than_version_js(self, version):
3903 3909 """
3904 3910 Checks if comment is made from previous version than given
3905 3911 """
3906 3912 return str_json(self.older_than_version(version))
3907 3913
3908 3914 @property
3909 3915 def commit_id(self):
3910 3916 """New style naming to stop using .revision"""
3911 3917 return self.revision
3912 3918
3913 3919 @property
3914 3920 def resolved(self):
3915 3921 return self.resolved_by[0] if self.resolved_by else None
3916 3922
3917 3923 @property
3918 3924 def is_todo(self):
3919 3925 return self.comment_type == self.COMMENT_TYPE_TODO
3920 3926
3921 3927 @property
3922 3928 def is_inline(self):
3923 3929 if self.line_no and self.f_path:
3924 3930 return True
3925 3931 return False
3926 3932
3927 3933 @property
3928 3934 def last_version(self):
3929 3935 version = 0
3930 3936 if self.history:
3931 3937 version = self.history[-1].version
3932 3938 return version
3933 3939
3934 3940 def get_index_version(self, versions):
3935 3941 return self.get_index_from_version(
3936 3942 self.pull_request_version_id, versions)
3937 3943
3938 3944 @property
3939 3945 def review_status(self):
3940 3946 if self.status_change:
3941 3947 return self.status_change[0].status
3942 3948
3943 3949 @property
3944 3950 def review_status_lbl(self):
3945 3951 if self.status_change:
3946 3952 return self.status_change[0].status_lbl
3947 3953
3948 3954 def __repr__(self):
3949 3955 if self.comment_id:
3950 3956 return f'<DB:Comment #{self.comment_id}>'
3951 3957 else:
3952 3958 return f'<DB:Comment at {id(self)!r}>'
3953 3959
3954 3960 def get_api_data(self):
3955 3961 comment = self
3956 3962
3957 3963 data = {
3958 3964 'comment_id': comment.comment_id,
3959 3965 'comment_type': comment.comment_type,
3960 3966 'comment_text': comment.text,
3961 3967 'comment_status': comment.status_change,
3962 3968 'comment_f_path': comment.f_path,
3963 3969 'comment_lineno': comment.line_no,
3964 3970 'comment_author': comment.author,
3965 3971 'comment_created_on': comment.created_on,
3966 3972 'comment_resolved_by': self.resolved,
3967 3973 'comment_commit_id': comment.revision,
3968 3974 'comment_pull_request_id': comment.pull_request_id,
3969 3975 'comment_last_version': self.last_version
3970 3976 }
3971 3977 return data
3972 3978
3973 3979 def __json__(self):
3974 3980 data = dict()
3975 3981 data.update(self.get_api_data())
3976 3982 return data
3977 3983
3978 3984
3979 3985 class ChangesetCommentHistory(Base, BaseModel):
3980 3986 __tablename__ = 'changeset_comments_history'
3981 3987 __table_args__ = (
3982 3988 Index('cch_comment_id_idx', 'comment_id'),
3983 3989 base_table_args,
3984 3990 )
3985 3991
3986 3992 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3987 3993 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3988 3994 version = Column("version", Integer(), nullable=False, default=0)
3989 3995 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3990 3996 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3991 3997 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3992 3998 deleted = Column('deleted', Boolean(), default=False)
3993 3999
3994 4000 author = relationship('User', lazy='joined')
3995 4001 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
3996 4002
3997 4003 @classmethod
3998 4004 def get_version(cls, comment_id):
3999 4005 q = Session().query(ChangesetCommentHistory).filter(
4000 4006 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4001 4007 if q.count() == 0:
4002 4008 return 1
4003 4009 elif q.count() >= q[0].version:
4004 4010 return q.count() + 1
4005 4011 else:
4006 4012 return q[0].version + 1
4007 4013
4008 4014
4009 4015 class ChangesetStatus(Base, BaseModel):
4010 4016 __tablename__ = 'changeset_statuses'
4011 4017 __table_args__ = (
4012 4018 Index('cs_revision_idx', 'revision'),
4013 4019 Index('cs_version_idx', 'version'),
4014 4020 UniqueConstraint('repo_id', 'revision', 'version'),
4015 4021 base_table_args
4016 4022 )
4017 4023
4018 4024 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4019 4025 STATUS_APPROVED = 'approved'
4020 4026 STATUS_REJECTED = 'rejected'
4021 4027 STATUS_UNDER_REVIEW = 'under_review'
4022 4028
4023 4029 STATUSES = [
4024 4030 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4025 4031 (STATUS_APPROVED, _("Approved")),
4026 4032 (STATUS_REJECTED, _("Rejected")),
4027 4033 (STATUS_UNDER_REVIEW, _("Under Review")),
4028 4034 ]
4029 4035
4030 4036 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4031 4037 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4032 4038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4033 4039 revision = Column('revision', String(40), nullable=False)
4034 4040 status = Column('status', String(128), nullable=False, default=DEFAULT)
4035 4041 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4036 4042 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4037 4043 version = Column('version', Integer(), nullable=False, default=0)
4038 4044 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4039 4045
4040 4046 author = relationship('User', lazy='select')
4041 4047 repo = relationship('Repository', lazy='select')
4042 4048 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4043 4049 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4044 4050
4045 4051 def __repr__(self):
4046 4052 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4047 4053
4048 4054 @classmethod
4049 4055 def get_status_lbl(cls, value):
4050 4056 return dict(cls.STATUSES).get(value)
4051 4057
4052 4058 @property
4053 4059 def status_lbl(self):
4054 4060 return ChangesetStatus.get_status_lbl(self.status)
4055 4061
4056 4062 def get_api_data(self):
4057 4063 status = self
4058 4064 data = {
4059 4065 'status_id': status.changeset_status_id,
4060 4066 'status': status.status,
4061 4067 }
4062 4068 return data
4063 4069
4064 4070 def __json__(self):
4065 4071 data = dict()
4066 4072 data.update(self.get_api_data())
4067 4073 return data
4068 4074
4069 4075
4070 4076 class _SetState(object):
4071 4077 """
4072 4078 Context processor allowing changing state for sensitive operation such as
4073 4079 pull request update or merge
4074 4080 """
4075 4081
4076 4082 def __init__(self, pull_request, pr_state, back_state=None):
4077 4083 self._pr = pull_request
4078 4084 self._org_state = back_state or pull_request.pull_request_state
4079 4085 self._pr_state = pr_state
4080 4086 self._current_state = None
4081 4087
4082 4088 def __enter__(self):
4083 4089 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4084 4090 self._pr, self._pr_state)
4085 4091 self.set_pr_state(self._pr_state)
4086 4092 return self
4087 4093
4088 4094 def __exit__(self, exc_type, exc_val, exc_tb):
4089 4095 if exc_val is not None or exc_type is not None:
4090 4096 log.error(traceback.format_tb(exc_tb))
4091 4097 return None
4092 4098
4093 4099 self.set_pr_state(self._org_state)
4094 4100 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4095 4101 self._pr, self._org_state)
4096 4102
4097 4103 @property
4098 4104 def state(self):
4099 4105 return self._current_state
4100 4106
4101 4107 def set_pr_state(self, pr_state):
4102 4108 try:
4103 4109 self._pr.pull_request_state = pr_state
4104 4110 Session().add(self._pr)
4105 4111 Session().commit()
4106 4112 self._current_state = pr_state
4107 4113 except Exception:
4108 4114 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4109 4115 raise
4110 4116
4111 4117
4112 4118 class _PullRequestBase(BaseModel):
4113 4119 """
4114 4120 Common attributes of pull request and version entries.
4115 4121 """
4116 4122
4117 4123 # .status values
4118 4124 STATUS_NEW = 'new'
4119 4125 STATUS_OPEN = 'open'
4120 4126 STATUS_CLOSED = 'closed'
4121 4127
4122 4128 # available states
4123 4129 STATE_CREATING = 'creating'
4124 4130 STATE_UPDATING = 'updating'
4125 4131 STATE_MERGING = 'merging'
4126 4132 STATE_CREATED = 'created'
4127 4133
4128 4134 title = Column('title', Unicode(255), nullable=True)
4129 4135 description = Column(
4130 4136 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4131 4137 nullable=True)
4132 4138 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4133 4139
4134 4140 # new/open/closed status of pull request (not approve/reject/etc)
4135 4141 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4136 4142 created_on = Column(
4137 4143 'created_on', DateTime(timezone=False), nullable=False,
4138 4144 default=datetime.datetime.now)
4139 4145 updated_on = Column(
4140 4146 'updated_on', DateTime(timezone=False), nullable=False,
4141 4147 default=datetime.datetime.now)
4142 4148
4143 4149 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4144 4150
4145 4151 @declared_attr
4146 4152 def user_id(cls):
4147 4153 return Column(
4148 4154 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4149 4155 unique=None)
4150 4156
4151 4157 # 500 revisions max
4152 4158 _revisions = Column(
4153 4159 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4154 4160
4155 4161 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4156 4162
4157 4163 @declared_attr
4158 4164 def source_repo_id(cls):
4159 4165 # TODO: dan: rename column to source_repo_id
4160 4166 return Column(
4161 4167 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4162 4168 nullable=False)
4163 4169
4164 4170 @declared_attr
4165 4171 def pr_source(cls):
4166 4172 return relationship(
4167 4173 'Repository',
4168 4174 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4169 4175 overlaps="pull_requests_source"
4170 4176 )
4171 4177
4172 4178 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4173 4179
4174 4180 @hybrid_property
4175 4181 def source_ref(self):
4176 4182 return self._source_ref
4177 4183
4178 4184 @source_ref.setter
4179 4185 def source_ref(self, val):
4180 4186 parts = (val or '').split(':')
4181 4187 if len(parts) != 3:
4182 4188 raise ValueError(
4183 4189 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4184 4190 self._source_ref = safe_str(val)
4185 4191
4186 4192 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4187 4193
4188 4194 @hybrid_property
4189 4195 def target_ref(self):
4190 4196 return self._target_ref
4191 4197
4192 4198 @target_ref.setter
4193 4199 def target_ref(self, val):
4194 4200 parts = (val or '').split(':')
4195 4201 if len(parts) != 3:
4196 4202 raise ValueError(
4197 4203 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4198 4204 self._target_ref = safe_str(val)
4199 4205
4200 4206 @declared_attr
4201 4207 def target_repo_id(cls):
4202 4208 # TODO: dan: rename column to target_repo_id
4203 4209 return Column(
4204 4210 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4205 4211 nullable=False)
4206 4212
4207 4213 @declared_attr
4208 4214 def pr_target(cls):
4209 4215 return relationship(
4210 4216 'Repository',
4211 4217 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4212 4218 overlaps="pull_requests_target"
4213 4219 )
4214 4220
4215 4221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4216 4222
4217 4223 # TODO: dan: rename column to last_merge_source_rev
4218 4224 _last_merge_source_rev = Column(
4219 4225 'last_merge_org_rev', String(40), nullable=True)
4220 4226 # TODO: dan: rename column to last_merge_target_rev
4221 4227 _last_merge_target_rev = Column(
4222 4228 'last_merge_other_rev', String(40), nullable=True)
4223 4229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4224 4230 last_merge_metadata = Column(
4225 4231 'last_merge_metadata', MutationObj.as_mutable(
4226 4232 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4227 4233
4228 4234 merge_rev = Column('merge_rev', String(40), nullable=True)
4229 4235
4230 4236 reviewer_data = Column(
4231 4237 'reviewer_data_json', MutationObj.as_mutable(
4232 4238 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4233 4239
4234 4240 @property
4235 4241 def reviewer_data_json(self):
4236 4242 return str_json(self.reviewer_data)
4237 4243
4238 4244 @property
4239 4245 def last_merge_metadata_parsed(self):
4240 4246 metadata = {}
4241 4247 if not self.last_merge_metadata:
4242 4248 return metadata
4243 4249
4244 4250 if hasattr(self.last_merge_metadata, 'de_coerce'):
4245 4251 for k, v in self.last_merge_metadata.de_coerce().items():
4246 4252 if k in ['target_ref', 'source_ref']:
4247 4253 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4248 4254 else:
4249 4255 if hasattr(v, 'de_coerce'):
4250 4256 metadata[k] = v.de_coerce()
4251 4257 else:
4252 4258 metadata[k] = v
4253 4259 return metadata
4254 4260
4255 4261 @property
4256 4262 def work_in_progress(self):
4257 4263 """checks if pull request is work in progress by checking the title"""
4258 4264 title = self.title.upper()
4259 4265 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4260 4266 return True
4261 4267 return False
4262 4268
4263 4269 @property
4264 4270 def title_safe(self):
4265 4271 return self.title\
4266 4272 .replace('{', '{{')\
4267 4273 .replace('}', '}}')
4268 4274
4269 4275 @hybrid_property
4270 4276 def description_safe(self):
4271 4277 from rhodecode.lib import helpers as h
4272 4278 return h.escape(self.description)
4273 4279
4274 4280 @hybrid_property
4275 4281 def revisions(self):
4276 4282 return self._revisions.split(':') if self._revisions else []
4277 4283
4278 4284 @revisions.setter
4279 4285 def revisions(self, val):
4280 4286 self._revisions = ':'.join(val)
4281 4287
4282 4288 @hybrid_property
4283 4289 def last_merge_status(self):
4284 4290 return safe_int(self._last_merge_status)
4285 4291
4286 4292 @last_merge_status.setter
4287 4293 def last_merge_status(self, val):
4288 4294 self._last_merge_status = val
4289 4295
4290 4296 @declared_attr
4291 4297 def author(cls):
4292 4298 return relationship(
4293 4299 'User', lazy='joined',
4294 4300 #TODO, problem that is somehow :?
4295 4301 #back_populates='user_pull_requests'
4296 4302 )
4297 4303
4298 4304 @declared_attr
4299 4305 def source_repo(cls):
4300 4306 return relationship(
4301 4307 'Repository',
4302 4308 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4303 4309 overlaps="pr_source"
4304 4310 )
4305 4311
4306 4312 @property
4307 4313 def source_ref_parts(self):
4308 4314 return self.unicode_to_reference(self.source_ref)
4309 4315
4310 4316 @declared_attr
4311 4317 def target_repo(cls):
4312 4318 return relationship(
4313 4319 'Repository',
4314 4320 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4315 4321 overlaps="pr_target"
4316 4322 )
4317 4323
4318 4324 @property
4319 4325 def target_ref_parts(self):
4320 4326 return self.unicode_to_reference(self.target_ref)
4321 4327
4322 4328 @property
4323 4329 def shadow_merge_ref(self):
4324 4330 return self.unicode_to_reference(self._shadow_merge_ref)
4325 4331
4326 4332 @shadow_merge_ref.setter
4327 4333 def shadow_merge_ref(self, ref):
4328 4334 self._shadow_merge_ref = self.reference_to_unicode(ref)
4329 4335
4330 4336 @staticmethod
4331 4337 def unicode_to_reference(raw):
4332 4338 return unicode_to_reference(raw)
4333 4339
4334 4340 @staticmethod
4335 4341 def reference_to_unicode(ref):
4336 4342 return reference_to_unicode(ref)
4337 4343
4338 4344 def get_api_data(self, with_merge_state=True):
4339 4345 from rhodecode.model.pull_request import PullRequestModel
4340 4346
4341 4347 pull_request = self
4342 4348 if with_merge_state:
4343 4349 merge_response, merge_status, msg = \
4344 4350 PullRequestModel().merge_status(pull_request)
4345 4351 merge_state = {
4346 4352 'status': merge_status,
4347 4353 'message': safe_str(msg),
4348 4354 }
4349 4355 else:
4350 4356 merge_state = {'status': 'not_available',
4351 4357 'message': 'not_available'}
4352 4358
4353 4359 merge_data = {
4354 4360 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4355 4361 'reference': (
4356 4362 pull_request.shadow_merge_ref.asdict()
4357 4363 if pull_request.shadow_merge_ref else None),
4358 4364 }
4359 4365
4360 4366 data = {
4361 4367 'pull_request_id': pull_request.pull_request_id,
4362 4368 'url': PullRequestModel().get_url(pull_request),
4363 4369 'title': pull_request.title,
4364 4370 'description': pull_request.description,
4365 4371 'status': pull_request.status,
4366 4372 'state': pull_request.pull_request_state,
4367 4373 'created_on': pull_request.created_on,
4368 4374 'updated_on': pull_request.updated_on,
4369 4375 'commit_ids': pull_request.revisions,
4370 4376 'review_status': pull_request.calculated_review_status(),
4371 4377 'mergeable': merge_state,
4372 4378 'source': {
4373 4379 'clone_url': pull_request.source_repo.clone_url(),
4374 4380 'repository': pull_request.source_repo.repo_name,
4375 4381 'reference': {
4376 4382 'name': pull_request.source_ref_parts.name,
4377 4383 'type': pull_request.source_ref_parts.type,
4378 4384 'commit_id': pull_request.source_ref_parts.commit_id,
4379 4385 },
4380 4386 },
4381 4387 'target': {
4382 4388 'clone_url': pull_request.target_repo.clone_url(),
4383 4389 'repository': pull_request.target_repo.repo_name,
4384 4390 'reference': {
4385 4391 'name': pull_request.target_ref_parts.name,
4386 4392 'type': pull_request.target_ref_parts.type,
4387 4393 'commit_id': pull_request.target_ref_parts.commit_id,
4388 4394 },
4389 4395 },
4390 4396 'merge': merge_data,
4391 4397 'author': pull_request.author.get_api_data(include_secrets=False,
4392 4398 details='basic'),
4393 4399 'reviewers': [
4394 4400 {
4395 4401 'user': reviewer.get_api_data(include_secrets=False,
4396 4402 details='basic'),
4397 4403 'reasons': reasons,
4398 4404 'review_status': st[0][1].status if st else 'not_reviewed',
4399 4405 }
4400 4406 for obj, reviewer, reasons, mandatory, st in
4401 4407 pull_request.reviewers_statuses()
4402 4408 ]
4403 4409 }
4404 4410
4405 4411 return data
4406 4412
4407 4413 def set_state(self, pull_request_state, final_state=None):
4408 4414 """
4409 4415 # goes from initial state to updating to initial state.
4410 4416 # initial state can be changed by specifying back_state=
4411 4417 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4412 4418 pull_request.merge()
4413 4419
4414 4420 :param pull_request_state:
4415 4421 :param final_state:
4416 4422
4417 4423 """
4418 4424
4419 4425 return _SetState(self, pull_request_state, back_state=final_state)
4420 4426
4421 4427
4422 4428 class PullRequest(Base, _PullRequestBase):
4423 4429 __tablename__ = 'pull_requests'
4424 4430 __table_args__ = (
4425 4431 base_table_args,
4426 4432 )
4427 4433 LATEST_VER = 'latest'
4428 4434
4429 4435 pull_request_id = Column(
4430 4436 'pull_request_id', Integer(), nullable=False, primary_key=True)
4431 4437
4432 4438 def __repr__(self):
4433 4439 if self.pull_request_id:
4434 4440 return f'<DB:PullRequest #{self.pull_request_id}>'
4435 4441 else:
4436 4442 return f'<DB:PullRequest at {id(self)!r}>'
4437 4443
4438 4444 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4439 4445 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4440 4446 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4441 4447 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4442 4448
4443 4449 @classmethod
4444 4450 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4445 4451 internal_methods=None):
4446 4452
4447 4453 class PullRequestDisplay(object):
4448 4454 """
4449 4455 Special object wrapper for showing PullRequest data via Versions
4450 4456 It mimics PR object as close as possible. This is read only object
4451 4457 just for display
4452 4458 """
4453 4459
4454 4460 def __init__(self, attrs, internal=None):
4455 4461 self.attrs = attrs
4456 4462 # internal have priority over the given ones via attrs
4457 4463 self.internal = internal or ['versions']
4458 4464
4459 4465 def __getattr__(self, item):
4460 4466 if item in self.internal:
4461 4467 return getattr(self, item)
4462 4468 try:
4463 4469 return self.attrs[item]
4464 4470 except KeyError:
4465 4471 raise AttributeError(
4466 4472 '%s object has no attribute %s' % (self, item))
4467 4473
4468 4474 def __repr__(self):
4469 4475 pr_id = self.attrs.get('pull_request_id')
4470 4476 return f'<DB:PullRequestDisplay #{pr_id}>'
4471 4477
4472 4478 def versions(self):
4473 4479 return pull_request_obj.versions.order_by(
4474 4480 PullRequestVersion.pull_request_version_id).all()
4475 4481
4476 4482 def is_closed(self):
4477 4483 return pull_request_obj.is_closed()
4478 4484
4479 4485 def is_state_changing(self):
4480 4486 return pull_request_obj.is_state_changing()
4481 4487
4482 4488 @property
4483 4489 def pull_request_version_id(self):
4484 4490 return getattr(pull_request_obj, 'pull_request_version_id', None)
4485 4491
4486 4492 @property
4487 4493 def pull_request_last_version(self):
4488 4494 return pull_request_obj.pull_request_last_version
4489 4495
4490 4496 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4491 4497
4492 4498 attrs.author = StrictAttributeDict(
4493 4499 pull_request_obj.author.get_api_data())
4494 4500 if pull_request_obj.target_repo:
4495 4501 attrs.target_repo = StrictAttributeDict(
4496 4502 pull_request_obj.target_repo.get_api_data())
4497 4503 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4498 4504
4499 4505 if pull_request_obj.source_repo:
4500 4506 attrs.source_repo = StrictAttributeDict(
4501 4507 pull_request_obj.source_repo.get_api_data())
4502 4508 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4503 4509
4504 4510 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4505 4511 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4506 4512 attrs.revisions = pull_request_obj.revisions
4507 4513 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4508 4514 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4509 4515 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4510 4516 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4511 4517
4512 4518 return PullRequestDisplay(attrs, internal=internal_methods)
4513 4519
4514 4520 def is_closed(self):
4515 4521 return self.status == self.STATUS_CLOSED
4516 4522
4517 4523 def is_state_changing(self):
4518 4524 return self.pull_request_state != PullRequest.STATE_CREATED
4519 4525
4520 4526 def __json__(self):
4521 4527 return {
4522 4528 'revisions': self.revisions,
4523 4529 'versions': self.versions_count
4524 4530 }
4525 4531
4526 4532 def calculated_review_status(self):
4527 4533 from rhodecode.model.changeset_status import ChangesetStatusModel
4528 4534 return ChangesetStatusModel().calculated_review_status(self)
4529 4535
4530 4536 def reviewers_statuses(self, user=None):
4531 4537 from rhodecode.model.changeset_status import ChangesetStatusModel
4532 4538 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4533 4539
4534 4540 def get_pull_request_reviewers(self, role=None):
4535 4541 qry = PullRequestReviewers.query()\
4536 4542 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4537 4543 if role:
4538 4544 qry = qry.filter(PullRequestReviewers.role == role)
4539 4545
4540 4546 return qry.all()
4541 4547
4542 4548 @property
4543 4549 def reviewers_count(self):
4544 4550 qry = PullRequestReviewers.query()\
4545 4551 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4546 4552 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4547 4553 return qry.count()
4548 4554
4549 4555 @property
4550 4556 def observers_count(self):
4551 4557 qry = PullRequestReviewers.query()\
4552 4558 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4553 4559 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4554 4560 return qry.count()
4555 4561
4556 4562 def observers(self):
4557 4563 qry = PullRequestReviewers.query()\
4558 4564 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4559 4565 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4560 4566 .all()
4561 4567
4562 4568 for entry in qry:
4563 4569 yield entry, entry.user
4564 4570
4565 4571 @property
4566 4572 def workspace_id(self):
4567 4573 from rhodecode.model.pull_request import PullRequestModel
4568 4574 return PullRequestModel()._workspace_id(self)
4569 4575
4570 4576 def get_shadow_repo(self):
4571 4577 workspace_id = self.workspace_id
4572 4578 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4573 4579 if os.path.isdir(shadow_repository_path):
4574 4580 vcs_obj = self.target_repo.scm_instance()
4575 4581 return vcs_obj.get_shadow_instance(shadow_repository_path)
4576 4582
4577 4583 @property
4578 4584 def versions_count(self):
4579 4585 """
4580 4586 return number of versions this PR have, e.g a PR that once been
4581 4587 updated will have 2 versions
4582 4588 """
4583 4589 return self.versions.count() + 1
4584 4590
4585 4591 @property
4586 4592 def pull_request_last_version(self):
4587 4593 return self.versions_count
4588 4594
4589 4595
4590 4596 class PullRequestVersion(Base, _PullRequestBase):
4591 4597 __tablename__ = 'pull_request_versions'
4592 4598 __table_args__ = (
4593 4599 base_table_args,
4594 4600 )
4595 4601
4596 4602 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4597 4603 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4598 4604 pull_request = relationship('PullRequest', back_populates='versions')
4599 4605
4600 4606 def __repr__(self):
4601 4607 if self.pull_request_version_id:
4602 4608 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4603 4609 else:
4604 4610 return f'<DB:PullRequestVersion at {id(self)!r}>'
4605 4611
4606 4612 @property
4607 4613 def reviewers(self):
4608 4614 return self.pull_request.reviewers
4609 4615
4610 4616 @property
4611 4617 def versions(self):
4612 4618 return self.pull_request.versions
4613 4619
4614 4620 def is_closed(self):
4615 4621 # calculate from original
4616 4622 return self.pull_request.status == self.STATUS_CLOSED
4617 4623
4618 4624 def is_state_changing(self):
4619 4625 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4620 4626
4621 4627 def calculated_review_status(self):
4622 4628 return self.pull_request.calculated_review_status()
4623 4629
4624 4630 def reviewers_statuses(self):
4625 4631 return self.pull_request.reviewers_statuses()
4626 4632
4627 4633 def observers(self):
4628 4634 return self.pull_request.observers()
4629 4635
4630 4636
4631 4637 class PullRequestReviewers(Base, BaseModel):
4632 4638 __tablename__ = 'pull_request_reviewers'
4633 4639 __table_args__ = (
4634 4640 base_table_args,
4635 4641 )
4636 4642 ROLE_REVIEWER = 'reviewer'
4637 4643 ROLE_OBSERVER = 'observer'
4638 4644 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4639 4645
4640 4646 @hybrid_property
4641 4647 def reasons(self):
4642 4648 if not self._reasons:
4643 4649 return []
4644 4650 return self._reasons
4645 4651
4646 4652 @reasons.setter
4647 4653 def reasons(self, val):
4648 4654 val = val or []
4649 4655 if any(not isinstance(x, str) for x in val):
4650 4656 raise Exception('invalid reasons type, must be list of strings')
4651 4657 self._reasons = val
4652 4658
4653 4659 pull_requests_reviewers_id = Column(
4654 4660 'pull_requests_reviewers_id', Integer(), nullable=False,
4655 4661 primary_key=True)
4656 4662 pull_request_id = Column(
4657 4663 "pull_request_id", Integer(),
4658 4664 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4659 4665 user_id = Column(
4660 4666 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4661 4667 _reasons = Column(
4662 4668 'reason', MutationList.as_mutable(
4663 4669 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4664 4670
4665 4671 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4666 4672 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4667 4673
4668 4674 user = relationship('User')
4669 4675 pull_request = relationship('PullRequest', back_populates='reviewers')
4670 4676
4671 4677 rule_data = Column(
4672 4678 'rule_data_json',
4673 4679 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4674 4680
4675 4681 def rule_user_group_data(self):
4676 4682 """
4677 4683 Returns the voting user group rule data for this reviewer
4678 4684 """
4679 4685
4680 4686 if self.rule_data and 'vote_rule' in self.rule_data:
4681 4687 user_group_data = {}
4682 4688 if 'rule_user_group_entry_id' in self.rule_data:
4683 4689 # means a group with voting rules !
4684 4690 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4685 4691 user_group_data['name'] = self.rule_data['rule_name']
4686 4692 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4687 4693
4688 4694 return user_group_data
4689 4695
4690 4696 @classmethod
4691 4697 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4692 4698 qry = PullRequestReviewers.query()\
4693 4699 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4694 4700 if role:
4695 4701 qry = qry.filter(PullRequestReviewers.role == role)
4696 4702
4697 4703 return qry.all()
4698 4704
4699 4705 def __repr__(self):
4700 4706 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4701 4707
4702 4708
4703 4709 class Notification(Base, BaseModel):
4704 4710 __tablename__ = 'notifications'
4705 4711 __table_args__ = (
4706 4712 Index('notification_type_idx', 'type'),
4707 4713 base_table_args,
4708 4714 )
4709 4715
4710 4716 TYPE_CHANGESET_COMMENT = 'cs_comment'
4711 4717 TYPE_MESSAGE = 'message'
4712 4718 TYPE_MENTION = 'mention'
4713 4719 TYPE_REGISTRATION = 'registration'
4714 4720 TYPE_PULL_REQUEST = 'pull_request'
4715 4721 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4716 4722 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4717 4723
4718 4724 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4719 4725 subject = Column('subject', Unicode(512), nullable=True)
4720 4726 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4721 4727 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4722 4728 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4723 4729 type_ = Column('type', Unicode(255))
4724 4730
4725 4731 created_by_user = relationship('User', back_populates='user_created_notifications')
4726 4732 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4727 4733
4728 4734 @property
4729 4735 def recipients(self):
4730 4736 return [x.user for x in UserNotification.query()\
4731 4737 .filter(UserNotification.notification == self)\
4732 4738 .order_by(UserNotification.user_id.asc()).all()]
4733 4739
4734 4740 @classmethod
4735 4741 def create(cls, created_by, subject, body, recipients, type_=None):
4736 4742 if type_ is None:
4737 4743 type_ = Notification.TYPE_MESSAGE
4738 4744
4739 4745 notification = cls()
4740 4746 notification.created_by_user = created_by
4741 4747 notification.subject = subject
4742 4748 notification.body = body
4743 4749 notification.type_ = type_
4744 4750 notification.created_on = datetime.datetime.now()
4745 4751
4746 4752 # For each recipient link the created notification to his account
4747 4753 for u in recipients:
4748 4754 assoc = UserNotification()
4749 4755 assoc.user_id = u.user_id
4750 4756 assoc.notification = notification
4751 4757
4752 4758 # if created_by is inside recipients mark his notification
4753 4759 # as read
4754 4760 if u.user_id == created_by.user_id:
4755 4761 assoc.read = True
4756 4762 Session().add(assoc)
4757 4763
4758 4764 Session().add(notification)
4759 4765
4760 4766 return notification
4761 4767
4762 4768
4763 4769 class UserNotification(Base, BaseModel):
4764 4770 __tablename__ = 'user_to_notification'
4765 4771 __table_args__ = (
4766 4772 UniqueConstraint('user_id', 'notification_id'),
4767 4773 base_table_args
4768 4774 )
4769 4775
4770 4776 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4771 4777 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4772 4778 read = Column('read', Boolean, default=False)
4773 4779 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4774 4780
4775 4781 user = relationship('User', lazy="joined", back_populates='notifications')
4776 4782 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4777 4783
4778 4784 def mark_as_read(self):
4779 4785 self.read = True
4780 4786 Session().add(self)
4781 4787
4782 4788
4783 4789 class UserNotice(Base, BaseModel):
4784 4790 __tablename__ = 'user_notices'
4785 4791 __table_args__ = (
4786 4792 base_table_args
4787 4793 )
4788 4794
4789 4795 NOTIFICATION_TYPE_MESSAGE = 'message'
4790 4796 NOTIFICATION_TYPE_NOTICE = 'notice'
4791 4797
4792 4798 NOTIFICATION_LEVEL_INFO = 'info'
4793 4799 NOTIFICATION_LEVEL_WARNING = 'warning'
4794 4800 NOTIFICATION_LEVEL_ERROR = 'error'
4795 4801
4796 4802 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4797 4803
4798 4804 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4799 4805 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4800 4806
4801 4807 notice_read = Column('notice_read', Boolean, default=False)
4802 4808
4803 4809 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4804 4810 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4805 4811
4806 4812 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4807 4813 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4808 4814
4809 4815 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4810 4816 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4811 4817
4812 4818 @classmethod
4813 4819 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4814 4820
4815 4821 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4816 4822 cls.NOTIFICATION_LEVEL_WARNING,
4817 4823 cls.NOTIFICATION_LEVEL_INFO]:
4818 4824 return
4819 4825
4820 4826 from rhodecode.model.user import UserModel
4821 4827 user = UserModel().get_user(user)
4822 4828
4823 4829 new_notice = UserNotice()
4824 4830 if not allow_duplicate:
4825 4831 existing_msg = UserNotice().query() \
4826 4832 .filter(UserNotice.user == user) \
4827 4833 .filter(UserNotice.notice_body == body) \
4828 4834 .filter(UserNotice.notice_read == false()) \
4829 4835 .scalar()
4830 4836 if existing_msg:
4831 4837 log.warning('Ignoring duplicate notice for user %s', user)
4832 4838 return
4833 4839
4834 4840 new_notice.user = user
4835 4841 new_notice.notice_subject = subject
4836 4842 new_notice.notice_body = body
4837 4843 new_notice.notification_level = notice_level
4838 4844 Session().add(new_notice)
4839 4845 Session().commit()
4840 4846
4841 4847
4842 4848 class Gist(Base, BaseModel):
4843 4849 __tablename__ = 'gists'
4844 4850 __table_args__ = (
4845 4851 Index('g_gist_access_id_idx', 'gist_access_id'),
4846 4852 Index('g_created_on_idx', 'created_on'),
4847 4853 base_table_args
4848 4854 )
4849 4855
4850 4856 GIST_PUBLIC = 'public'
4851 4857 GIST_PRIVATE = 'private'
4852 4858 DEFAULT_FILENAME = 'gistfile1.txt'
4853 4859
4854 4860 ACL_LEVEL_PUBLIC = 'acl_public'
4855 4861 ACL_LEVEL_PRIVATE = 'acl_private'
4856 4862
4857 4863 gist_id = Column('gist_id', Integer(), primary_key=True)
4858 4864 gist_access_id = Column('gist_access_id', Unicode(250))
4859 4865 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4860 4866 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4861 4867 gist_expires = Column('gist_expires', Float(53), nullable=False)
4862 4868 gist_type = Column('gist_type', Unicode(128), nullable=False)
4863 4869 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4864 4870 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4865 4871 acl_level = Column('acl_level', Unicode(128), nullable=True)
4866 4872
4867 4873 owner = relationship('User', back_populates='user_gists')
4868 4874
4869 4875 def __repr__(self):
4870 4876 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4871 4877
4872 4878 @hybrid_property
4873 4879 def description_safe(self):
4874 4880 from rhodecode.lib import helpers as h
4875 4881 return h.escape(self.gist_description)
4876 4882
4877 4883 @classmethod
4878 4884 def get_or_404(cls, id_):
4879 4885 from pyramid.httpexceptions import HTTPNotFound
4880 4886
4881 4887 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4882 4888 if not res:
4883 4889 log.debug('WARN: No DB entry with id %s', id_)
4884 4890 raise HTTPNotFound()
4885 4891 return res
4886 4892
4887 4893 @classmethod
4888 4894 def get_by_access_id(cls, gist_access_id):
4889 4895 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4890 4896
4891 4897 def gist_url(self):
4892 4898 from rhodecode.model.gist import GistModel
4893 4899 return GistModel().get_url(self)
4894 4900
4895 4901 @classmethod
4896 4902 def base_path(cls):
4897 4903 """
4898 4904 Returns base path when all gists are stored
4899 4905
4900 4906 :param cls:
4901 4907 """
4902 4908 from rhodecode.model.gist import GIST_STORE_LOC
4903 4909 q = Session().query(RhodeCodeUi)\
4904 4910 .filter(RhodeCodeUi.ui_key == URL_SEP)
4905 4911 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4906 4912 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4907 4913
4908 4914 def get_api_data(self):
4909 4915 """
4910 4916 Common function for generating gist related data for API
4911 4917 """
4912 4918 gist = self
4913 4919 data = {
4914 4920 'gist_id': gist.gist_id,
4915 4921 'type': gist.gist_type,
4916 4922 'access_id': gist.gist_access_id,
4917 4923 'description': gist.gist_description,
4918 4924 'url': gist.gist_url(),
4919 4925 'expires': gist.gist_expires,
4920 4926 'created_on': gist.created_on,
4921 4927 'modified_at': gist.modified_at,
4922 4928 'content': None,
4923 4929 'acl_level': gist.acl_level,
4924 4930 }
4925 4931 return data
4926 4932
4927 4933 def __json__(self):
4928 4934 data = dict(
4929 4935 )
4930 4936 data.update(self.get_api_data())
4931 4937 return data
4932 4938 # SCM functions
4933 4939
4934 4940 def scm_instance(self, **kwargs):
4935 4941 """
4936 4942 Get an instance of VCS Repository
4937 4943
4938 4944 :param kwargs:
4939 4945 """
4940 4946 from rhodecode.model.gist import GistModel
4941 4947 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4942 4948 return get_vcs_instance(
4943 4949 repo_path=safe_str(full_repo_path), create=False,
4944 4950 _vcs_alias=GistModel.vcs_backend)
4945 4951
4946 4952
4947 4953 class ExternalIdentity(Base, BaseModel):
4948 4954 __tablename__ = 'external_identities'
4949 4955 __table_args__ = (
4950 4956 Index('local_user_id_idx', 'local_user_id'),
4951 4957 Index('external_id_idx', 'external_id'),
4952 4958 base_table_args
4953 4959 )
4954 4960
4955 4961 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4956 4962 external_username = Column('external_username', Unicode(1024), default='')
4957 4963 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4958 4964 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4959 4965 access_token = Column('access_token', String(1024), default='')
4960 4966 alt_token = Column('alt_token', String(1024), default='')
4961 4967 token_secret = Column('token_secret', String(1024), default='')
4962 4968
4963 4969 @classmethod
4964 4970 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4965 4971 """
4966 4972 Returns ExternalIdentity instance based on search params
4967 4973
4968 4974 :param external_id:
4969 4975 :param provider_name:
4970 4976 :return: ExternalIdentity
4971 4977 """
4972 4978 query = cls.query()
4973 4979 query = query.filter(cls.external_id == external_id)
4974 4980 query = query.filter(cls.provider_name == provider_name)
4975 4981 if local_user_id:
4976 4982 query = query.filter(cls.local_user_id == local_user_id)
4977 4983 return query.first()
4978 4984
4979 4985 @classmethod
4980 4986 def user_by_external_id_and_provider(cls, external_id, provider_name):
4981 4987 """
4982 4988 Returns User instance based on search params
4983 4989
4984 4990 :param external_id:
4985 4991 :param provider_name:
4986 4992 :return: User
4987 4993 """
4988 4994 query = User.query()
4989 4995 query = query.filter(cls.external_id == external_id)
4990 4996 query = query.filter(cls.provider_name == provider_name)
4991 4997 query = query.filter(User.user_id == cls.local_user_id)
4992 4998 return query.first()
4993 4999
4994 5000 @classmethod
4995 5001 def by_local_user_id(cls, local_user_id):
4996 5002 """
4997 5003 Returns all tokens for user
4998 5004
4999 5005 :param local_user_id:
5000 5006 :return: ExternalIdentity
5001 5007 """
5002 5008 query = cls.query()
5003 5009 query = query.filter(cls.local_user_id == local_user_id)
5004 5010 return query
5005 5011
5006 5012 @classmethod
5007 5013 def load_provider_plugin(cls, plugin_id):
5008 5014 from rhodecode.authentication.base import loadplugin
5009 5015 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5010 5016 auth_plugin = loadplugin(_plugin_id)
5011 5017 return auth_plugin
5012 5018
5013 5019
5014 5020 class Integration(Base, BaseModel):
5015 5021 __tablename__ = 'integrations'
5016 5022 __table_args__ = (
5017 5023 base_table_args
5018 5024 )
5019 5025
5020 5026 integration_id = Column('integration_id', Integer(), primary_key=True)
5021 5027 integration_type = Column('integration_type', String(255))
5022 5028 enabled = Column('enabled', Boolean(), nullable=False)
5023 5029 name = Column('name', String(255), nullable=False)
5024 5030 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5025 5031
5026 5032 settings = Column(
5027 5033 'settings_json', MutationObj.as_mutable(
5028 5034 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5029 5035 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5030 5036 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5031 5037
5032 5038 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5033 5039 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5034 5040
5035 5041 @property
5036 5042 def scope(self):
5037 5043 if self.repo:
5038 5044 return repr(self.repo)
5039 5045 if self.repo_group:
5040 5046 if self.child_repos_only:
5041 5047 return repr(self.repo_group) + ' (child repos only)'
5042 5048 else:
5043 5049 return repr(self.repo_group) + ' (recursive)'
5044 5050 if self.child_repos_only:
5045 5051 return 'root_repos'
5046 5052 return 'global'
5047 5053
5048 5054 def __repr__(self):
5049 5055 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5050 5056
5051 5057
5052 5058 class RepoReviewRuleUser(Base, BaseModel):
5053 5059 __tablename__ = 'repo_review_rules_users'
5054 5060 __table_args__ = (
5055 5061 base_table_args
5056 5062 )
5057 5063 ROLE_REVIEWER = 'reviewer'
5058 5064 ROLE_OBSERVER = 'observer'
5059 5065 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5060 5066
5061 5067 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5062 5068 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5063 5069 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5064 5070 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5065 5071 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5066 5072 user = relationship('User', back_populates='user_review_rules')
5067 5073
5068 5074 def rule_data(self):
5069 5075 return {
5070 5076 'mandatory': self.mandatory,
5071 5077 'role': self.role,
5072 5078 }
5073 5079
5074 5080
5075 5081 class RepoReviewRuleUserGroup(Base, BaseModel):
5076 5082 __tablename__ = 'repo_review_rules_users_groups'
5077 5083 __table_args__ = (
5078 5084 base_table_args
5079 5085 )
5080 5086
5081 5087 VOTE_RULE_ALL = -1
5082 5088 ROLE_REVIEWER = 'reviewer'
5083 5089 ROLE_OBSERVER = 'observer'
5084 5090 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5085 5091
5086 5092 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5087 5093 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5088 5094 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5089 5095 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5090 5096 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5091 5097 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5092 5098 users_group = relationship('UserGroup')
5093 5099
5094 5100 def rule_data(self):
5095 5101 return {
5096 5102 'mandatory': self.mandatory,
5097 5103 'role': self.role,
5098 5104 'vote_rule': self.vote_rule
5099 5105 }
5100 5106
5101 5107 @property
5102 5108 def vote_rule_label(self):
5103 5109 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5104 5110 return 'all must vote'
5105 5111 else:
5106 5112 return 'min. vote {}'.format(self.vote_rule)
5107 5113
5108 5114
5109 5115 class RepoReviewRule(Base, BaseModel):
5110 5116 __tablename__ = 'repo_review_rules'
5111 5117 __table_args__ = (
5112 5118 base_table_args
5113 5119 )
5114 5120
5115 5121 repo_review_rule_id = Column(
5116 5122 'repo_review_rule_id', Integer(), primary_key=True)
5117 5123 repo_id = Column(
5118 5124 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5119 5125 repo = relationship('Repository', back_populates='review_rules')
5120 5126
5121 5127 review_rule_name = Column('review_rule_name', String(255))
5122 5128 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5123 5129 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5124 5130 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5125 5131
5126 5132 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5127 5133
5128 5134 # Legacy fields, just for backward compat
5129 5135 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5130 5136 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5131 5137
5132 5138 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5133 5139 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5134 5140
5135 5141 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5136 5142
5137 5143 rule_users = relationship('RepoReviewRuleUser')
5138 5144 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5139 5145
5140 5146 def _validate_pattern(self, value):
5141 5147 re.compile('^' + glob2re(value) + '$')
5142 5148
5143 5149 @hybrid_property
5144 5150 def source_branch_pattern(self):
5145 5151 return self._branch_pattern or '*'
5146 5152
5147 5153 @source_branch_pattern.setter
5148 5154 def source_branch_pattern(self, value):
5149 5155 self._validate_pattern(value)
5150 5156 self._branch_pattern = value or '*'
5151 5157
5152 5158 @hybrid_property
5153 5159 def target_branch_pattern(self):
5154 5160 return self._target_branch_pattern or '*'
5155 5161
5156 5162 @target_branch_pattern.setter
5157 5163 def target_branch_pattern(self, value):
5158 5164 self._validate_pattern(value)
5159 5165 self._target_branch_pattern = value or '*'
5160 5166
5161 5167 @hybrid_property
5162 5168 def file_pattern(self):
5163 5169 return self._file_pattern or '*'
5164 5170
5165 5171 @file_pattern.setter
5166 5172 def file_pattern(self, value):
5167 5173 self._validate_pattern(value)
5168 5174 self._file_pattern = value or '*'
5169 5175
5170 5176 @hybrid_property
5171 5177 def forbid_pr_author_to_review(self):
5172 5178 return self.pr_author == 'forbid_pr_author'
5173 5179
5174 5180 @hybrid_property
5175 5181 def include_pr_author_to_review(self):
5176 5182 return self.pr_author == 'include_pr_author'
5177 5183
5178 5184 @hybrid_property
5179 5185 def forbid_commit_author_to_review(self):
5180 5186 return self.commit_author == 'forbid_commit_author'
5181 5187
5182 5188 @hybrid_property
5183 5189 def include_commit_author_to_review(self):
5184 5190 return self.commit_author == 'include_commit_author'
5185 5191
5186 5192 def matches(self, source_branch, target_branch, files_changed):
5187 5193 """
5188 5194 Check if this review rule matches a branch/files in a pull request
5189 5195
5190 5196 :param source_branch: source branch name for the commit
5191 5197 :param target_branch: target branch name for the commit
5192 5198 :param files_changed: list of file paths changed in the pull request
5193 5199 """
5194 5200
5195 5201 source_branch = source_branch or ''
5196 5202 target_branch = target_branch or ''
5197 5203 files_changed = files_changed or []
5198 5204
5199 5205 branch_matches = True
5200 5206 if source_branch or target_branch:
5201 5207 if self.source_branch_pattern == '*':
5202 5208 source_branch_match = True
5203 5209 else:
5204 5210 if self.source_branch_pattern.startswith('re:'):
5205 5211 source_pattern = self.source_branch_pattern[3:]
5206 5212 else:
5207 5213 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5208 5214 source_branch_regex = re.compile(source_pattern)
5209 5215 source_branch_match = bool(source_branch_regex.search(source_branch))
5210 5216 if self.target_branch_pattern == '*':
5211 5217 target_branch_match = True
5212 5218 else:
5213 5219 if self.target_branch_pattern.startswith('re:'):
5214 5220 target_pattern = self.target_branch_pattern[3:]
5215 5221 else:
5216 5222 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5217 5223 target_branch_regex = re.compile(target_pattern)
5218 5224 target_branch_match = bool(target_branch_regex.search(target_branch))
5219 5225
5220 5226 branch_matches = source_branch_match and target_branch_match
5221 5227
5222 5228 files_matches = True
5223 5229 if self.file_pattern != '*':
5224 5230 files_matches = False
5225 5231 if self.file_pattern.startswith('re:'):
5226 5232 file_pattern = self.file_pattern[3:]
5227 5233 else:
5228 5234 file_pattern = glob2re(self.file_pattern)
5229 5235 file_regex = re.compile(file_pattern)
5230 5236 for file_data in files_changed:
5231 5237 filename = file_data.get('filename')
5232 5238
5233 5239 if file_regex.search(filename):
5234 5240 files_matches = True
5235 5241 break
5236 5242
5237 5243 return branch_matches and files_matches
5238 5244
5239 5245 @property
5240 5246 def review_users(self):
5241 5247 """ Returns the users which this rule applies to """
5242 5248
5243 5249 users = collections.OrderedDict()
5244 5250
5245 5251 for rule_user in self.rule_users:
5246 5252 if rule_user.user.active:
5247 5253 if rule_user.user not in users:
5248 5254 users[rule_user.user.username] = {
5249 5255 'user': rule_user.user,
5250 5256 'source': 'user',
5251 5257 'source_data': {},
5252 5258 'data': rule_user.rule_data()
5253 5259 }
5254 5260
5255 5261 for rule_user_group in self.rule_user_groups:
5256 5262 source_data = {
5257 5263 'user_group_id': rule_user_group.users_group.users_group_id,
5258 5264 'name': rule_user_group.users_group.users_group_name,
5259 5265 'members': len(rule_user_group.users_group.members)
5260 5266 }
5261 5267 for member in rule_user_group.users_group.members:
5262 5268 if member.user.active:
5263 5269 key = member.user.username
5264 5270 if key in users:
5265 5271 # skip this member as we have him already
5266 5272 # this prevents from override the "first" matched
5267 5273 # users with duplicates in multiple groups
5268 5274 continue
5269 5275
5270 5276 users[key] = {
5271 5277 'user': member.user,
5272 5278 'source': 'user_group',
5273 5279 'source_data': source_data,
5274 5280 'data': rule_user_group.rule_data()
5275 5281 }
5276 5282
5277 5283 return users
5278 5284
5279 5285 def user_group_vote_rule(self, user_id):
5280 5286
5281 5287 rules = []
5282 5288 if not self.rule_user_groups:
5283 5289 return rules
5284 5290
5285 5291 for user_group in self.rule_user_groups:
5286 5292 user_group_members = [x.user_id for x in user_group.users_group.members]
5287 5293 if user_id in user_group_members:
5288 5294 rules.append(user_group)
5289 5295 return rules
5290 5296
5291 5297 def __repr__(self):
5292 5298 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5293 5299
5294 5300
5295 5301 class ScheduleEntry(Base, BaseModel):
5296 5302 __tablename__ = 'schedule_entries'
5297 5303 __table_args__ = (
5298 5304 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5299 5305 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5300 5306 base_table_args,
5301 5307 )
5302 5308 SCHEDULE_TYPE_INTEGER = "integer"
5303 5309 SCHEDULE_TYPE_CRONTAB = "crontab"
5304 5310
5305 5311 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5306 5312 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5307 5313
5308 5314 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5309 5315 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5310 5316 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5311 5317
5312 5318 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5313 5319 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5314 5320
5315 5321 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5316 5322 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5317 5323
5318 5324 # task
5319 5325 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5320 5326 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5321 5327 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5322 5328 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5323 5329
5324 5330 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5325 5331 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5326 5332
5327 5333 @hybrid_property
5328 5334 def schedule_type(self):
5329 5335 return self._schedule_type
5330 5336
5331 5337 @schedule_type.setter
5332 5338 def schedule_type(self, val):
5333 5339 if val not in self.schedule_types:
5334 5340 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5335 5341 val, self.schedule_type))
5336 5342
5337 5343 self._schedule_type = val
5338 5344
5339 5345 @classmethod
5340 5346 def get_uid(cls, obj):
5341 5347 args = obj.task_args
5342 5348 kwargs = obj.task_kwargs
5343 5349 if isinstance(args, JsonRaw):
5344 5350 try:
5345 5351 args = json.loads(args)
5346 5352 except ValueError:
5347 5353 args = tuple()
5348 5354
5349 5355 if isinstance(kwargs, JsonRaw):
5350 5356 try:
5351 5357 kwargs = json.loads(kwargs)
5352 5358 except ValueError:
5353 5359 kwargs = dict()
5354 5360
5355 5361 dot_notation = obj.task_dot_notation
5356 5362 val = '.'.join(map(safe_str, [
5357 5363 sorted(dot_notation), args, sorted(kwargs.items())]))
5358 5364 return sha1(safe_bytes(val))
5359 5365
5360 5366 @classmethod
5361 5367 def get_by_schedule_name(cls, schedule_name):
5362 5368 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5363 5369
5364 5370 @classmethod
5365 5371 def get_by_schedule_id(cls, schedule_id):
5366 5372 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5367 5373
5368 5374 @property
5369 5375 def task(self):
5370 5376 return self.task_dot_notation
5371 5377
5372 5378 @property
5373 5379 def schedule(self):
5374 5380 from rhodecode.lib.celerylib.utils import raw_2_schedule
5375 5381 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5376 5382 return schedule
5377 5383
5378 5384 @property
5379 5385 def args(self):
5380 5386 try:
5381 5387 return list(self.task_args or [])
5382 5388 except ValueError:
5383 5389 return list()
5384 5390
5385 5391 @property
5386 5392 def kwargs(self):
5387 5393 try:
5388 5394 return dict(self.task_kwargs or {})
5389 5395 except ValueError:
5390 5396 return dict()
5391 5397
5392 5398 def _as_raw(self, val, indent=False):
5393 5399 if hasattr(val, 'de_coerce'):
5394 5400 val = val.de_coerce()
5395 5401 if val:
5396 5402 if indent:
5397 5403 val = ext_json.formatted_str_json(val)
5398 5404 else:
5399 5405 val = ext_json.str_json(val)
5400 5406
5401 5407 return val
5402 5408
5403 5409 @property
5404 5410 def schedule_definition_raw(self):
5405 5411 return self._as_raw(self.schedule_definition)
5406 5412
5407 5413 def args_raw(self, indent=False):
5408 5414 return self._as_raw(self.task_args, indent)
5409 5415
5410 5416 def kwargs_raw(self, indent=False):
5411 5417 return self._as_raw(self.task_kwargs, indent)
5412 5418
5413 5419 def __repr__(self):
5414 5420 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5415 5421
5416 5422
5417 5423 @event.listens_for(ScheduleEntry, 'before_update')
5418 5424 def update_task_uid(mapper, connection, target):
5419 5425 target.task_uid = ScheduleEntry.get_uid(target)
5420 5426
5421 5427
5422 5428 @event.listens_for(ScheduleEntry, 'before_insert')
5423 5429 def set_task_uid(mapper, connection, target):
5424 5430 target.task_uid = ScheduleEntry.get_uid(target)
5425 5431
5426 5432
5427 5433 class _BaseBranchPerms(BaseModel):
5428 5434 @classmethod
5429 5435 def compute_hash(cls, value):
5430 5436 return sha1_safe(value)
5431 5437
5432 5438 @hybrid_property
5433 5439 def branch_pattern(self):
5434 5440 return self._branch_pattern or '*'
5435 5441
5436 5442 @hybrid_property
5437 5443 def branch_hash(self):
5438 5444 return self._branch_hash
5439 5445
5440 5446 def _validate_glob(self, value):
5441 5447 re.compile('^' + glob2re(value) + '$')
5442 5448
5443 5449 @branch_pattern.setter
5444 5450 def branch_pattern(self, value):
5445 5451 self._validate_glob(value)
5446 5452 self._branch_pattern = value or '*'
5447 5453 # set the Hash when setting the branch pattern
5448 5454 self._branch_hash = self.compute_hash(self._branch_pattern)
5449 5455
5450 5456 def matches(self, branch):
5451 5457 """
5452 5458 Check if this the branch matches entry
5453 5459
5454 5460 :param branch: branch name for the commit
5455 5461 """
5456 5462
5457 5463 branch = branch or ''
5458 5464
5459 5465 branch_matches = True
5460 5466 if branch:
5461 5467 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5462 5468 branch_matches = bool(branch_regex.search(branch))
5463 5469
5464 5470 return branch_matches
5465 5471
5466 5472
5467 5473 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5468 5474 __tablename__ = 'user_to_repo_branch_permissions'
5469 5475 __table_args__ = (
5470 5476 base_table_args
5471 5477 )
5472 5478
5473 5479 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5474 5480
5475 5481 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5476 5482 repo = relationship('Repository', back_populates='user_branch_perms')
5477 5483
5478 5484 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5479 5485 permission = relationship('Permission')
5480 5486
5481 5487 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5482 5488 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5483 5489
5484 5490 rule_order = Column('rule_order', Integer(), nullable=False)
5485 5491 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5486 5492 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5487 5493
5488 5494 def __repr__(self):
5489 5495 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5490 5496
5491 5497
5492 5498 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5493 5499 __tablename__ = 'user_group_to_repo_branch_permissions'
5494 5500 __table_args__ = (
5495 5501 base_table_args
5496 5502 )
5497 5503
5498 5504 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5499 5505
5500 5506 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5501 5507 repo = relationship('Repository', back_populates='user_group_branch_perms')
5502 5508
5503 5509 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5504 5510 permission = relationship('Permission')
5505 5511
5506 5512 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5507 5513 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5508 5514
5509 5515 rule_order = Column('rule_order', Integer(), nullable=False)
5510 5516 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5511 5517 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5512 5518
5513 5519 def __repr__(self):
5514 5520 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5515 5521
5516 5522
5517 5523 class UserBookmark(Base, BaseModel):
5518 5524 __tablename__ = 'user_bookmarks'
5519 5525 __table_args__ = (
5520 5526 UniqueConstraint('user_id', 'bookmark_repo_id'),
5521 5527 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5522 5528 UniqueConstraint('user_id', 'bookmark_position'),
5523 5529 base_table_args
5524 5530 )
5525 5531
5526 5532 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5527 5533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5528 5534 position = Column("bookmark_position", Integer(), nullable=False)
5529 5535 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5530 5536 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5531 5537 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5532 5538
5533 5539 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5534 5540 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5535 5541
5536 5542 user = relationship("User")
5537 5543
5538 5544 repository = relationship("Repository")
5539 5545 repository_group = relationship("RepoGroup")
5540 5546
5541 5547 @classmethod
5542 5548 def get_by_position_for_user(cls, position, user_id):
5543 5549 return cls.query() \
5544 5550 .filter(UserBookmark.user_id == user_id) \
5545 5551 .filter(UserBookmark.position == position).scalar()
5546 5552
5547 5553 @classmethod
5548 5554 def get_bookmarks_for_user(cls, user_id, cache=True):
5549 5555 bookmarks = cls.query() \
5550 5556 .filter(UserBookmark.user_id == user_id) \
5551 5557 .options(joinedload(UserBookmark.repository)) \
5552 5558 .options(joinedload(UserBookmark.repository_group)) \
5553 5559 .order_by(UserBookmark.position.asc())
5554 5560
5555 5561 if cache:
5556 5562 bookmarks = bookmarks.options(
5557 5563 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5558 5564 )
5559 5565
5560 5566 return bookmarks.all()
5561 5567
5562 5568 def __repr__(self):
5563 5569 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5564 5570
5565 5571
5566 5572 class FileStore(Base, BaseModel):
5567 5573 __tablename__ = 'file_store'
5568 5574 __table_args__ = (
5569 5575 base_table_args
5570 5576 )
5571 5577
5572 5578 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5573 5579 file_uid = Column('file_uid', String(1024), nullable=False)
5574 5580 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5575 5581 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5576 5582 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5577 5583
5578 5584 # sha256 hash
5579 5585 file_hash = Column('file_hash', String(512), nullable=False)
5580 5586 file_size = Column('file_size', BigInteger(), nullable=False)
5581 5587
5582 5588 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5583 5589 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5584 5590 accessed_count = Column('accessed_count', Integer(), default=0)
5585 5591
5586 5592 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5587 5593
5588 5594 # if repo/repo_group reference is set, check for permissions
5589 5595 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5590 5596
5591 5597 # hidden defines an attachment that should be hidden from showing in artifact listing
5592 5598 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5593 5599
5594 5600 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5595 5601 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5596 5602
5597 5603 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5598 5604
5599 5605 # scope limited to user, which requester have access to
5600 5606 scope_user_id = Column(
5601 5607 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5602 5608 nullable=True, unique=None, default=None)
5603 5609 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5604 5610
5605 5611 # scope limited to user group, which requester have access to
5606 5612 scope_user_group_id = Column(
5607 5613 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5608 5614 nullable=True, unique=None, default=None)
5609 5615 user_group = relationship('UserGroup', lazy='joined')
5610 5616
5611 5617 # scope limited to repo, which requester have access to
5612 5618 scope_repo_id = Column(
5613 5619 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5614 5620 nullable=True, unique=None, default=None)
5615 5621 repo = relationship('Repository', lazy='joined')
5616 5622
5617 5623 # scope limited to repo group, which requester have access to
5618 5624 scope_repo_group_id = Column(
5619 5625 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5620 5626 nullable=True, unique=None, default=None)
5621 5627 repo_group = relationship('RepoGroup', lazy='joined')
5622 5628
5623 5629 @classmethod
5624 5630 def get_scope(cls, scope_type, scope_id):
5625 5631 if scope_type == 'repo':
5626 5632 return f'repo:{scope_id}'
5627 5633 elif scope_type == 'repo-group':
5628 5634 return f'repo-group:{scope_id}'
5629 5635 elif scope_type == 'user':
5630 5636 return f'user:{scope_id}'
5631 5637 elif scope_type == 'user-group':
5632 5638 return f'user-group:{scope_id}'
5633 5639 else:
5634 5640 return scope_type
5635 5641
5636 5642 @classmethod
5637 5643 def get_by_store_uid(cls, file_store_uid, safe=False):
5638 5644 if safe:
5639 5645 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5640 5646 else:
5641 5647 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5642 5648
5643 5649 @classmethod
5644 5650 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5645 5651 file_description='', enabled=True, hidden=False, check_acl=True,
5646 5652 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5647 5653
5648 5654 store_entry = FileStore()
5649 5655 store_entry.file_uid = file_uid
5650 5656 store_entry.file_display_name = file_display_name
5651 5657 store_entry.file_org_name = filename
5652 5658 store_entry.file_size = file_size
5653 5659 store_entry.file_hash = file_hash
5654 5660 store_entry.file_description = file_description
5655 5661
5656 5662 store_entry.check_acl = check_acl
5657 5663 store_entry.enabled = enabled
5658 5664 store_entry.hidden = hidden
5659 5665
5660 5666 store_entry.user_id = user_id
5661 5667 store_entry.scope_user_id = scope_user_id
5662 5668 store_entry.scope_repo_id = scope_repo_id
5663 5669 store_entry.scope_repo_group_id = scope_repo_group_id
5664 5670
5665 5671 return store_entry
5666 5672
5667 5673 @classmethod
5668 5674 def store_metadata(cls, file_store_id, args, commit=True):
5669 5675 file_store = FileStore.get(file_store_id)
5670 5676 if file_store is None:
5671 5677 return
5672 5678
5673 5679 for section, key, value, value_type in args:
5674 5680 has_key = FileStoreMetadata().query() \
5675 5681 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5676 5682 .filter(FileStoreMetadata.file_store_meta_section == section) \
5677 5683 .filter(FileStoreMetadata.file_store_meta_key == key) \
5678 5684 .scalar()
5679 5685 if has_key:
5680 5686 msg = 'key `{}` already defined under section `{}` for this file.'\
5681 5687 .format(key, section)
5682 5688 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5683 5689
5684 5690 # NOTE(marcink): raises ArtifactMetadataBadValueType
5685 5691 FileStoreMetadata.valid_value_type(value_type)
5686 5692
5687 5693 meta_entry = FileStoreMetadata()
5688 5694 meta_entry.file_store = file_store
5689 5695 meta_entry.file_store_meta_section = section
5690 5696 meta_entry.file_store_meta_key = key
5691 5697 meta_entry.file_store_meta_value_type = value_type
5692 5698 meta_entry.file_store_meta_value = value
5693 5699
5694 5700 Session().add(meta_entry)
5695 5701
5696 5702 try:
5697 5703 if commit:
5698 5704 Session().commit()
5699 5705 except IntegrityError:
5700 5706 Session().rollback()
5701 5707 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5702 5708
5703 5709 @classmethod
5704 5710 def bump_access_counter(cls, file_uid, commit=True):
5705 5711 FileStore().query()\
5706 5712 .filter(FileStore.file_uid == file_uid)\
5707 5713 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5708 5714 FileStore.accessed_on: datetime.datetime.now()})
5709 5715 if commit:
5710 5716 Session().commit()
5711 5717
5712 5718 def __json__(self):
5713 5719 data = {
5714 5720 'filename': self.file_display_name,
5715 5721 'filename_org': self.file_org_name,
5716 5722 'file_uid': self.file_uid,
5717 5723 'description': self.file_description,
5718 5724 'hidden': self.hidden,
5719 5725 'size': self.file_size,
5720 5726 'created_on': self.created_on,
5721 5727 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5722 5728 'downloaded_times': self.accessed_count,
5723 5729 'sha256': self.file_hash,
5724 5730 'metadata': self.file_metadata,
5725 5731 }
5726 5732
5727 5733 return data
5728 5734
5729 5735 def __repr__(self):
5730 5736 return f'<FileStore({self.file_store_id})>'
5731 5737
5732 5738
5733 5739 class FileStoreMetadata(Base, BaseModel):
5734 5740 __tablename__ = 'file_store_metadata'
5735 5741 __table_args__ = (
5736 5742 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5737 5743 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5738 5744 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5739 5745 base_table_args
5740 5746 )
5741 5747 SETTINGS_TYPES = {
5742 5748 'str': safe_str,
5743 5749 'int': safe_int,
5744 5750 'unicode': safe_str,
5745 5751 'bool': str2bool,
5746 5752 'list': functools.partial(aslist, sep=',')
5747 5753 }
5748 5754
5749 5755 file_store_meta_id = Column(
5750 5756 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5751 5757 primary_key=True)
5752 5758 _file_store_meta_section = Column(
5753 5759 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5754 5760 nullable=True, unique=None, default=None)
5755 5761 _file_store_meta_section_hash = Column(
5756 5762 "file_store_meta_section_hash", String(255),
5757 5763 nullable=True, unique=None, default=None)
5758 5764 _file_store_meta_key = Column(
5759 5765 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5760 5766 nullable=True, unique=None, default=None)
5761 5767 _file_store_meta_key_hash = Column(
5762 5768 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5763 5769 _file_store_meta_value = Column(
5764 5770 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5765 5771 nullable=True, unique=None, default=None)
5766 5772 _file_store_meta_value_type = Column(
5767 5773 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5768 5774 default='unicode')
5769 5775
5770 5776 file_store_id = Column(
5771 5777 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5772 5778 nullable=True, unique=None, default=None)
5773 5779
5774 5780 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5775 5781
5776 5782 @classmethod
5777 5783 def valid_value_type(cls, value):
5778 5784 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5779 5785 raise ArtifactMetadataBadValueType(
5780 5786 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5781 5787
5782 5788 @hybrid_property
5783 5789 def file_store_meta_section(self):
5784 5790 return self._file_store_meta_section
5785 5791
5786 5792 @file_store_meta_section.setter
5787 5793 def file_store_meta_section(self, value):
5788 5794 self._file_store_meta_section = value
5789 5795 self._file_store_meta_section_hash = _hash_key(value)
5790 5796
5791 5797 @hybrid_property
5792 5798 def file_store_meta_key(self):
5793 5799 return self._file_store_meta_key
5794 5800
5795 5801 @file_store_meta_key.setter
5796 5802 def file_store_meta_key(self, value):
5797 5803 self._file_store_meta_key = value
5798 5804 self._file_store_meta_key_hash = _hash_key(value)
5799 5805
5800 5806 @hybrid_property
5801 5807 def file_store_meta_value(self):
5802 5808 val = self._file_store_meta_value
5803 5809
5804 5810 if self._file_store_meta_value_type:
5805 5811 # e.g unicode.encrypted == unicode
5806 5812 _type = self._file_store_meta_value_type.split('.')[0]
5807 5813 # decode the encrypted value if it's encrypted field type
5808 5814 if '.encrypted' in self._file_store_meta_value_type:
5809 5815 cipher = EncryptedTextValue()
5810 5816 val = safe_str(cipher.process_result_value(val, None))
5811 5817 # do final type conversion
5812 5818 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5813 5819 val = converter(val)
5814 5820
5815 5821 return val
5816 5822
5817 5823 @file_store_meta_value.setter
5818 5824 def file_store_meta_value(self, val):
5819 5825 val = safe_str(val)
5820 5826 # encode the encrypted value
5821 5827 if '.encrypted' in self.file_store_meta_value_type:
5822 5828 cipher = EncryptedTextValue()
5823 5829 val = safe_str(cipher.process_bind_param(val, None))
5824 5830 self._file_store_meta_value = val
5825 5831
5826 5832 @hybrid_property
5827 5833 def file_store_meta_value_type(self):
5828 5834 return self._file_store_meta_value_type
5829 5835
5830 5836 @file_store_meta_value_type.setter
5831 5837 def file_store_meta_value_type(self, val):
5832 5838 # e.g unicode.encrypted
5833 5839 self.valid_value_type(val)
5834 5840 self._file_store_meta_value_type = val
5835 5841
5836 5842 def __json__(self):
5837 5843 data = {
5838 5844 'artifact': self.file_store.file_uid,
5839 5845 'section': self.file_store_meta_section,
5840 5846 'key': self.file_store_meta_key,
5841 5847 'value': self.file_store_meta_value,
5842 5848 }
5843 5849
5844 5850 return data
5845 5851
5846 5852 def __repr__(self):
5847 5853 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5848 5854 self.file_store_meta_key, self.file_store_meta_value)
5849 5855
5850 5856
5851 5857 class DbMigrateVersion(Base, BaseModel):
5852 5858 __tablename__ = 'db_migrate_version'
5853 5859 __table_args__ = (
5854 5860 base_table_args,
5855 5861 )
5856 5862
5857 5863 repository_id = Column('repository_id', String(250), primary_key=True)
5858 5864 repository_path = Column('repository_path', Text)
5859 5865 version = Column('version', Integer)
5860 5866
5861 5867 @classmethod
5862 5868 def set_version(cls, version):
5863 5869 """
5864 5870 Helper for forcing a different version, usually for debugging purposes via ishell.
5865 5871 """
5866 5872 ver = DbMigrateVersion.query().first()
5867 5873 ver.version = version
5868 5874 Session().commit()
5869 5875
5870 5876
5871 5877 class DbSession(Base, BaseModel):
5872 5878 __tablename__ = 'db_session'
5873 5879 __table_args__ = (
5874 5880 base_table_args,
5875 5881 )
5876 5882
5877 5883 def __repr__(self):
5878 5884 return f'<DB:DbSession({self.id})>'
5879 5885
5880 5886 id = Column('id', Integer())
5881 5887 namespace = Column('namespace', String(255), primary_key=True)
5882 5888 accessed = Column('accessed', DateTime, nullable=False)
5883 5889 created = Column('created', DateTime, nullable=False)
5884 5890 data = Column('data', PickleType, nullable=False)
@@ -1,1111 +1,1111 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Set of generic validators
21 21 """
22 22
23 23
24 24 import os
25 25 import re
26 26 import logging
27 27 import collections
28 28
29 29 import formencode
30 30 import ipaddress
31 31 from formencode.validators import (
32 32 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
33 33 NotEmpty, IPAddress, CIDR, String, FancyValidator
34 34 )
35 35
36 36 from sqlalchemy.sql.expression import true
37 37 from sqlalchemy.util import OrderedSet
38 38
39 39 from rhodecode.authentication import (
40 40 legacy_plugin_prefix, _import_legacy_plugin)
41 41 from rhodecode.authentication.base import loadplugin
42 42 from rhodecode.apps._base import ADMIN_PREFIX
43 43 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
44 44 from rhodecode.lib.utils import repo_name_slug, make_db_config
45 45 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
46 46 from rhodecode.lib.str_utils import safe_str
47 47 from rhodecode.lib.hash_utils import md5_safe
48 48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 51 from rhodecode.model.db import (
52 52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 # silence warnings and pylint
56 56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class _Missing(object):
63 63 pass
64 64
65 65
66 66 Missing = _Missing()
67 67
68 68
69 69 def M(self, key, state, **kwargs):
70 70 """
71 71 returns string from self.message based on given key,
72 72 passed kw params are used to substitute %(named)s params inside
73 73 translated strings
74 74
75 75 :param msg:
76 76 :param state:
77 77 """
78 78
79 79 #state._ = staticmethod(_)
80 80 # inject validator into state object
81 81 return self.message(key, state, **kwargs)
82 82
83 83
84 84 def UniqueList(localizer, convert=None):
85 85 _ = localizer
86 86
87 87 class _validator(formencode.FancyValidator):
88 88 """
89 89 Unique List !
90 90 """
91 91 accept_iterator = True
92 92
93 93 messages = {
94 94 'empty': _('Value cannot be an empty list'),
95 95 'missing_value': _('Value cannot be an empty list'),
96 96 }
97 97
98 98 def _convert_to_python(self, value, state):
99 99
100 100 def make_unique(_value):
101 101 seen = []
102 102 return [c for c in _value if not (c in seen or seen.append(c))]
103 103
104 104 if isinstance(value, list):
105 105 ret_val = make_unique(value)
106 106 elif isinstance(value, set):
107 107 ret_val = make_unique(list(value))
108 108 elif isinstance(value, tuple):
109 109 ret_val = make_unique(list(value))
110 110 elif value is None:
111 111 ret_val = []
112 112 else:
113 113 ret_val = [value]
114 114
115 115 if convert:
116 116 ret_val = list(map(convert, ret_val))
117 117 return ret_val
118 118
119 119 def empty_value(self, value):
120 120 return []
121 121
122 122 return _validator
123 123
124 124
125 125 def UniqueListFromString(localizer):
126 126 _ = localizer
127 127
128 128 class _validator(UniqueList(localizer)):
129 129 def _convert_to_python(self, value, state):
130 130 if isinstance(value, str):
131 131 value = aslist(value, ',')
132 132 return super()._convert_to_python(value, state)
133 133 return _validator
134 134
135 135
136 136 def ValidSvnPattern(localizer, section, repo_name=None):
137 137 _ = localizer
138 138
139 139 class _validator(formencode.validators.FancyValidator):
140 140 messages = {
141 141 'pattern_exists': _('Pattern already exists'),
142 142 }
143 143
144 144 def _validate_python(self, value, state):
145 145 if not value:
146 146 return
147 147 model = VcsSettingsModel(repo=repo_name)
148 148 ui_settings = model.get_svn_patterns(section=section)
149 149 for entry in ui_settings:
150 150 if value == entry.value:
151 151 msg = M(self, 'pattern_exists', state)
152 152 raise formencode.Invalid(msg, value, state)
153 153 return _validator
154 154
155 155
156 156 def ValidUsername(localizer, edit=False, old_data=None):
157 157 _ = localizer
158 158 old_data = old_data or {}
159 159
160 160 class _validator(formencode.validators.FancyValidator):
161 161 messages = {
162 162 'username_exists': _('Username "%(username)s" already exists'),
163 163 'system_invalid_username':
164 164 _('Username "%(username)s" is forbidden'),
165 165 'invalid_username':
166 166 _('Username may only contain alphanumeric characters '
167 167 'underscores, periods or dashes and must begin with '
168 168 'alphanumeric character or underscore')
169 169 }
170 170
171 171 def _validate_python(self, value, state):
172 172 if value in ['default', 'new_user']:
173 173 msg = M(self, 'system_invalid_username', state, username=value)
174 174 raise formencode.Invalid(msg, value, state)
175 175 # check if user is unique
176 176 old_un = None
177 177 if edit:
178 178 old_un = User.get(old_data.get('user_id')).username
179 179
180 180 if old_un != value or not edit:
181 181 if User.get_by_username(value, case_insensitive=True):
182 182 msg = M(self, 'username_exists', state, username=value)
183 183 raise formencode.Invalid(msg, value, state)
184 184
185 185 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
186 186 is None):
187 187 msg = M(self, 'invalid_username', state)
188 188 raise formencode.Invalid(msg, value, state)
189 189 return _validator
190 190
191 191
192 192 def ValidRepoUser(localizer, allow_disabled=False):
193 193 _ = localizer
194 194
195 195 class _validator(formencode.validators.FancyValidator):
196 196 messages = {
197 197 'invalid_username': _('Username %(username)s is not valid'),
198 198 'disabled_username': _('Username %(username)s is disabled')
199 199 }
200 200
201 201 def _validate_python(self, value, state):
202 202 try:
203 203 user = User.query().filter(User.username == value).one()
204 204 except Exception:
205 205 msg = M(self, 'invalid_username', state, username=value)
206 206 raise formencode.Invalid(
207 207 msg, value, state, error_dict={'username': msg}
208 208 )
209 209 if user and (not allow_disabled and not user.active):
210 210 msg = M(self, 'disabled_username', state, username=value)
211 211 raise formencode.Invalid(
212 212 msg, value, state, error_dict={'username': msg}
213 213 )
214 214 return _validator
215 215
216 216
217 217 def ValidUserGroup(localizer, edit=False, old_data=None):
218 218 _ = localizer
219 219 old_data = old_data or {}
220 220
221 221 class _validator(formencode.validators.FancyValidator):
222 222 messages = {
223 223 'invalid_group': _('Invalid user group name'),
224 224 'group_exist': _('User group `%(usergroup)s` already exists'),
225 225 'invalid_usergroup_name':
226 226 _('User group name may only contain alphanumeric '
227 227 'characters underscores, periods or dashes and must begin '
228 228 'with alphanumeric character')
229 229 }
230 230
231 231 def _validate_python(self, value, state):
232 232 if value in ['default']:
233 233 msg = M(self, 'invalid_group', state)
234 234 raise formencode.Invalid(msg, value, state)
235 235 # check if group is unique
236 236 old_ugname = None
237 237 if edit:
238 238 old_id = old_data.get('users_group_id')
239 239 old_ugname = UserGroup.get(old_id).users_group_name
240 240
241 241 if old_ugname != value or not edit:
242 242 is_existing_group = UserGroup.get_by_group_name(
243 243 value, case_insensitive=True)
244 244 if is_existing_group:
245 245 msg = M(self, 'group_exist', state, usergroup=value)
246 246 raise formencode.Invalid(
247 247 msg, value, state, error_dict={'users_group_name': msg}
248 248 )
249 249
250 250 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
251 251 msg = M(self, 'invalid_usergroup_name', state)
252 252 raise formencode.Invalid(msg, value, state)
253 253 return _validator
254 254
255 255
256 256 def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=False):
257 257 _ = localizer
258 258 old_data = old_data or {}
259 259
260 260 class _validator(formencode.validators.FancyValidator):
261 261 messages = {
262 262 'group_parent_id': _('Cannot assign this group as parent'),
263 263 'group_exists': _('Group "%(group_name)s" already exists'),
264 264 'repo_exists': _('Repository with name "%(group_name)s" '
265 265 'already exists'),
266 266 'permission_denied': _("no permission to store repository group"
267 267 "in this location"),
268 268 'permission_denied_root': _(
269 269 "no permission to store repository group "
270 270 "in root location")
271 271 }
272 272
273 273 def _convert_to_python(self, value, state):
274 274 group_name = repo_name_slug(value.get('group_name', ''))
275 275 group_parent_id = safe_int(value.get('group_parent_id'))
276 276 gr = RepoGroup.get(group_parent_id)
277 277 if gr:
278 278 parent_group_path = gr.full_path
279 279 # value needs to be aware of group name in order to check
280 280 # db key This is an actual just the name to store in the
281 281 # database
282 282 group_name_full = (
283 283 parent_group_path + RepoGroup.url_sep() + group_name)
284 284 else:
285 285 group_name_full = group_name
286 286
287 287 value['group_name'] = group_name
288 288 value['group_name_full'] = group_name_full
289 289 value['group_parent_id'] = group_parent_id
290 290 return value
291 291
292 292 def _validate_python(self, value, state):
293 293
294 294 old_group_name = None
295 295 group_name = value.get('group_name')
296 296 group_name_full = value.get('group_name_full')
297 297 group_parent_id = safe_int(value.get('group_parent_id'))
298 298 if group_parent_id == -1:
299 299 group_parent_id = None
300 300
301 301 group_obj = RepoGroup.get(old_data.get('group_id'))
302 302 parent_group_changed = False
303 303 if edit:
304 304 old_group_name = group_obj.group_name
305 305 old_group_parent_id = group_obj.group_parent_id
306 306
307 307 if group_parent_id != old_group_parent_id:
308 308 parent_group_changed = True
309 309
310 310 # TODO: mikhail: the following if statement is not reached
311 311 # since group_parent_id's OneOf validation fails before.
312 312 # Can be removed.
313 313
314 314 # check against setting a parent of self
315 315 parent_of_self = (
316 316 old_data['group_id'] == group_parent_id
317 317 if group_parent_id else False
318 318 )
319 319 if parent_of_self:
320 320 msg = M(self, 'group_parent_id', state)
321 321 raise formencode.Invalid(
322 322 msg, value, state, error_dict={'group_parent_id': msg}
323 323 )
324 324
325 325 # group we're moving current group inside
326 326 child_group = None
327 327 if group_parent_id:
328 328 child_group = RepoGroup.query().filter(
329 329 RepoGroup.group_id == group_parent_id).scalar()
330 330
331 331 # do a special check that we cannot move a group to one of
332 332 # it's children
333 333 if edit and child_group:
334 334 parents = [x.group_id for x in child_group.parents]
335 335 move_to_children = old_data['group_id'] in parents
336 336 if move_to_children:
337 337 msg = M(self, 'group_parent_id', state)
338 338 raise formencode.Invalid(
339 339 msg, value, state, error_dict={'group_parent_id': msg})
340 340
341 341 # Check if we have permission to store in the parent.
342 342 # Only check if the parent group changed.
343 343 if parent_group_changed:
344 344 if child_group is None:
345 345 if not can_create_in_root:
346 346 msg = M(self, 'permission_denied_root', state)
347 347 raise formencode.Invalid(
348 348 msg, value, state,
349 349 error_dict={'group_parent_id': msg})
350 350 else:
351 351 valid = HasRepoGroupPermissionAny('group.admin')
352 352 forbidden = not valid(
353 353 child_group.group_name, 'can create group validator')
354 354 if forbidden:
355 355 msg = M(self, 'permission_denied', state)
356 356 raise formencode.Invalid(
357 357 msg, value, state,
358 358 error_dict={'group_parent_id': msg})
359 359
360 360 # if we change the name or it's new group, check for existing names
361 361 # or repositories with the same name
362 362 if old_group_name != group_name_full or not edit:
363 363 # check group
364 364 gr = RepoGroup.get_by_group_name(group_name_full)
365 365 if gr:
366 366 msg = M(self, 'group_exists', state, group_name=group_name)
367 367 raise formencode.Invalid(
368 368 msg, value, state, error_dict={'group_name': msg})
369 369
370 370 # check for same repo
371 371 repo = Repository.get_by_repo_name(group_name_full)
372 372 if repo:
373 373 msg = M(self, 'repo_exists', state, group_name=group_name)
374 374 raise formencode.Invalid(
375 375 msg, value, state, error_dict={'group_name': msg})
376 376 return _validator
377 377
378 378
379 379 def ValidPassword(localizer):
380 380 _ = localizer
381 381
382 382 class _validator(formencode.validators.FancyValidator):
383 383 messages = {
384 384 'invalid_password':
385 385 _('Invalid characters (non-ascii) in password')
386 386 }
387 387
388 388 def _validate_python(self, value, state):
389 389 if value and not value.isascii():
390 390 msg = M(self, 'invalid_password', state)
391 391 raise formencode.Invalid(msg, value, state,)
392 392 return _validator
393 393
394 394
395 395 def ValidPasswordsMatch(
396 396 localizer, passwd='new_password',
397 397 passwd_confirmation='password_confirmation'):
398 398 _ = localizer
399 399
400 400 class _validator(formencode.validators.FancyValidator):
401 401 messages = {
402 402 'password_mismatch': _('Passwords do not match'),
403 403 }
404 404
405 405 def _validate_python(self, value, state):
406 406
407 407 pass_val = value.get('password') or value.get(passwd)
408 408 if pass_val != value[passwd_confirmation]:
409 409 msg = M(self, 'password_mismatch', state)
410 410 raise formencode.Invalid(
411 411 msg, value, state,
412 412 error_dict={passwd: msg, passwd_confirmation: msg}
413 413 )
414 414 return _validator
415 415
416 416
417 417 def ValidAuth(localizer):
418 418 _ = localizer
419 419
420 420 class _validator(formencode.validators.FancyValidator):
421 421 messages = {
422 422 'invalid_password': _('invalid password'),
423 423 'invalid_username': _('invalid user name'),
424 424 'disabled_account': _('Your account is disabled')
425 425 }
426 426
427 427 def _validate_python(self, value, state):
428 428 from rhodecode.authentication.base import authenticate, HTTP_TYPE
429 429
430 430 password = value['password']
431 431 username = value['username']
432 432
433 433 if not authenticate(username, password, '', HTTP_TYPE,
434 434 skip_missing=True):
435 user = User.get_by_username(username)
435 user = User.get_by_username_or_primary_email(username)
436 436 if user and not user.active:
437 437 log.warning('user %s is disabled', username)
438 438 msg = M(self, 'disabled_account', state)
439 439 raise formencode.Invalid(
440 440 msg, value, state, error_dict={'username': msg}
441 441 )
442 442 else:
443 443 log.warning('user `%s` failed to authenticate', username)
444 444 msg = M(self, 'invalid_username', state)
445 445 msg2 = M(self, 'invalid_password', state)
446 446 raise formencode.Invalid(
447 447 msg, value, state,
448 448 error_dict={'username': msg, 'password': msg2}
449 449 )
450 450 return _validator
451 451
452 452
453 453 def ValidRepoName(localizer, edit=False, old_data=None):
454 454 old_data = old_data or {}
455 455 _ = localizer
456 456
457 457 class _validator(formencode.validators.FancyValidator):
458 458 messages = {
459 459 'invalid_repo_name':
460 460 _('Repository name %(repo)s is disallowed'),
461 461 # top level
462 462 'repository_exists': _('Repository with name %(repo)s '
463 463 'already exists'),
464 464 'group_exists': _('Repository group with name "%(repo)s" '
465 465 'already exists'),
466 466 # inside a group
467 467 'repository_in_group_exists': _('Repository with name %(repo)s '
468 468 'exists in group "%(group)s"'),
469 469 'group_in_group_exists': _(
470 470 'Repository group with name "%(repo)s" '
471 471 'exists in group "%(group)s"'),
472 472 }
473 473
474 474 def _convert_to_python(self, value, state):
475 475 repo_name = repo_name_slug(value.get('repo_name', ''))
476 476 repo_group = value.get('repo_group')
477 477 if repo_group:
478 478 gr = RepoGroup.get(repo_group)
479 479 group_path = gr.full_path
480 480 group_name = gr.group_name
481 481 # value needs to be aware of group name in order to check
482 482 # db key This is an actual just the name to store in the
483 483 # database
484 484 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
485 485 else:
486 486 group_name = group_path = ''
487 487 repo_name_full = repo_name
488 488
489 489 value['repo_name'] = repo_name
490 490 value['repo_name_full'] = repo_name_full
491 491 value['group_path'] = group_path
492 492 value['group_name'] = group_name
493 493 return value
494 494
495 495 def _validate_python(self, value, state):
496 496
497 497 repo_name = value.get('repo_name')
498 498 repo_name_full = value.get('repo_name_full')
499 499 group_path = value.get('group_path')
500 500 group_name = value.get('group_name')
501 501
502 502 if repo_name in [ADMIN_PREFIX, '']:
503 503 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
504 504 raise formencode.Invalid(
505 505 msg, value, state, error_dict={'repo_name': msg})
506 506
507 507 rename = old_data.get('repo_name') != repo_name_full
508 508 create = not edit
509 509 if rename or create:
510 510
511 511 if group_path:
512 512 if Repository.get_by_repo_name(repo_name_full):
513 513 msg = M(self, 'repository_in_group_exists', state,
514 514 repo=repo_name, group=group_name)
515 515 raise formencode.Invalid(
516 516 msg, value, state, error_dict={'repo_name': msg})
517 517 if RepoGroup.get_by_group_name(repo_name_full):
518 518 msg = M(self, 'group_in_group_exists', state,
519 519 repo=repo_name, group=group_name)
520 520 raise formencode.Invalid(
521 521 msg, value, state, error_dict={'repo_name': msg})
522 522 else:
523 523 if RepoGroup.get_by_group_name(repo_name_full):
524 524 msg = M(self, 'group_exists', state, repo=repo_name)
525 525 raise formencode.Invalid(
526 526 msg, value, state, error_dict={'repo_name': msg})
527 527
528 528 if Repository.get_by_repo_name(repo_name_full):
529 529 msg = M(
530 530 self, 'repository_exists', state, repo=repo_name)
531 531 raise formencode.Invalid(
532 532 msg, value, state, error_dict={'repo_name': msg})
533 533 return value
534 534 return _validator
535 535
536 536
537 537 def ValidForkName(localizer, *args, **kwargs):
538 538 _ = localizer
539 539
540 540 return ValidRepoName(localizer, *args, **kwargs)
541 541
542 542
543 543 def SlugifyName(localizer):
544 544 _ = localizer
545 545
546 546 class _validator(formencode.validators.FancyValidator):
547 547
548 548 def _convert_to_python(self, value, state):
549 549 return repo_name_slug(value)
550 550
551 551 def _validate_python(self, value, state):
552 552 pass
553 553 return _validator
554 554
555 555
556 556 def CannotHaveGitSuffix(localizer):
557 557 _ = localizer
558 558
559 559 class _validator(formencode.validators.FancyValidator):
560 560 messages = {
561 561 'has_git_suffix':
562 562 _('Repository name cannot end with .git'),
563 563 }
564 564
565 565 def _convert_to_python(self, value, state):
566 566 return value
567 567
568 568 def _validate_python(self, value, state):
569 569 if value and value.endswith('.git'):
570 570 msg = M(
571 571 self, 'has_git_suffix', state)
572 572 raise formencode.Invalid(
573 573 msg, value, state, error_dict={'repo_name': msg})
574 574 return _validator
575 575
576 576
577 577 def ValidCloneUri(localizer):
578 578 _ = localizer
579 579
580 580 class InvalidCloneUrl(Exception):
581 581 allowed_prefixes = ()
582 582
583 583 def url_handler(repo_type, url):
584 584 config = make_db_config(clear_session=False)
585 585 if repo_type == 'hg':
586 586 allowed_prefixes = ('http', 'svn+http', 'git+http')
587 587
588 588 if 'http' in url[:4]:
589 589 # initially check if it's at least the proper URL
590 590 # or does it pass basic auth
591 591 MercurialRepository.check_url(url, config)
592 592 elif 'svn+http' in url[:8]: # svn->hg import
593 593 SubversionRepository.check_url(url, config)
594 594 elif 'git+http' in url[:8]: # git->hg import
595 595 raise NotImplementedError()
596 596 else:
597 597 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
598 598 'Allowed url must start with one of %s'
599 599 % (url, ','.join(allowed_prefixes)))
600 600 exc.allowed_prefixes = allowed_prefixes
601 601 raise exc
602 602
603 603 elif repo_type == 'git':
604 604 allowed_prefixes = ('http', 'svn+http', 'hg+http')
605 605 if 'http' in url[:4]:
606 606 # initially check if it's at least the proper URL
607 607 # or does it pass basic auth
608 608 GitRepository.check_url(url, config)
609 609 elif 'svn+http' in url[:8]: # svn->git import
610 610 raise NotImplementedError()
611 611 elif 'hg+http' in url[:8]: # hg->git import
612 612 raise NotImplementedError()
613 613 else:
614 614 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
615 615 'Allowed url must start with one of %s'
616 616 % (url, ','.join(allowed_prefixes)))
617 617 exc.allowed_prefixes = allowed_prefixes
618 618 raise exc
619 619
620 620 class _validator(formencode.validators.FancyValidator):
621 621 messages = {
622 622 'clone_uri': _('invalid clone url or credentials for %(rtype)s repository'),
623 623 'invalid_clone_uri': _(
624 624 'Invalid clone url, provide a valid clone '
625 625 'url starting with one of %(allowed_prefixes)s')
626 626 }
627 627
628 628 def _validate_python(self, value, state):
629 629 repo_type = value.get('repo_type')
630 630 url = value.get('clone_uri')
631 631
632 632 if url:
633 633 try:
634 634 url_handler(repo_type, url)
635 635 except InvalidCloneUrl as e:
636 636 log.warning(e)
637 637 msg = M(self, 'invalid_clone_uri', state, rtype=repo_type,
638 638 allowed_prefixes=','.join(e.allowed_prefixes))
639 639 raise formencode.Invalid(msg, value, state,
640 640 error_dict={'clone_uri': msg})
641 641 except Exception:
642 642 log.exception('Url validation failed')
643 643 msg = M(self, 'clone_uri', state, rtype=repo_type)
644 644 raise formencode.Invalid(msg, value, state,
645 645 error_dict={'clone_uri': msg})
646 646 return _validator
647 647
648 648
649 649 def ValidForkType(localizer, old_data=None):
650 650 _ = localizer
651 651 old_data = old_data or {}
652 652
653 653 class _validator(formencode.validators.FancyValidator):
654 654 messages = {
655 655 'invalid_fork_type': _('Fork have to be the same type as parent')
656 656 }
657 657
658 658 def _validate_python(self, value, state):
659 659 if old_data['repo_type'] != value:
660 660 msg = M(self, 'invalid_fork_type', state)
661 661 raise formencode.Invalid(
662 662 msg, value, state, error_dict={'repo_type': msg}
663 663 )
664 664 return _validator
665 665
666 666
667 667 def CanWriteGroup(localizer, old_data=None):
668 668 _ = localizer
669 669
670 670 class _validator(formencode.validators.FancyValidator):
671 671 messages = {
672 672 'permission_denied': _(
673 673 "You do not have the permission "
674 674 "to create repositories in this group."),
675 675 'permission_denied_root': _(
676 676 "You do not have the permission to store repositories in "
677 677 "the root location.")
678 678 }
679 679
680 680 def _convert_to_python(self, value, state):
681 681 # root location
682 682 if value in [-1, "-1"]:
683 683 return None
684 684 return value
685 685
686 686 def _validate_python(self, value, state):
687 687 gr = RepoGroup.get(value)
688 688 gr_name = gr.group_name if gr else None # None means ROOT location
689 689 # create repositories with write permission on group is set to true
690 690 create_on_write = HasPermissionAny(
691 691 'hg.create.write_on_repogroup.true')()
692 692 group_admin = HasRepoGroupPermissionAny('group.admin')(
693 693 gr_name, 'can write into group validator')
694 694 group_write = HasRepoGroupPermissionAny('group.write')(
695 695 gr_name, 'can write into group validator')
696 696 forbidden = not (group_admin or (group_write and create_on_write))
697 697 can_create_repos = HasPermissionAny(
698 698 'hg.admin', 'hg.create.repository')
699 699 gid = (old_data['repo_group'].get('group_id')
700 700 if (old_data and 'repo_group' in old_data) else None)
701 701 value_changed = gid != safe_int(value)
702 702 new = not old_data
703 703 # do check if we changed the value, there's a case that someone got
704 704 # revoked write permissions to a repository, he still created, we
705 705 # don't need to check permission if he didn't change the value of
706 706 # groups in form box
707 707 if value_changed or new:
708 708 # parent group need to be existing
709 709 if gr and forbidden:
710 710 msg = M(self, 'permission_denied', state)
711 711 raise formencode.Invalid(
712 712 msg, value, state, error_dict={'repo_type': msg}
713 713 )
714 714 # check if we can write to root location !
715 715 elif gr is None and not can_create_repos():
716 716 msg = M(self, 'permission_denied_root', state)
717 717 raise formencode.Invalid(
718 718 msg, value, state, error_dict={'repo_type': msg}
719 719 )
720 720 return _validator
721 721
722 722
723 723 def ValidPerms(localizer, type_='repo'):
724 724 _ = localizer
725 725 if type_ == 'repo_group':
726 726 EMPTY_PERM = 'group.none'
727 727 elif type_ == 'repo':
728 728 EMPTY_PERM = 'repository.none'
729 729 elif type_ == 'user_group':
730 730 EMPTY_PERM = 'usergroup.none'
731 731
732 732 class _validator(formencode.validators.FancyValidator):
733 733 messages = {
734 734 'perm_new_member_name':
735 735 _('This username or user group name is not valid')
736 736 }
737 737
738 738 def _convert_to_python(self, value, state):
739 739 perm_updates = OrderedSet()
740 740 perm_additions = OrderedSet()
741 741 perm_deletions = OrderedSet()
742 742 # build a list of permission to update/delete and new permission
743 743
744 744 # Read the perm_new_member/perm_del_member attributes and group
745 745 # them by they IDs
746 746 new_perms_group = collections.defaultdict(dict)
747 747 del_perms_group = collections.defaultdict(dict)
748 748 for k, v in list(value.copy().items()):
749 749 if k.startswith('perm_del_member'):
750 750 # delete from org storage so we don't process that later
751 751 del value[k]
752 752 # part is `id`, `type`
753 753 _type, part = k.split('perm_del_member_')
754 754 args = part.split('_')
755 755 if len(args) == 2:
756 756 _key, pos = args
757 757 del_perms_group[pos][_key] = v
758 758 if k.startswith('perm_new_member'):
759 759 # delete from org storage so we don't process that later
760 760 del value[k]
761 761 # part is `id`, `type`, `perm`
762 762 _type, part = k.split('perm_new_member_')
763 763 args = part.split('_')
764 764 if len(args) == 2:
765 765 _key, pos = args
766 766 new_perms_group[pos][_key] = v
767 767
768 768 # store the deletes
769 769 for k in sorted(del_perms_group.keys()):
770 770 perm_dict = del_perms_group[k]
771 771 del_member = perm_dict.get('id')
772 772 del_type = perm_dict.get('type')
773 773 if del_member and del_type:
774 774 perm_deletions.add(
775 775 (del_member, None, del_type))
776 776
777 777 # store additions in order of how they were added in web form
778 778 for k in sorted(new_perms_group.keys()):
779 779 perm_dict = new_perms_group[k]
780 780 new_member = perm_dict.get('id')
781 781 new_type = perm_dict.get('type')
782 782 new_perm = perm_dict.get('perm')
783 783 if new_member and new_perm and new_type:
784 784 perm_additions.add(
785 785 (new_member, new_perm, new_type))
786 786
787 787 # get updates of permissions
788 788 # (read the existing radio button states)
789 789 default_user_id = User.get_default_user_id()
790 790
791 791 for k, update_value in list(value.items()):
792 792 if k.startswith('u_perm_') or k.startswith('g_perm_'):
793 793 obj_type = k[0]
794 794 obj_id = k[7:]
795 795 update_type = {'u': 'user',
796 796 'g': 'user_group'}[obj_type]
797 797
798 798 if obj_type == 'u' and safe_int(obj_id) == default_user_id:
799 799 if str2bool(value.get('repo_private')):
800 800 # prevent from updating default user permissions
801 801 # when this repository is marked as private
802 802 update_value = EMPTY_PERM
803 803
804 804 perm_updates.add(
805 805 (obj_id, update_value, update_type))
806 806
807 807 value['perm_additions'] = [] # propagated later
808 808 value['perm_updates'] = list(perm_updates)
809 809 value['perm_deletions'] = list(perm_deletions)
810 810
811 811 updates_map = dict(
812 812 (x[0], (x[1], x[2])) for x in value['perm_updates'])
813 813 # make sure Additions don't override updates.
814 814 for member_id, perm, member_type in list(perm_additions):
815 815 if member_id in updates_map:
816 816 perm = updates_map[member_id][0]
817 817 value['perm_additions'].append((member_id, perm, member_type))
818 818
819 819 # on new entries validate users they exist and they are active !
820 820 # this leaves feedback to the form
821 821 try:
822 822 if member_type == 'user':
823 823 User.query()\
824 824 .filter(User.active == true())\
825 825 .filter(User.user_id == member_id).one()
826 826 if member_type == 'user_group':
827 827 UserGroup.query()\
828 828 .filter(UserGroup.users_group_active == true())\
829 829 .filter(UserGroup.users_group_id == member_id)\
830 830 .one()
831 831
832 832 except Exception:
833 833 log.exception('Updated permission failed: org_exc:')
834 834 msg = M(self, 'perm_new_member_type', state)
835 835 raise formencode.Invalid(
836 836 msg, value, state, error_dict={
837 837 'perm_new_member_name': msg}
838 838 )
839 839 return value
840 840 return _validator
841 841
842 842
843 843 def ValidPath(localizer):
844 844 _ = localizer
845 845
846 846 class _validator(formencode.validators.FancyValidator):
847 847 messages = {
848 848 'invalid_path': _('This is not a valid path')
849 849 }
850 850
851 851 def _validate_python(self, value, state):
852 852 if not os.path.isdir(value):
853 853 msg = M(self, 'invalid_path', state)
854 854 raise formencode.Invalid(
855 855 msg, value, state, error_dict={'paths_root_path': msg}
856 856 )
857 857 return _validator
858 858
859 859
860 860 def UniqSystemEmail(localizer, old_data=None):
861 861 _ = localizer
862 862 old_data = old_data or {}
863 863
864 864 class _validator(formencode.validators.FancyValidator):
865 865 messages = {
866 866 'email_taken': _('This e-mail address is already taken')
867 867 }
868 868
869 869 def _convert_to_python(self, value, state):
870 870 return value.lower()
871 871
872 872 def _validate_python(self, value, state):
873 873 if (old_data.get('email') or '').lower() != value:
874 874 user = User.get_by_email(value, case_insensitive=True)
875 875 if user:
876 876 msg = M(self, 'email_taken', state)
877 877 raise formencode.Invalid(
878 878 msg, value, state, error_dict={'email': msg}
879 879 )
880 880 return _validator
881 881
882 882
883 883 def ValidSystemEmail(localizer):
884 884 _ = localizer
885 885
886 886 class _validator(formencode.validators.FancyValidator):
887 887 messages = {
888 888 'non_existing_email': _('e-mail "%(email)s" does not exist.')
889 889 }
890 890
891 891 def _convert_to_python(self, value, state):
892 892 return value.lower()
893 893
894 894 def _validate_python(self, value, state):
895 895 user = User.get_by_email(value, case_insensitive=True)
896 896 if user is None:
897 897 msg = M(self, 'non_existing_email', state, email=value)
898 898 raise formencode.Invalid(
899 899 msg, value, state, error_dict={'email': msg}
900 900 )
901 901 return _validator
902 902
903 903
904 904 def NotReviewedRevisions(localizer, repo_id):
905 905 _ = localizer
906 906 class _validator(formencode.validators.FancyValidator):
907 907 messages = {
908 908 'rev_already_reviewed':
909 909 _('Revisions %(revs)s are already part of pull request '
910 910 'or have set status'),
911 911 }
912 912
913 913 def _validate_python(self, value, state):
914 914 # check revisions if they are not reviewed, or a part of another
915 915 # pull request
916 916 statuses = ChangesetStatus.query()\
917 917 .filter(ChangesetStatus.revision.in_(value))\
918 918 .filter(ChangesetStatus.repo_id == repo_id)\
919 919 .all()
920 920
921 921 errors = []
922 922 for status in statuses:
923 923 if status.pull_request_id:
924 924 errors.append(['pull_req', status.revision[:12]])
925 925 elif status.status:
926 926 errors.append(['status', status.revision[:12]])
927 927
928 928 if errors:
929 929 revs = ','.join([x[1] for x in errors])
930 930 msg = M(self, 'rev_already_reviewed', state, revs=revs)
931 931 raise formencode.Invalid(
932 932 msg, value, state, error_dict={'revisions': revs})
933 933
934 934 return _validator
935 935
936 936
937 937 def ValidIp(localizer):
938 938 _ = localizer
939 939
940 940 class _validator(CIDR):
941 941 messages = {
942 942 'badFormat': _('Please enter a valid IPv4 or IpV6 address'),
943 943 'illegalBits': _(
944 944 'The network size (bits) must be within the range '
945 945 'of 0-32 (not %(bits)r)'),
946 946 }
947 947
948 948 # we override the default to_python() call
949 949 def to_python(self, value, state):
950 950 v = super().to_python(value, state)
951 951 v = safe_str(v.strip())
952 952 net = ipaddress.ip_network(address=v, strict=False)
953 953 return str(net)
954 954
955 955 def _validate_python(self, value, state):
956 956 try:
957 957 addr = safe_str(value.strip())
958 958 # this raises an ValueError if address is not IpV4 or IpV6
959 959 ipaddress.ip_network(addr, strict=False)
960 960 except ValueError:
961 961 raise formencode.Invalid(self.message('badFormat', state),
962 962 value, state)
963 963 return _validator
964 964
965 965
966 966 def FieldKey(localizer):
967 967 _ = localizer
968 968
969 969 class _validator(formencode.validators.FancyValidator):
970 970 messages = {
971 971 'badFormat': _(
972 972 'Key name can only consist of letters, '
973 973 'underscore, dash or numbers'),
974 974 }
975 975
976 976 def _validate_python(self, value, state):
977 977 if not re.match('[a-zA-Z0-9_-]+$', value):
978 978 raise formencode.Invalid(self.message('badFormat', state),
979 979 value, state)
980 980 return _validator
981 981
982 982
983 983 def ValidAuthPlugins(localizer):
984 984 _ = localizer
985 985
986 986 class _validator(formencode.validators.FancyValidator):
987 987 messages = {
988 988 'import_duplicate': _(
989 989 'Plugins %(loaded)s and %(next_to_load)s '
990 990 'both export the same name'),
991 991 'missing_includeme': _(
992 992 'The plugin "%(plugin_id)s" is missing an includeme '
993 993 'function.'),
994 994 'import_error': _(
995 995 'Can not load plugin "%(plugin_id)s"'),
996 996 'no_plugin': _(
997 997 'No plugin available with ID "%(plugin_id)s"'),
998 998 }
999 999
1000 1000 def _convert_to_python(self, value, state):
1001 1001 # filter empty values
1002 1002 return [s for s in value if s not in [None, '']]
1003 1003
1004 1004 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1005 1005 """
1006 1006 Validates that the plugin import works. It also checks that the
1007 1007 plugin has an includeme attribute.
1008 1008 """
1009 1009 try:
1010 1010 plugin = _import_legacy_plugin(plugin_id)
1011 1011 except Exception as e:
1012 1012 log.exception(
1013 1013 'Exception during import of auth legacy plugin "{}"'
1014 1014 .format(plugin_id))
1015 1015 msg = M(self, 'import_error', state, plugin_id=plugin_id)
1016 1016 raise formencode.Invalid(msg, value, state)
1017 1017
1018 1018 if not hasattr(plugin, 'includeme'):
1019 1019 msg = M(self, 'missing_includeme', state, plugin_id=plugin_id)
1020 1020 raise formencode.Invalid(msg, value, state)
1021 1021
1022 1022 return plugin
1023 1023
1024 1024 def _validate_plugin_id(self, plugin_id, value, state):
1025 1025 """
1026 1026 Plugins are already imported during app start up. Therefore this
1027 1027 validation only retrieves the plugin from the plugin registry and
1028 1028 if it returns something not None everything is OK.
1029 1029 """
1030 1030 plugin = loadplugin(plugin_id)
1031 1031
1032 1032 if plugin is None:
1033 1033 msg = M(self, 'no_plugin', state, plugin_id=plugin_id)
1034 1034 raise formencode.Invalid(msg, value, state)
1035 1035
1036 1036 return plugin
1037 1037
1038 1038 def _validate_python(self, value, state):
1039 1039 unique_names = {}
1040 1040 for plugin_id in value:
1041 1041
1042 1042 # Validate legacy or normal plugin.
1043 1043 if plugin_id.startswith(legacy_plugin_prefix):
1044 1044 plugin = self._validate_legacy_plugin_id(
1045 1045 plugin_id, value, state)
1046 1046 else:
1047 1047 plugin = self._validate_plugin_id(plugin_id, value, state)
1048 1048
1049 1049 # Only allow unique plugin names.
1050 1050 if plugin.name in unique_names:
1051 1051 msg = M(self, 'import_duplicate', state,
1052 1052 loaded=unique_names[plugin.name],
1053 1053 next_to_load=plugin)
1054 1054 raise formencode.Invalid(msg, value, state)
1055 1055 unique_names[plugin.name] = plugin
1056 1056 return _validator
1057 1057
1058 1058
1059 1059 def ValidPattern(localizer):
1060 1060 _ = localizer
1061 1061
1062 1062 class _validator(formencode.validators.FancyValidator):
1063 1063 messages = {
1064 1064 'bad_format': _('Url must start with http or /'),
1065 1065 }
1066 1066
1067 1067 def _convert_to_python(self, value, state):
1068 1068 patterns = []
1069 1069
1070 1070 prefix = 'new_pattern'
1071 1071 for name, v in list(value.items()):
1072 1072 pattern_name = '_'.join((prefix, 'pattern'))
1073 1073 if name.startswith(pattern_name):
1074 1074 new_item_id = name[len(pattern_name)+1:]
1075 1075
1076 1076 def _field(name):
1077 1077 return '{}_{}_{}'.format(prefix, name, new_item_id)
1078 1078
1079 1079 values = {
1080 1080 'issuetracker_pat': value.get(_field('pattern')),
1081 1081 'issuetracker_url': value.get(_field('url')),
1082 1082 'issuetracker_pref': value.get(_field('prefix')),
1083 1083 'issuetracker_desc': value.get(_field('description'))
1084 1084 }
1085 1085 new_uid = md5_safe(values['issuetracker_pat'])
1086 1086
1087 1087 has_required_fields = (
1088 1088 values['issuetracker_pat']
1089 1089 and values['issuetracker_url'])
1090 1090
1091 1091 if has_required_fields:
1092 1092 # validate url that it starts with http or /
1093 1093 # otherwise it can lead to JS injections
1094 1094 # e.g specifig javascript:<malicios code>
1095 1095 if not values['issuetracker_url'].startswith(('http', '/')):
1096 1096 raise formencode.Invalid(
1097 1097 self.message('bad_format', state),
1098 1098 value, state)
1099 1099
1100 1100 settings = [
1101 1101 ('_'.join((key, new_uid)), values[key], 'unicode')
1102 1102 for key in values]
1103 1103 patterns.append(settings)
1104 1104
1105 1105 value['patterns'] = patterns
1106 1106 delete_patterns = value.get('uid') or []
1107 1107 if not isinstance(delete_patterns, (list, tuple)):
1108 1108 delete_patterns = [delete_patterns]
1109 1109 value['delete_patterns'] = delete_patterns
1110 1110 return value
1111 1111 return _validator
@@ -1,105 +1,105 b''
1 1 <%inherit file="base/root.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('Sign In')}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9 <style>body{background-color:#eeeeee;}</style>
10 10
11 11 <div class="loginbox">
12 12 <div class="header-account">
13 13 <div id="header-inner" class="title">
14 14 <div id="logo">
15 15 <div class="logo-wrapper">
16 16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 17 </div>
18 18 % if c.rhodecode_name:
19 19 <div class="branding">
20 20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 21 </div>
22 22 % endif
23 23 </div>
24 24 </div>
25 25 </div>
26 26
27 27 <div class="loginwrapper">
28 28 <rhodecode-toast id="notifications"></rhodecode-toast>
29 29
30 30 <div class="auth-image-wrapper">
31 31 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 32 </div>
33 33
34 34 <div id="login">
35 35 <%block name="above_login_button" />
36 36 <!-- login -->
37 37 <div class="sign-in-title">
38 <h1>${_('Sign In using username/password')}</h1>
38 <h1>${_('Sign In using credentials')}</h1>
39 39 </div>
40 40 <div class="inner form">
41 41 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
42 42
43 <label for="username">${_('Username')}:</label>
43 <label for="username">${_('Username or email address')}:</label>
44 44 ${h.text('username', class_='focus', value=defaults.get('username'))}
45 45 %if 'username' in errors:
46 46 <span class="error-message">${errors.get('username')}</span>
47 47 <br />
48 48 %endif
49 49
50 50 <label for="password">${_('Password')}:
51 51 %if h.HasPermissionAny('hg.password_reset.enabled')():
52 52 <div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
53 53 %endif
54 54
55 55 </label>
56 56 ${h.password('password', class_='focus')}
57 57 %if 'password' in errors:
58 58 <span class="error-message">${errors.get('password')}</span>
59 59 <br />
60 60 %endif
61 61
62 62 ${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
63 63 <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %>
64 64 % if timeout == '0':
65 65 <% remember_label = _('Remember my indefinitely') %>
66 66 % else:
67 67 <% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
68 68 % endif
69 69 <label class="checkbox" for="remember">${remember_label}</label>
70 70
71 71 <p class="links">
72 72 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
73 73 ${h.link_to(_("Create a new account."), request.route_path('register'), class_='new_account')}
74 74 %endif
75 75 </p>
76 76
77 77 %if not h.HasPermissionAny('hg.password_reset.enabled')():
78 78 ## password reset hidden or disabled.
79 79 <p class="help-block">
80 80 ${_('Password reset is disabled.')} <br/>
81 81 ${_('Please contact ')}
82 82 % if c.visual.rhodecode_support_url:
83 83 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
84 84 ${_('or')}
85 85 % endif
86 86 ${_('an administrator if you need help.')}
87 87 </p>
88 88 %endif
89 89
90 90 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
91 91
92 92 ${h.end_form()}
93 93 <script type="text/javascript">
94 94 $(document).ready(function(){
95 95 $('#username').focus();
96 96 })
97 97 </script>
98 98
99 99 </div>
100 100 <!-- end login -->
101 101
102 102 <%block name="below_login_button" />
103 103 </div>
104 104 </div>
105 105 </div>
General Comments 0
You need to be logged in to leave comments. Login now