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