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