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