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