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