##// END OF EJS Templates
auth-tokens: don't generate builtin token for new users....
marcink -
r1478:8313bda1 default
parent child Browse files
Show More
@@ -1,3917 +1,3915 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict, cleaned_uri)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 354 HOOK_PUSH = 'changegroup.push_logger'
355 355
356 356 # TODO: johbo: Unify way how hooks are configured for git and hg,
357 357 # git part is currently hardcoded.
358 358
359 359 # SVN PATTERNS
360 360 SVN_BRANCH_ID = 'vcs_svn_branch'
361 361 SVN_TAG_ID = 'vcs_svn_tag'
362 362
363 363 ui_id = Column(
364 364 "ui_id", Integer(), nullable=False, unique=True, default=None,
365 365 primary_key=True)
366 366 ui_section = Column(
367 367 "ui_section", String(255), nullable=True, unique=None, default=None)
368 368 ui_key = Column(
369 369 "ui_key", String(255), nullable=True, unique=None, default=None)
370 370 ui_value = Column(
371 371 "ui_value", String(255), nullable=True, unique=None, default=None)
372 372 ui_active = Column(
373 373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
374 374
375 375 def __repr__(self):
376 376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
377 377 self.ui_key, self.ui_value)
378 378
379 379
380 380 class RepoRhodeCodeSetting(Base, BaseModel):
381 381 __tablename__ = 'repo_rhodecode_settings'
382 382 __table_args__ = (
383 383 UniqueConstraint(
384 384 'app_settings_name', 'repository_id',
385 385 name='uq_repo_rhodecode_setting_name_repo_id'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 repository_id = Column(
391 391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
392 392 nullable=False)
393 393 app_settings_id = Column(
394 394 "app_settings_id", Integer(), nullable=False, unique=True,
395 395 default=None, primary_key=True)
396 396 app_settings_name = Column(
397 397 "app_settings_name", String(255), nullable=True, unique=None,
398 398 default=None)
399 399 _app_settings_value = Column(
400 400 "app_settings_value", String(4096), nullable=True, unique=None,
401 401 default=None)
402 402 _app_settings_type = Column(
403 403 "app_settings_type", String(255), nullable=True, unique=None,
404 404 default=None)
405 405
406 406 repository = relationship('Repository')
407 407
408 408 def __init__(self, repository_id, key='', val='', type='unicode'):
409 409 self.repository_id = repository_id
410 410 self.app_settings_name = key
411 411 self.app_settings_type = type
412 412 self.app_settings_value = val
413 413
414 414 @validates('_app_settings_value')
415 415 def validate_settings_value(self, key, val):
416 416 assert type(val) == unicode
417 417 return val
418 418
419 419 @hybrid_property
420 420 def app_settings_value(self):
421 421 v = self._app_settings_value
422 422 type_ = self.app_settings_type
423 423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
424 424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
425 425 return converter(v)
426 426
427 427 @app_settings_value.setter
428 428 def app_settings_value(self, val):
429 429 """
430 430 Setter that will always make sure we use unicode in app_settings_value
431 431
432 432 :param val:
433 433 """
434 434 self._app_settings_value = safe_unicode(val)
435 435
436 436 @hybrid_property
437 437 def app_settings_type(self):
438 438 return self._app_settings_type
439 439
440 440 @app_settings_type.setter
441 441 def app_settings_type(self, val):
442 442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
443 443 if val not in SETTINGS_TYPES:
444 444 raise Exception('type must be one of %s got %s'
445 445 % (SETTINGS_TYPES.keys(), val))
446 446 self._app_settings_type = val
447 447
448 448 def __unicode__(self):
449 449 return u"<%s('%s:%s:%s[%s]')>" % (
450 450 self.__class__.__name__, self.repository.repo_name,
451 451 self.app_settings_name, self.app_settings_value,
452 452 self.app_settings_type
453 453 )
454 454
455 455
456 456 class RepoRhodeCodeUi(Base, BaseModel):
457 457 __tablename__ = 'repo_rhodecode_ui'
458 458 __table_args__ = (
459 459 UniqueConstraint(
460 460 'repository_id', 'ui_section', 'ui_key',
461 461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
462 462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
464 464 )
465 465
466 466 repository_id = Column(
467 467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
468 468 nullable=False)
469 469 ui_id = Column(
470 470 "ui_id", Integer(), nullable=False, unique=True, default=None,
471 471 primary_key=True)
472 472 ui_section = Column(
473 473 "ui_section", String(255), nullable=True, unique=None, default=None)
474 474 ui_key = Column(
475 475 "ui_key", String(255), nullable=True, unique=None, default=None)
476 476 ui_value = Column(
477 477 "ui_value", String(255), nullable=True, unique=None, default=None)
478 478 ui_active = Column(
479 479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
480 480
481 481 repository = relationship('Repository')
482 482
483 483 def __repr__(self):
484 484 return '<%s[%s:%s]%s=>%s]>' % (
485 485 self.__class__.__name__, self.repository.repo_name,
486 486 self.ui_section, self.ui_key, self.ui_value)
487 487
488 488
489 489 class User(Base, BaseModel):
490 490 __tablename__ = 'users'
491 491 __table_args__ = (
492 492 UniqueConstraint('username'), UniqueConstraint('email'),
493 493 Index('u_username_idx', 'username'),
494 494 Index('u_email_idx', 'email'),
495 495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 497 )
498 498 DEFAULT_USER = 'default'
499 499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
500 500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
501 501
502 502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
503 503 username = Column("username", String(255), nullable=True, unique=None, default=None)
504 504 password = Column("password", String(255), nullable=True, unique=None, default=None)
505 505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
506 506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
507 507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
508 508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
509 509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
510 510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
511 511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
512 512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
513 513 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
514 514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
515 515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
516 516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
517 517
518 518 user_log = relationship('UserLog')
519 519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
520 520
521 521 repositories = relationship('Repository')
522 522 repository_groups = relationship('RepoGroup')
523 523 user_groups = relationship('UserGroup')
524 524
525 525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
526 526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
527 527
528 528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
529 529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
530 530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
531 531
532 532 group_member = relationship('UserGroupMember', cascade='all')
533 533
534 534 notifications = relationship('UserNotification', cascade='all')
535 535 # notifications assigned to this user
536 536 user_created_notifications = relationship('Notification', cascade='all')
537 537 # comments created by this user
538 538 user_comments = relationship('ChangesetComment', cascade='all')
539 539 # user profile extra info
540 540 user_emails = relationship('UserEmailMap', cascade='all')
541 541 user_ip_map = relationship('UserIpMap', cascade='all')
542 542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
543 543 # gists
544 544 user_gists = relationship('Gist', cascade='all')
545 545 # user pull requests
546 546 user_pull_requests = relationship('PullRequest', cascade='all')
547 547 # external identities
548 548 extenal_identities = relationship(
549 549 'ExternalIdentity',
550 550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
551 551 cascade='all')
552 552
553 553 def __unicode__(self):
554 554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
555 555 self.user_id, self.username)
556 556
557 557 @hybrid_property
558 558 def email(self):
559 559 return self._email
560 560
561 561 @email.setter
562 562 def email(self, val):
563 563 self._email = val.lower() if val else None
564 564
565 565 @property
566 566 def firstname(self):
567 567 # alias for future
568 568 return self.name
569 569
570 570 @property
571 571 def emails(self):
572 572 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
573 573 return [self.email] + [x.email for x in other]
574 574
575 575 @property
576 576 def auth_tokens(self):
577 577 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
578 578
579 579 @property
580 580 def extra_auth_tokens(self):
581 581 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
582 582
583 583 @property
584 584 def feed_token(self):
585 585 return self.get_feed_token()
586 586
587 587 def get_feed_token(self):
588 588 feed_tokens = UserApiKeys.query()\
589 589 .filter(UserApiKeys.user == self)\
590 590 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 591 .all()
592 592 if feed_tokens:
593 593 return feed_tokens[0].api_key
594 594 return 'NO_FEED_TOKEN_AVAILABLE'
595 595
596 596 @classmethod
597 597 def extra_valid_auth_tokens(cls, user, role=None):
598 598 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
599 599 .filter(or_(UserApiKeys.expires == -1,
600 600 UserApiKeys.expires >= time.time()))
601 601 if role:
602 602 tokens = tokens.filter(or_(UserApiKeys.role == role,
603 603 UserApiKeys.role == UserApiKeys.ROLE_ALL))
604 604 return tokens.all()
605 605
606 606 def authenticate_by_token(self, auth_token, roles=None):
607 607 from rhodecode.lib import auth
608 608
609 609 log.debug('Trying to authenticate user: %s via auth-token, '
610 610 'and roles: %s', self, roles)
611 611
612 612 if not auth_token:
613 613 return False
614 614
615 615 crypto_backend = auth.crypto_backend()
616 616
617 617 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
618 618 tokens_q = UserApiKeys.query()\
619 619 .filter(UserApiKeys.user_id == self.user_id)\
620 620 .filter(or_(UserApiKeys.expires == -1,
621 621 UserApiKeys.expires >= time.time()))
622 622
623 623 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
624 624
625 625 plain_tokens = []
626 626 hash_tokens = []
627 627
628 628 for token in tokens_q.all():
629 629 if token.api_key.startswith(crypto_backend.ENC_PREF):
630 630 hash_tokens.append(token.api_key)
631 631 else:
632 632 plain_tokens.append(token.api_key)
633 633
634 634 is_plain_match = auth_token in plain_tokens
635 635 if is_plain_match:
636 636 return True
637 637
638 638 for hashed in hash_tokens:
639 639 # marcink: this is expensive to calculate, but the most secure
640 640 match = crypto_backend.hash_check(auth_token, hashed)
641 641 if match:
642 642 return True
643 643
644 644 return False
645 645
646 646 @property
647 647 def builtin_token_roles(self):
648 648 roles = [
649 649 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
650 650 ]
651 651 return map(UserApiKeys._get_role_name, roles)
652 652
653 653 @property
654 654 def ip_addresses(self):
655 655 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
656 656 return [x.ip_addr for x in ret]
657 657
658 658 @property
659 659 def username_and_name(self):
660 660 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
661 661
662 662 @property
663 663 def username_or_name_or_email(self):
664 664 full_name = self.full_name if self.full_name is not ' ' else None
665 665 return self.username or full_name or self.email
666 666
667 667 @property
668 668 def full_name(self):
669 669 return '%s %s' % (self.firstname, self.lastname)
670 670
671 671 @property
672 672 def full_name_or_username(self):
673 673 return ('%s %s' % (self.firstname, self.lastname)
674 674 if (self.firstname and self.lastname) else self.username)
675 675
676 676 @property
677 677 def full_contact(self):
678 678 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
679 679
680 680 @property
681 681 def short_contact(self):
682 682 return '%s %s' % (self.firstname, self.lastname)
683 683
684 684 @property
685 685 def is_admin(self):
686 686 return self.admin
687 687
688 688 @property
689 689 def AuthUser(self):
690 690 """
691 691 Returns instance of AuthUser for this user
692 692 """
693 693 from rhodecode.lib.auth import AuthUser
694 694 return AuthUser(user_id=self.user_id, api_key=self.api_key,
695 695 username=self.username)
696 696
697 697 @hybrid_property
698 698 def user_data(self):
699 699 if not self._user_data:
700 700 return {}
701 701
702 702 try:
703 703 return json.loads(self._user_data)
704 704 except TypeError:
705 705 return {}
706 706
707 707 @user_data.setter
708 708 def user_data(self, val):
709 709 if not isinstance(val, dict):
710 710 raise Exception('user_data must be dict, got %s' % type(val))
711 711 try:
712 712 self._user_data = json.dumps(val)
713 713 except Exception:
714 714 log.error(traceback.format_exc())
715 715
716 716 @classmethod
717 717 def get_by_username(cls, username, case_insensitive=False,
718 718 cache=False, identity_cache=False):
719 719 session = Session()
720 720
721 721 if case_insensitive:
722 722 q = cls.query().filter(
723 723 func.lower(cls.username) == func.lower(username))
724 724 else:
725 725 q = cls.query().filter(cls.username == username)
726 726
727 727 if cache:
728 728 if identity_cache:
729 729 val = cls.identity_cache(session, 'username', username)
730 730 if val:
731 731 return val
732 732 else:
733 733 q = q.options(
734 734 FromCache("sql_cache_short",
735 735 "get_user_by_name_%s" % _hash_key(username)))
736 736
737 737 return q.scalar()
738 738
739 739 @classmethod
740 740 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
741 741 q = cls.query().filter(cls.api_key == auth_token)
742 742
743 743 if cache:
744 744 q = q.options(FromCache("sql_cache_short",
745 745 "get_auth_token_%s" % auth_token))
746 746 res = q.scalar()
747 747
748 748 if fallback and not res:
749 749 #fallback to additional keys
750 750 _res = UserApiKeys.query()\
751 751 .filter(UserApiKeys.api_key == auth_token)\
752 752 .filter(or_(UserApiKeys.expires == -1,
753 753 UserApiKeys.expires >= time.time()))\
754 754 .first()
755 755 if _res:
756 756 res = _res.user
757 757 return res
758 758
759 759 @classmethod
760 760 def get_by_email(cls, email, case_insensitive=False, cache=False):
761 761
762 762 if case_insensitive:
763 763 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
764 764
765 765 else:
766 766 q = cls.query().filter(cls.email == email)
767 767
768 768 if cache:
769 769 q = q.options(FromCache("sql_cache_short",
770 770 "get_email_key_%s" % _hash_key(email)))
771 771
772 772 ret = q.scalar()
773 773 if ret is None:
774 774 q = UserEmailMap.query()
775 775 # try fetching in alternate email map
776 776 if case_insensitive:
777 777 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
778 778 else:
779 779 q = q.filter(UserEmailMap.email == email)
780 780 q = q.options(joinedload(UserEmailMap.user))
781 781 if cache:
782 782 q = q.options(FromCache("sql_cache_short",
783 783 "get_email_map_key_%s" % email))
784 784 ret = getattr(q.scalar(), 'user', None)
785 785
786 786 return ret
787 787
788 788 @classmethod
789 789 def get_from_cs_author(cls, author):
790 790 """
791 791 Tries to get User objects out of commit author string
792 792
793 793 :param author:
794 794 """
795 795 from rhodecode.lib.helpers import email, author_name
796 796 # Valid email in the attribute passed, see if they're in the system
797 797 _email = email(author)
798 798 if _email:
799 799 user = cls.get_by_email(_email, case_insensitive=True)
800 800 if user:
801 801 return user
802 802 # Maybe we can match by username?
803 803 _author = author_name(author)
804 804 user = cls.get_by_username(_author, case_insensitive=True)
805 805 if user:
806 806 return user
807 807
808 808 def update_userdata(self, **kwargs):
809 809 usr = self
810 810 old = usr.user_data
811 811 old.update(**kwargs)
812 812 usr.user_data = old
813 813 Session().add(usr)
814 814 log.debug('updated userdata with ', kwargs)
815 815
816 816 def update_lastlogin(self):
817 817 """Update user lastlogin"""
818 818 self.last_login = datetime.datetime.now()
819 819 Session().add(self)
820 820 log.debug('updated user %s lastlogin', self.username)
821 821
822 822 def update_lastactivity(self):
823 823 """Update user lastactivity"""
824 824 usr = self
825 825 old = usr.user_data
826 826 old.update({'last_activity': time.time()})
827 827 usr.user_data = old
828 828 Session().add(usr)
829 829 log.debug('updated user %s lastactivity', usr.username)
830 830
831 def update_password(self, new_password, change_api_key=False):
832 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
831 def update_password(self, new_password):
832 from rhodecode.lib.auth import get_crypt_password
833 833
834 834 self.password = get_crypt_password(new_password)
835 if change_api_key:
836 self.api_key = generate_auth_token(self.username)
837 835 Session().add(self)
838 836
839 837 @classmethod
840 838 def get_first_super_admin(cls):
841 839 user = User.query().filter(User.admin == true()).first()
842 840 if user is None:
843 841 raise Exception('FATAL: Missing administrative account!')
844 842 return user
845 843
846 844 @classmethod
847 845 def get_all_super_admins(cls):
848 846 """
849 847 Returns all admin accounts sorted by username
850 848 """
851 849 return User.query().filter(User.admin == true())\
852 850 .order_by(User.username.asc()).all()
853 851
854 852 @classmethod
855 853 def get_default_user(cls, cache=False):
856 854 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
857 855 if user is None:
858 856 raise Exception('FATAL: Missing default account!')
859 857 return user
860 858
861 859 def _get_default_perms(self, user, suffix=''):
862 860 from rhodecode.model.permission import PermissionModel
863 861 return PermissionModel().get_default_perms(user.user_perms, suffix)
864 862
865 863 def get_default_perms(self, suffix=''):
866 864 return self._get_default_perms(self, suffix)
867 865
868 866 def get_api_data(self, include_secrets=False, details='full'):
869 867 """
870 868 Common function for generating user related data for API
871 869
872 870 :param include_secrets: By default secrets in the API data will be replaced
873 871 by a placeholder value to prevent exposing this data by accident. In case
874 872 this data shall be exposed, set this flag to ``True``.
875 873
876 874 :param details: details can be 'basic|full' basic gives only a subset of
877 875 the available user information that includes user_id, name and emails.
878 876 """
879 877 user = self
880 878 user_data = self.user_data
881 879 data = {
882 880 'user_id': user.user_id,
883 881 'username': user.username,
884 882 'firstname': user.name,
885 883 'lastname': user.lastname,
886 884 'email': user.email,
887 885 'emails': user.emails,
888 886 }
889 887 if details == 'basic':
890 888 return data
891 889
892 890 api_key_length = 40
893 891 api_key_replacement = '*' * api_key_length
894 892
895 893 extras = {
896 894 'api_key': api_key_replacement,
897 895 'api_keys': [api_key_replacement],
898 896 'active': user.active,
899 897 'admin': user.admin,
900 898 'extern_type': user.extern_type,
901 899 'extern_name': user.extern_name,
902 900 'last_login': user.last_login,
903 901 'ip_addresses': user.ip_addresses,
904 902 'language': user_data.get('language')
905 903 }
906 904 data.update(extras)
907 905
908 906 if include_secrets:
909 907 data['api_key'] = user.api_key
910 908 data['api_keys'] = user.auth_tokens
911 909 return data
912 910
913 911 def __json__(self):
914 912 data = {
915 913 'full_name': self.full_name,
916 914 'full_name_or_username': self.full_name_or_username,
917 915 'short_contact': self.short_contact,
918 916 'full_contact': self.full_contact,
919 917 }
920 918 data.update(self.get_api_data())
921 919 return data
922 920
923 921
924 922 class UserApiKeys(Base, BaseModel):
925 923 __tablename__ = 'user_api_keys'
926 924 __table_args__ = (
927 925 Index('uak_api_key_idx', 'api_key'),
928 926 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
929 927 UniqueConstraint('api_key'),
930 928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
931 929 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
932 930 )
933 931 __mapper_args__ = {}
934 932
935 933 # ApiKey role
936 934 ROLE_ALL = 'token_role_all'
937 935 ROLE_HTTP = 'token_role_http'
938 936 ROLE_VCS = 'token_role_vcs'
939 937 ROLE_API = 'token_role_api'
940 938 ROLE_FEED = 'token_role_feed'
941 939 ROLE_PASSWORD_RESET = 'token_password_reset'
942 940
943 941 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
944 942
945 943 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
946 944 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
947 945 api_key = Column("api_key", String(255), nullable=False, unique=True)
948 946 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
949 947 expires = Column('expires', Float(53), nullable=False)
950 948 role = Column('role', String(255), nullable=True)
951 949 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
952 950
953 951 # scope columns
954 952 repo_id = Column(
955 953 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
956 954 nullable=True, unique=None, default=None)
957 955 repo = relationship('Repository', lazy='joined')
958 956
959 957 repo_group_id = Column(
960 958 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
961 959 nullable=True, unique=None, default=None)
962 960 repo_group = relationship('RepoGroup', lazy='joined')
963 961
964 962 user = relationship('User', lazy='joined')
965 963
966 964 @classmethod
967 965 def _get_role_name(cls, role):
968 966 return {
969 967 cls.ROLE_ALL: _('all'),
970 968 cls.ROLE_HTTP: _('http/web interface'),
971 969 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
972 970 cls.ROLE_API: _('api calls'),
973 971 cls.ROLE_FEED: _('feed access'),
974 972 }.get(role, role)
975 973
976 974 @property
977 975 def expired(self):
978 976 if self.expires == -1:
979 977 return False
980 978 return time.time() > self.expires
981 979
982 980 @property
983 981 def role_humanized(self):
984 982 return self._get_role_name(self.role)
985 983
986 984
987 985 class UserEmailMap(Base, BaseModel):
988 986 __tablename__ = 'user_email_map'
989 987 __table_args__ = (
990 988 Index('uem_email_idx', 'email'),
991 989 UniqueConstraint('email'),
992 990 {'extend_existing': True, 'mysql_engine': 'InnoDB',
993 991 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
994 992 )
995 993 __mapper_args__ = {}
996 994
997 995 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 996 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 997 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1000 998 user = relationship('User', lazy='joined')
1001 999
1002 1000 @validates('_email')
1003 1001 def validate_email(self, key, email):
1004 1002 # check if this email is not main one
1005 1003 main_email = Session().query(User).filter(User.email == email).scalar()
1006 1004 if main_email is not None:
1007 1005 raise AttributeError('email %s is present is user table' % email)
1008 1006 return email
1009 1007
1010 1008 @hybrid_property
1011 1009 def email(self):
1012 1010 return self._email
1013 1011
1014 1012 @email.setter
1015 1013 def email(self, val):
1016 1014 self._email = val.lower() if val else None
1017 1015
1018 1016
1019 1017 class UserIpMap(Base, BaseModel):
1020 1018 __tablename__ = 'user_ip_map'
1021 1019 __table_args__ = (
1022 1020 UniqueConstraint('user_id', 'ip_addr'),
1023 1021 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1024 1022 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1025 1023 )
1026 1024 __mapper_args__ = {}
1027 1025
1028 1026 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 1027 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1030 1028 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1031 1029 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1032 1030 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1033 1031 user = relationship('User', lazy='joined')
1034 1032
1035 1033 @classmethod
1036 1034 def _get_ip_range(cls, ip_addr):
1037 1035 net = ipaddress.ip_network(ip_addr, strict=False)
1038 1036 return [str(net.network_address), str(net.broadcast_address)]
1039 1037
1040 1038 def __json__(self):
1041 1039 return {
1042 1040 'ip_addr': self.ip_addr,
1043 1041 'ip_range': self._get_ip_range(self.ip_addr),
1044 1042 }
1045 1043
1046 1044 def __unicode__(self):
1047 1045 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1048 1046 self.user_id, self.ip_addr)
1049 1047
1050 1048 class UserLog(Base, BaseModel):
1051 1049 __tablename__ = 'user_logs'
1052 1050 __table_args__ = (
1053 1051 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1054 1052 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1055 1053 )
1056 1054 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1057 1055 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1058 1056 username = Column("username", String(255), nullable=True, unique=None, default=None)
1059 1057 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1060 1058 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1061 1059 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1062 1060 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1063 1061 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1064 1062
1065 1063 def __unicode__(self):
1066 1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1067 1065 self.repository_name,
1068 1066 self.action)
1069 1067
1070 1068 @property
1071 1069 def action_as_day(self):
1072 1070 return datetime.date(*self.action_date.timetuple()[:3])
1073 1071
1074 1072 user = relationship('User')
1075 1073 repository = relationship('Repository', cascade='')
1076 1074
1077 1075
1078 1076 class UserGroup(Base, BaseModel):
1079 1077 __tablename__ = 'users_groups'
1080 1078 __table_args__ = (
1081 1079 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1082 1080 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1083 1081 )
1084 1082
1085 1083 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1086 1084 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1087 1085 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1088 1086 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1089 1087 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1090 1088 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1091 1089 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1092 1090 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1093 1091
1094 1092 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1095 1093 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1096 1094 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1097 1095 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1098 1096 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1099 1097 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1100 1098
1101 1099 user = relationship('User')
1102 1100
1103 1101 @hybrid_property
1104 1102 def group_data(self):
1105 1103 if not self._group_data:
1106 1104 return {}
1107 1105
1108 1106 try:
1109 1107 return json.loads(self._group_data)
1110 1108 except TypeError:
1111 1109 return {}
1112 1110
1113 1111 @group_data.setter
1114 1112 def group_data(self, val):
1115 1113 try:
1116 1114 self._group_data = json.dumps(val)
1117 1115 except Exception:
1118 1116 log.error(traceback.format_exc())
1119 1117
1120 1118 def __unicode__(self):
1121 1119 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1122 1120 self.users_group_id,
1123 1121 self.users_group_name)
1124 1122
1125 1123 @classmethod
1126 1124 def get_by_group_name(cls, group_name, cache=False,
1127 1125 case_insensitive=False):
1128 1126 if case_insensitive:
1129 1127 q = cls.query().filter(func.lower(cls.users_group_name) ==
1130 1128 func.lower(group_name))
1131 1129
1132 1130 else:
1133 1131 q = cls.query().filter(cls.users_group_name == group_name)
1134 1132 if cache:
1135 1133 q = q.options(FromCache(
1136 1134 "sql_cache_short",
1137 1135 "get_group_%s" % _hash_key(group_name)))
1138 1136 return q.scalar()
1139 1137
1140 1138 @classmethod
1141 1139 def get(cls, user_group_id, cache=False):
1142 1140 user_group = cls.query()
1143 1141 if cache:
1144 1142 user_group = user_group.options(FromCache("sql_cache_short",
1145 1143 "get_users_group_%s" % user_group_id))
1146 1144 return user_group.get(user_group_id)
1147 1145
1148 1146 def permissions(self, with_admins=True, with_owner=True):
1149 1147 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1150 1148 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1151 1149 joinedload(UserUserGroupToPerm.user),
1152 1150 joinedload(UserUserGroupToPerm.permission),)
1153 1151
1154 1152 # get owners and admins and permissions. We do a trick of re-writing
1155 1153 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1156 1154 # has a global reference and changing one object propagates to all
1157 1155 # others. This means if admin is also an owner admin_row that change
1158 1156 # would propagate to both objects
1159 1157 perm_rows = []
1160 1158 for _usr in q.all():
1161 1159 usr = AttributeDict(_usr.user.get_dict())
1162 1160 usr.permission = _usr.permission.permission_name
1163 1161 perm_rows.append(usr)
1164 1162
1165 1163 # filter the perm rows by 'default' first and then sort them by
1166 1164 # admin,write,read,none permissions sorted again alphabetically in
1167 1165 # each group
1168 1166 perm_rows = sorted(perm_rows, key=display_sort)
1169 1167
1170 1168 _admin_perm = 'usergroup.admin'
1171 1169 owner_row = []
1172 1170 if with_owner:
1173 1171 usr = AttributeDict(self.user.get_dict())
1174 1172 usr.owner_row = True
1175 1173 usr.permission = _admin_perm
1176 1174 owner_row.append(usr)
1177 1175
1178 1176 super_admin_rows = []
1179 1177 if with_admins:
1180 1178 for usr in User.get_all_super_admins():
1181 1179 # if this admin is also owner, don't double the record
1182 1180 if usr.user_id == owner_row[0].user_id:
1183 1181 owner_row[0].admin_row = True
1184 1182 else:
1185 1183 usr = AttributeDict(usr.get_dict())
1186 1184 usr.admin_row = True
1187 1185 usr.permission = _admin_perm
1188 1186 super_admin_rows.append(usr)
1189 1187
1190 1188 return super_admin_rows + owner_row + perm_rows
1191 1189
1192 1190 def permission_user_groups(self):
1193 1191 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1194 1192 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1195 1193 joinedload(UserGroupUserGroupToPerm.target_user_group),
1196 1194 joinedload(UserGroupUserGroupToPerm.permission),)
1197 1195
1198 1196 perm_rows = []
1199 1197 for _user_group in q.all():
1200 1198 usr = AttributeDict(_user_group.user_group.get_dict())
1201 1199 usr.permission = _user_group.permission.permission_name
1202 1200 perm_rows.append(usr)
1203 1201
1204 1202 return perm_rows
1205 1203
1206 1204 def _get_default_perms(self, user_group, suffix=''):
1207 1205 from rhodecode.model.permission import PermissionModel
1208 1206 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1209 1207
1210 1208 def get_default_perms(self, suffix=''):
1211 1209 return self._get_default_perms(self, suffix)
1212 1210
1213 1211 def get_api_data(self, with_group_members=True, include_secrets=False):
1214 1212 """
1215 1213 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1216 1214 basically forwarded.
1217 1215
1218 1216 """
1219 1217 user_group = self
1220 1218
1221 1219 data = {
1222 1220 'users_group_id': user_group.users_group_id,
1223 1221 'group_name': user_group.users_group_name,
1224 1222 'group_description': user_group.user_group_description,
1225 1223 'active': user_group.users_group_active,
1226 1224 'owner': user_group.user.username,
1227 1225 }
1228 1226 if with_group_members:
1229 1227 users = []
1230 1228 for user in user_group.members:
1231 1229 user = user.user
1232 1230 users.append(user.get_api_data(include_secrets=include_secrets))
1233 1231 data['users'] = users
1234 1232
1235 1233 return data
1236 1234
1237 1235
1238 1236 class UserGroupMember(Base, BaseModel):
1239 1237 __tablename__ = 'users_groups_members'
1240 1238 __table_args__ = (
1241 1239 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1242 1240 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1243 1241 )
1244 1242
1245 1243 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1246 1244 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1247 1245 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1248 1246
1249 1247 user = relationship('User', lazy='joined')
1250 1248 users_group = relationship('UserGroup')
1251 1249
1252 1250 def __init__(self, gr_id='', u_id=''):
1253 1251 self.users_group_id = gr_id
1254 1252 self.user_id = u_id
1255 1253
1256 1254
1257 1255 class RepositoryField(Base, BaseModel):
1258 1256 __tablename__ = 'repositories_fields'
1259 1257 __table_args__ = (
1260 1258 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1261 1259 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1262 1260 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1263 1261 )
1264 1262 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1265 1263
1266 1264 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1267 1265 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1268 1266 field_key = Column("field_key", String(250))
1269 1267 field_label = Column("field_label", String(1024), nullable=False)
1270 1268 field_value = Column("field_value", String(10000), nullable=False)
1271 1269 field_desc = Column("field_desc", String(1024), nullable=False)
1272 1270 field_type = Column("field_type", String(255), nullable=False, unique=None)
1273 1271 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1274 1272
1275 1273 repository = relationship('Repository')
1276 1274
1277 1275 @property
1278 1276 def field_key_prefixed(self):
1279 1277 return 'ex_%s' % self.field_key
1280 1278
1281 1279 @classmethod
1282 1280 def un_prefix_key(cls, key):
1283 1281 if key.startswith(cls.PREFIX):
1284 1282 return key[len(cls.PREFIX):]
1285 1283 return key
1286 1284
1287 1285 @classmethod
1288 1286 def get_by_key_name(cls, key, repo):
1289 1287 row = cls.query()\
1290 1288 .filter(cls.repository == repo)\
1291 1289 .filter(cls.field_key == key).scalar()
1292 1290 return row
1293 1291
1294 1292
1295 1293 class Repository(Base, BaseModel):
1296 1294 __tablename__ = 'repositories'
1297 1295 __table_args__ = (
1298 1296 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1299 1297 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1300 1298 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1301 1299 )
1302 1300 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1303 1301 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1304 1302
1305 1303 STATE_CREATED = 'repo_state_created'
1306 1304 STATE_PENDING = 'repo_state_pending'
1307 1305 STATE_ERROR = 'repo_state_error'
1308 1306
1309 1307 LOCK_AUTOMATIC = 'lock_auto'
1310 1308 LOCK_API = 'lock_api'
1311 1309 LOCK_WEB = 'lock_web'
1312 1310 LOCK_PULL = 'lock_pull'
1313 1311
1314 1312 NAME_SEP = URL_SEP
1315 1313
1316 1314 repo_id = Column(
1317 1315 "repo_id", Integer(), nullable=False, unique=True, default=None,
1318 1316 primary_key=True)
1319 1317 _repo_name = Column(
1320 1318 "repo_name", Text(), nullable=False, default=None)
1321 1319 _repo_name_hash = Column(
1322 1320 "repo_name_hash", String(255), nullable=False, unique=True)
1323 1321 repo_state = Column("repo_state", String(255), nullable=True)
1324 1322
1325 1323 clone_uri = Column(
1326 1324 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1327 1325 default=None)
1328 1326 repo_type = Column(
1329 1327 "repo_type", String(255), nullable=False, unique=False, default=None)
1330 1328 user_id = Column(
1331 1329 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1332 1330 unique=False, default=None)
1333 1331 private = Column(
1334 1332 "private", Boolean(), nullable=True, unique=None, default=None)
1335 1333 enable_statistics = Column(
1336 1334 "statistics", Boolean(), nullable=True, unique=None, default=True)
1337 1335 enable_downloads = Column(
1338 1336 "downloads", Boolean(), nullable=True, unique=None, default=True)
1339 1337 description = Column(
1340 1338 "description", String(10000), nullable=True, unique=None, default=None)
1341 1339 created_on = Column(
1342 1340 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1343 1341 default=datetime.datetime.now)
1344 1342 updated_on = Column(
1345 1343 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1346 1344 default=datetime.datetime.now)
1347 1345 _landing_revision = Column(
1348 1346 "landing_revision", String(255), nullable=False, unique=False,
1349 1347 default=None)
1350 1348 enable_locking = Column(
1351 1349 "enable_locking", Boolean(), nullable=False, unique=None,
1352 1350 default=False)
1353 1351 _locked = Column(
1354 1352 "locked", String(255), nullable=True, unique=False, default=None)
1355 1353 _changeset_cache = Column(
1356 1354 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1357 1355
1358 1356 fork_id = Column(
1359 1357 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1360 1358 nullable=True, unique=False, default=None)
1361 1359 group_id = Column(
1362 1360 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1363 1361 unique=False, default=None)
1364 1362
1365 1363 user = relationship('User', lazy='joined')
1366 1364 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1367 1365 group = relationship('RepoGroup', lazy='joined')
1368 1366 repo_to_perm = relationship(
1369 1367 'UserRepoToPerm', cascade='all',
1370 1368 order_by='UserRepoToPerm.repo_to_perm_id')
1371 1369 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1372 1370 stats = relationship('Statistics', cascade='all', uselist=False)
1373 1371
1374 1372 followers = relationship(
1375 1373 'UserFollowing',
1376 1374 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1377 1375 cascade='all')
1378 1376 extra_fields = relationship(
1379 1377 'RepositoryField', cascade="all, delete, delete-orphan")
1380 1378 logs = relationship('UserLog')
1381 1379 comments = relationship(
1382 1380 'ChangesetComment', cascade="all, delete, delete-orphan")
1383 1381 pull_requests_source = relationship(
1384 1382 'PullRequest',
1385 1383 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1386 1384 cascade="all, delete, delete-orphan")
1387 1385 pull_requests_target = relationship(
1388 1386 'PullRequest',
1389 1387 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1390 1388 cascade="all, delete, delete-orphan")
1391 1389 ui = relationship('RepoRhodeCodeUi', cascade="all")
1392 1390 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1393 1391 integrations = relationship('Integration',
1394 1392 cascade="all, delete, delete-orphan")
1395 1393
1396 1394 def __unicode__(self):
1397 1395 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1398 1396 safe_unicode(self.repo_name))
1399 1397
1400 1398 @hybrid_property
1401 1399 def landing_rev(self):
1402 1400 # always should return [rev_type, rev]
1403 1401 if self._landing_revision:
1404 1402 _rev_info = self._landing_revision.split(':')
1405 1403 if len(_rev_info) < 2:
1406 1404 _rev_info.insert(0, 'rev')
1407 1405 return [_rev_info[0], _rev_info[1]]
1408 1406 return [None, None]
1409 1407
1410 1408 @landing_rev.setter
1411 1409 def landing_rev(self, val):
1412 1410 if ':' not in val:
1413 1411 raise ValueError('value must be delimited with `:` and consist '
1414 1412 'of <rev_type>:<rev>, got %s instead' % val)
1415 1413 self._landing_revision = val
1416 1414
1417 1415 @hybrid_property
1418 1416 def locked(self):
1419 1417 if self._locked:
1420 1418 user_id, timelocked, reason = self._locked.split(':')
1421 1419 lock_values = int(user_id), timelocked, reason
1422 1420 else:
1423 1421 lock_values = [None, None, None]
1424 1422 return lock_values
1425 1423
1426 1424 @locked.setter
1427 1425 def locked(self, val):
1428 1426 if val and isinstance(val, (list, tuple)):
1429 1427 self._locked = ':'.join(map(str, val))
1430 1428 else:
1431 1429 self._locked = None
1432 1430
1433 1431 @hybrid_property
1434 1432 def changeset_cache(self):
1435 1433 from rhodecode.lib.vcs.backends.base import EmptyCommit
1436 1434 dummy = EmptyCommit().__json__()
1437 1435 if not self._changeset_cache:
1438 1436 return dummy
1439 1437 try:
1440 1438 return json.loads(self._changeset_cache)
1441 1439 except TypeError:
1442 1440 return dummy
1443 1441 except Exception:
1444 1442 log.error(traceback.format_exc())
1445 1443 return dummy
1446 1444
1447 1445 @changeset_cache.setter
1448 1446 def changeset_cache(self, val):
1449 1447 try:
1450 1448 self._changeset_cache = json.dumps(val)
1451 1449 except Exception:
1452 1450 log.error(traceback.format_exc())
1453 1451
1454 1452 @hybrid_property
1455 1453 def repo_name(self):
1456 1454 return self._repo_name
1457 1455
1458 1456 @repo_name.setter
1459 1457 def repo_name(self, value):
1460 1458 self._repo_name = value
1461 1459 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1462 1460
1463 1461 @classmethod
1464 1462 def normalize_repo_name(cls, repo_name):
1465 1463 """
1466 1464 Normalizes os specific repo_name to the format internally stored inside
1467 1465 database using URL_SEP
1468 1466
1469 1467 :param cls:
1470 1468 :param repo_name:
1471 1469 """
1472 1470 return cls.NAME_SEP.join(repo_name.split(os.sep))
1473 1471
1474 1472 @classmethod
1475 1473 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1476 1474 session = Session()
1477 1475 q = session.query(cls).filter(cls.repo_name == repo_name)
1478 1476
1479 1477 if cache:
1480 1478 if identity_cache:
1481 1479 val = cls.identity_cache(session, 'repo_name', repo_name)
1482 1480 if val:
1483 1481 return val
1484 1482 else:
1485 1483 q = q.options(
1486 1484 FromCache("sql_cache_short",
1487 1485 "get_repo_by_name_%s" % _hash_key(repo_name)))
1488 1486
1489 1487 return q.scalar()
1490 1488
1491 1489 @classmethod
1492 1490 def get_by_full_path(cls, repo_full_path):
1493 1491 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1494 1492 repo_name = cls.normalize_repo_name(repo_name)
1495 1493 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1496 1494
1497 1495 @classmethod
1498 1496 def get_repo_forks(cls, repo_id):
1499 1497 return cls.query().filter(Repository.fork_id == repo_id)
1500 1498
1501 1499 @classmethod
1502 1500 def base_path(cls):
1503 1501 """
1504 1502 Returns base path when all repos are stored
1505 1503
1506 1504 :param cls:
1507 1505 """
1508 1506 q = Session().query(RhodeCodeUi)\
1509 1507 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1510 1508 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1511 1509 return q.one().ui_value
1512 1510
1513 1511 @classmethod
1514 1512 def is_valid(cls, repo_name):
1515 1513 """
1516 1514 returns True if given repo name is a valid filesystem repository
1517 1515
1518 1516 :param cls:
1519 1517 :param repo_name:
1520 1518 """
1521 1519 from rhodecode.lib.utils import is_valid_repo
1522 1520
1523 1521 return is_valid_repo(repo_name, cls.base_path())
1524 1522
1525 1523 @classmethod
1526 1524 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1527 1525 case_insensitive=True):
1528 1526 q = Repository.query()
1529 1527
1530 1528 if not isinstance(user_id, Optional):
1531 1529 q = q.filter(Repository.user_id == user_id)
1532 1530
1533 1531 if not isinstance(group_id, Optional):
1534 1532 q = q.filter(Repository.group_id == group_id)
1535 1533
1536 1534 if case_insensitive:
1537 1535 q = q.order_by(func.lower(Repository.repo_name))
1538 1536 else:
1539 1537 q = q.order_by(Repository.repo_name)
1540 1538 return q.all()
1541 1539
1542 1540 @property
1543 1541 def forks(self):
1544 1542 """
1545 1543 Return forks of this repo
1546 1544 """
1547 1545 return Repository.get_repo_forks(self.repo_id)
1548 1546
1549 1547 @property
1550 1548 def parent(self):
1551 1549 """
1552 1550 Returns fork parent
1553 1551 """
1554 1552 return self.fork
1555 1553
1556 1554 @property
1557 1555 def just_name(self):
1558 1556 return self.repo_name.split(self.NAME_SEP)[-1]
1559 1557
1560 1558 @property
1561 1559 def groups_with_parents(self):
1562 1560 groups = []
1563 1561 if self.group is None:
1564 1562 return groups
1565 1563
1566 1564 cur_gr = self.group
1567 1565 groups.insert(0, cur_gr)
1568 1566 while 1:
1569 1567 gr = getattr(cur_gr, 'parent_group', None)
1570 1568 cur_gr = cur_gr.parent_group
1571 1569 if gr is None:
1572 1570 break
1573 1571 groups.insert(0, gr)
1574 1572
1575 1573 return groups
1576 1574
1577 1575 @property
1578 1576 def groups_and_repo(self):
1579 1577 return self.groups_with_parents, self
1580 1578
1581 1579 @LazyProperty
1582 1580 def repo_path(self):
1583 1581 """
1584 1582 Returns base full path for that repository means where it actually
1585 1583 exists on a filesystem
1586 1584 """
1587 1585 q = Session().query(RhodeCodeUi).filter(
1588 1586 RhodeCodeUi.ui_key == self.NAME_SEP)
1589 1587 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1590 1588 return q.one().ui_value
1591 1589
1592 1590 @property
1593 1591 def repo_full_path(self):
1594 1592 p = [self.repo_path]
1595 1593 # we need to split the name by / since this is how we store the
1596 1594 # names in the database, but that eventually needs to be converted
1597 1595 # into a valid system path
1598 1596 p += self.repo_name.split(self.NAME_SEP)
1599 1597 return os.path.join(*map(safe_unicode, p))
1600 1598
1601 1599 @property
1602 1600 def cache_keys(self):
1603 1601 """
1604 1602 Returns associated cache keys for that repo
1605 1603 """
1606 1604 return CacheKey.query()\
1607 1605 .filter(CacheKey.cache_args == self.repo_name)\
1608 1606 .order_by(CacheKey.cache_key)\
1609 1607 .all()
1610 1608
1611 1609 def get_new_name(self, repo_name):
1612 1610 """
1613 1611 returns new full repository name based on assigned group and new new
1614 1612
1615 1613 :param group_name:
1616 1614 """
1617 1615 path_prefix = self.group.full_path_splitted if self.group else []
1618 1616 return self.NAME_SEP.join(path_prefix + [repo_name])
1619 1617
1620 1618 @property
1621 1619 def _config(self):
1622 1620 """
1623 1621 Returns db based config object.
1624 1622 """
1625 1623 from rhodecode.lib.utils import make_db_config
1626 1624 return make_db_config(clear_session=False, repo=self)
1627 1625
1628 1626 def permissions(self, with_admins=True, with_owner=True):
1629 1627 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1630 1628 q = q.options(joinedload(UserRepoToPerm.repository),
1631 1629 joinedload(UserRepoToPerm.user),
1632 1630 joinedload(UserRepoToPerm.permission),)
1633 1631
1634 1632 # get owners and admins and permissions. We do a trick of re-writing
1635 1633 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1636 1634 # has a global reference and changing one object propagates to all
1637 1635 # others. This means if admin is also an owner admin_row that change
1638 1636 # would propagate to both objects
1639 1637 perm_rows = []
1640 1638 for _usr in q.all():
1641 1639 usr = AttributeDict(_usr.user.get_dict())
1642 1640 usr.permission = _usr.permission.permission_name
1643 1641 perm_rows.append(usr)
1644 1642
1645 1643 # filter the perm rows by 'default' first and then sort them by
1646 1644 # admin,write,read,none permissions sorted again alphabetically in
1647 1645 # each group
1648 1646 perm_rows = sorted(perm_rows, key=display_sort)
1649 1647
1650 1648 _admin_perm = 'repository.admin'
1651 1649 owner_row = []
1652 1650 if with_owner:
1653 1651 usr = AttributeDict(self.user.get_dict())
1654 1652 usr.owner_row = True
1655 1653 usr.permission = _admin_perm
1656 1654 owner_row.append(usr)
1657 1655
1658 1656 super_admin_rows = []
1659 1657 if with_admins:
1660 1658 for usr in User.get_all_super_admins():
1661 1659 # if this admin is also owner, don't double the record
1662 1660 if usr.user_id == owner_row[0].user_id:
1663 1661 owner_row[0].admin_row = True
1664 1662 else:
1665 1663 usr = AttributeDict(usr.get_dict())
1666 1664 usr.admin_row = True
1667 1665 usr.permission = _admin_perm
1668 1666 super_admin_rows.append(usr)
1669 1667
1670 1668 return super_admin_rows + owner_row + perm_rows
1671 1669
1672 1670 def permission_user_groups(self):
1673 1671 q = UserGroupRepoToPerm.query().filter(
1674 1672 UserGroupRepoToPerm.repository == self)
1675 1673 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1676 1674 joinedload(UserGroupRepoToPerm.users_group),
1677 1675 joinedload(UserGroupRepoToPerm.permission),)
1678 1676
1679 1677 perm_rows = []
1680 1678 for _user_group in q.all():
1681 1679 usr = AttributeDict(_user_group.users_group.get_dict())
1682 1680 usr.permission = _user_group.permission.permission_name
1683 1681 perm_rows.append(usr)
1684 1682
1685 1683 return perm_rows
1686 1684
1687 1685 def get_api_data(self, include_secrets=False):
1688 1686 """
1689 1687 Common function for generating repo api data
1690 1688
1691 1689 :param include_secrets: See :meth:`User.get_api_data`.
1692 1690
1693 1691 """
1694 1692 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1695 1693 # move this methods on models level.
1696 1694 from rhodecode.model.settings import SettingsModel
1697 1695
1698 1696 repo = self
1699 1697 _user_id, _time, _reason = self.locked
1700 1698
1701 1699 data = {
1702 1700 'repo_id': repo.repo_id,
1703 1701 'repo_name': repo.repo_name,
1704 1702 'repo_type': repo.repo_type,
1705 1703 'clone_uri': repo.clone_uri or '',
1706 1704 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1707 1705 'private': repo.private,
1708 1706 'created_on': repo.created_on,
1709 1707 'description': repo.description,
1710 1708 'landing_rev': repo.landing_rev,
1711 1709 'owner': repo.user.username,
1712 1710 'fork_of': repo.fork.repo_name if repo.fork else None,
1713 1711 'enable_statistics': repo.enable_statistics,
1714 1712 'enable_locking': repo.enable_locking,
1715 1713 'enable_downloads': repo.enable_downloads,
1716 1714 'last_changeset': repo.changeset_cache,
1717 1715 'locked_by': User.get(_user_id).get_api_data(
1718 1716 include_secrets=include_secrets) if _user_id else None,
1719 1717 'locked_date': time_to_datetime(_time) if _time else None,
1720 1718 'lock_reason': _reason if _reason else None,
1721 1719 }
1722 1720
1723 1721 # TODO: mikhail: should be per-repo settings here
1724 1722 rc_config = SettingsModel().get_all_settings()
1725 1723 repository_fields = str2bool(
1726 1724 rc_config.get('rhodecode_repository_fields'))
1727 1725 if repository_fields:
1728 1726 for f in self.extra_fields:
1729 1727 data[f.field_key_prefixed] = f.field_value
1730 1728
1731 1729 return data
1732 1730
1733 1731 @classmethod
1734 1732 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1735 1733 if not lock_time:
1736 1734 lock_time = time.time()
1737 1735 if not lock_reason:
1738 1736 lock_reason = cls.LOCK_AUTOMATIC
1739 1737 repo.locked = [user_id, lock_time, lock_reason]
1740 1738 Session().add(repo)
1741 1739 Session().commit()
1742 1740
1743 1741 @classmethod
1744 1742 def unlock(cls, repo):
1745 1743 repo.locked = None
1746 1744 Session().add(repo)
1747 1745 Session().commit()
1748 1746
1749 1747 @classmethod
1750 1748 def getlock(cls, repo):
1751 1749 return repo.locked
1752 1750
1753 1751 def is_user_lock(self, user_id):
1754 1752 if self.lock[0]:
1755 1753 lock_user_id = safe_int(self.lock[0])
1756 1754 user_id = safe_int(user_id)
1757 1755 # both are ints, and they are equal
1758 1756 return all([lock_user_id, user_id]) and lock_user_id == user_id
1759 1757
1760 1758 return False
1761 1759
1762 1760 def get_locking_state(self, action, user_id, only_when_enabled=True):
1763 1761 """
1764 1762 Checks locking on this repository, if locking is enabled and lock is
1765 1763 present returns a tuple of make_lock, locked, locked_by.
1766 1764 make_lock can have 3 states None (do nothing) True, make lock
1767 1765 False release lock, This value is later propagated to hooks, which
1768 1766 do the locking. Think about this as signals passed to hooks what to do.
1769 1767
1770 1768 """
1771 1769 # TODO: johbo: This is part of the business logic and should be moved
1772 1770 # into the RepositoryModel.
1773 1771
1774 1772 if action not in ('push', 'pull'):
1775 1773 raise ValueError("Invalid action value: %s" % repr(action))
1776 1774
1777 1775 # defines if locked error should be thrown to user
1778 1776 currently_locked = False
1779 1777 # defines if new lock should be made, tri-state
1780 1778 make_lock = None
1781 1779 repo = self
1782 1780 user = User.get(user_id)
1783 1781
1784 1782 lock_info = repo.locked
1785 1783
1786 1784 if repo and (repo.enable_locking or not only_when_enabled):
1787 1785 if action == 'push':
1788 1786 # check if it's already locked !, if it is compare users
1789 1787 locked_by_user_id = lock_info[0]
1790 1788 if user.user_id == locked_by_user_id:
1791 1789 log.debug(
1792 1790 'Got `push` action from user %s, now unlocking', user)
1793 1791 # unlock if we have push from user who locked
1794 1792 make_lock = False
1795 1793 else:
1796 1794 # we're not the same user who locked, ban with
1797 1795 # code defined in settings (default is 423 HTTP Locked) !
1798 1796 log.debug('Repo %s is currently locked by %s', repo, user)
1799 1797 currently_locked = True
1800 1798 elif action == 'pull':
1801 1799 # [0] user [1] date
1802 1800 if lock_info[0] and lock_info[1]:
1803 1801 log.debug('Repo %s is currently locked by %s', repo, user)
1804 1802 currently_locked = True
1805 1803 else:
1806 1804 log.debug('Setting lock on repo %s by %s', repo, user)
1807 1805 make_lock = True
1808 1806
1809 1807 else:
1810 1808 log.debug('Repository %s do not have locking enabled', repo)
1811 1809
1812 1810 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1813 1811 make_lock, currently_locked, lock_info)
1814 1812
1815 1813 from rhodecode.lib.auth import HasRepoPermissionAny
1816 1814 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1817 1815 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1818 1816 # if we don't have at least write permission we cannot make a lock
1819 1817 log.debug('lock state reset back to FALSE due to lack '
1820 1818 'of at least read permission')
1821 1819 make_lock = False
1822 1820
1823 1821 return make_lock, currently_locked, lock_info
1824 1822
1825 1823 @property
1826 1824 def last_db_change(self):
1827 1825 return self.updated_on
1828 1826
1829 1827 @property
1830 1828 def clone_uri_hidden(self):
1831 1829 clone_uri = self.clone_uri
1832 1830 if clone_uri:
1833 1831 import urlobject
1834 1832 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1835 1833 if url_obj.password:
1836 1834 clone_uri = url_obj.with_password('*****')
1837 1835 return clone_uri
1838 1836
1839 1837 def clone_url(self, **override):
1840 1838 qualified_home_url = url('home', qualified=True)
1841 1839
1842 1840 uri_tmpl = None
1843 1841 if 'with_id' in override:
1844 1842 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1845 1843 del override['with_id']
1846 1844
1847 1845 if 'uri_tmpl' in override:
1848 1846 uri_tmpl = override['uri_tmpl']
1849 1847 del override['uri_tmpl']
1850 1848
1851 1849 # we didn't override our tmpl from **overrides
1852 1850 if not uri_tmpl:
1853 1851 uri_tmpl = self.DEFAULT_CLONE_URI
1854 1852 try:
1855 1853 from pylons import tmpl_context as c
1856 1854 uri_tmpl = c.clone_uri_tmpl
1857 1855 except Exception:
1858 1856 # in any case if we call this outside of request context,
1859 1857 # ie, not having tmpl_context set up
1860 1858 pass
1861 1859
1862 1860 return get_clone_url(uri_tmpl=uri_tmpl,
1863 1861 qualifed_home_url=qualified_home_url,
1864 1862 repo_name=self.repo_name,
1865 1863 repo_id=self.repo_id, **override)
1866 1864
1867 1865 def set_state(self, state):
1868 1866 self.repo_state = state
1869 1867 Session().add(self)
1870 1868 #==========================================================================
1871 1869 # SCM PROPERTIES
1872 1870 #==========================================================================
1873 1871
1874 1872 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1875 1873 return get_commit_safe(
1876 1874 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1877 1875
1878 1876 def get_changeset(self, rev=None, pre_load=None):
1879 1877 warnings.warn("Use get_commit", DeprecationWarning)
1880 1878 commit_id = None
1881 1879 commit_idx = None
1882 1880 if isinstance(rev, basestring):
1883 1881 commit_id = rev
1884 1882 else:
1885 1883 commit_idx = rev
1886 1884 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1887 1885 pre_load=pre_load)
1888 1886
1889 1887 def get_landing_commit(self):
1890 1888 """
1891 1889 Returns landing commit, or if that doesn't exist returns the tip
1892 1890 """
1893 1891 _rev_type, _rev = self.landing_rev
1894 1892 commit = self.get_commit(_rev)
1895 1893 if isinstance(commit, EmptyCommit):
1896 1894 return self.get_commit()
1897 1895 return commit
1898 1896
1899 1897 def update_commit_cache(self, cs_cache=None, config=None):
1900 1898 """
1901 1899 Update cache of last changeset for repository, keys should be::
1902 1900
1903 1901 short_id
1904 1902 raw_id
1905 1903 revision
1906 1904 parents
1907 1905 message
1908 1906 date
1909 1907 author
1910 1908
1911 1909 :param cs_cache:
1912 1910 """
1913 1911 from rhodecode.lib.vcs.backends.base import BaseChangeset
1914 1912 if cs_cache is None:
1915 1913 # use no-cache version here
1916 1914 scm_repo = self.scm_instance(cache=False, config=config)
1917 1915 if scm_repo:
1918 1916 cs_cache = scm_repo.get_commit(
1919 1917 pre_load=["author", "date", "message", "parents"])
1920 1918 else:
1921 1919 cs_cache = EmptyCommit()
1922 1920
1923 1921 if isinstance(cs_cache, BaseChangeset):
1924 1922 cs_cache = cs_cache.__json__()
1925 1923
1926 1924 def is_outdated(new_cs_cache):
1927 1925 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1928 1926 new_cs_cache['revision'] != self.changeset_cache['revision']):
1929 1927 return True
1930 1928 return False
1931 1929
1932 1930 # check if we have maybe already latest cached revision
1933 1931 if is_outdated(cs_cache) or not self.changeset_cache:
1934 1932 _default = datetime.datetime.fromtimestamp(0)
1935 1933 last_change = cs_cache.get('date') or _default
1936 1934 log.debug('updated repo %s with new cs cache %s',
1937 1935 self.repo_name, cs_cache)
1938 1936 self.updated_on = last_change
1939 1937 self.changeset_cache = cs_cache
1940 1938 Session().add(self)
1941 1939 Session().commit()
1942 1940 else:
1943 1941 log.debug('Skipping update_commit_cache for repo:`%s` '
1944 1942 'commit already with latest changes', self.repo_name)
1945 1943
1946 1944 @property
1947 1945 def tip(self):
1948 1946 return self.get_commit('tip')
1949 1947
1950 1948 @property
1951 1949 def author(self):
1952 1950 return self.tip.author
1953 1951
1954 1952 @property
1955 1953 def last_change(self):
1956 1954 return self.scm_instance().last_change
1957 1955
1958 1956 def get_comments(self, revisions=None):
1959 1957 """
1960 1958 Returns comments for this repository grouped by revisions
1961 1959
1962 1960 :param revisions: filter query by revisions only
1963 1961 """
1964 1962 cmts = ChangesetComment.query()\
1965 1963 .filter(ChangesetComment.repo == self)
1966 1964 if revisions:
1967 1965 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1968 1966 grouped = collections.defaultdict(list)
1969 1967 for cmt in cmts.all():
1970 1968 grouped[cmt.revision].append(cmt)
1971 1969 return grouped
1972 1970
1973 1971 def statuses(self, revisions=None):
1974 1972 """
1975 1973 Returns statuses for this repository
1976 1974
1977 1975 :param revisions: list of revisions to get statuses for
1978 1976 """
1979 1977 statuses = ChangesetStatus.query()\
1980 1978 .filter(ChangesetStatus.repo == self)\
1981 1979 .filter(ChangesetStatus.version == 0)
1982 1980
1983 1981 if revisions:
1984 1982 # Try doing the filtering in chunks to avoid hitting limits
1985 1983 size = 500
1986 1984 status_results = []
1987 1985 for chunk in xrange(0, len(revisions), size):
1988 1986 status_results += statuses.filter(
1989 1987 ChangesetStatus.revision.in_(
1990 1988 revisions[chunk: chunk+size])
1991 1989 ).all()
1992 1990 else:
1993 1991 status_results = statuses.all()
1994 1992
1995 1993 grouped = {}
1996 1994
1997 1995 # maybe we have open new pullrequest without a status?
1998 1996 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1999 1997 status_lbl = ChangesetStatus.get_status_lbl(stat)
2000 1998 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2001 1999 for rev in pr.revisions:
2002 2000 pr_id = pr.pull_request_id
2003 2001 pr_repo = pr.target_repo.repo_name
2004 2002 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2005 2003
2006 2004 for stat in status_results:
2007 2005 pr_id = pr_repo = None
2008 2006 if stat.pull_request:
2009 2007 pr_id = stat.pull_request.pull_request_id
2010 2008 pr_repo = stat.pull_request.target_repo.repo_name
2011 2009 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2012 2010 pr_id, pr_repo]
2013 2011 return grouped
2014 2012
2015 2013 # ==========================================================================
2016 2014 # SCM CACHE INSTANCE
2017 2015 # ==========================================================================
2018 2016
2019 2017 def scm_instance(self, **kwargs):
2020 2018 import rhodecode
2021 2019
2022 2020 # Passing a config will not hit the cache currently only used
2023 2021 # for repo2dbmapper
2024 2022 config = kwargs.pop('config', None)
2025 2023 cache = kwargs.pop('cache', None)
2026 2024 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2027 2025 # if cache is NOT defined use default global, else we have a full
2028 2026 # control over cache behaviour
2029 2027 if cache is None and full_cache and not config:
2030 2028 return self._get_instance_cached()
2031 2029 return self._get_instance(cache=bool(cache), config=config)
2032 2030
2033 2031 def _get_instance_cached(self):
2034 2032 @cache_region('long_term')
2035 2033 def _get_repo(cache_key):
2036 2034 return self._get_instance()
2037 2035
2038 2036 invalidator_context = CacheKey.repo_context_cache(
2039 2037 _get_repo, self.repo_name, None, thread_scoped=True)
2040 2038
2041 2039 with invalidator_context as context:
2042 2040 context.invalidate()
2043 2041 repo = context.compute()
2044 2042
2045 2043 return repo
2046 2044
2047 2045 def _get_instance(self, cache=True, config=None):
2048 2046 config = config or self._config
2049 2047 custom_wire = {
2050 2048 'cache': cache # controls the vcs.remote cache
2051 2049 }
2052 2050 repo = get_vcs_instance(
2053 2051 repo_path=safe_str(self.repo_full_path),
2054 2052 config=config,
2055 2053 with_wire=custom_wire,
2056 2054 create=False,
2057 2055 _vcs_alias=self.repo_type)
2058 2056
2059 2057 return repo
2060 2058
2061 2059 def __json__(self):
2062 2060 return {'landing_rev': self.landing_rev}
2063 2061
2064 2062 def get_dict(self):
2065 2063
2066 2064 # Since we transformed `repo_name` to a hybrid property, we need to
2067 2065 # keep compatibility with the code which uses `repo_name` field.
2068 2066
2069 2067 result = super(Repository, self).get_dict()
2070 2068 result['repo_name'] = result.pop('_repo_name', None)
2071 2069 return result
2072 2070
2073 2071
2074 2072 class RepoGroup(Base, BaseModel):
2075 2073 __tablename__ = 'groups'
2076 2074 __table_args__ = (
2077 2075 UniqueConstraint('group_name', 'group_parent_id'),
2078 2076 CheckConstraint('group_id != group_parent_id'),
2079 2077 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2080 2078 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2081 2079 )
2082 2080 __mapper_args__ = {'order_by': 'group_name'}
2083 2081
2084 2082 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2085 2083
2086 2084 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2087 2085 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2088 2086 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2089 2087 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2090 2088 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2091 2089 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2092 2090 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2093 2091 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2094 2092
2095 2093 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2096 2094 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2097 2095 parent_group = relationship('RepoGroup', remote_side=group_id)
2098 2096 user = relationship('User')
2099 2097 integrations = relationship('Integration',
2100 2098 cascade="all, delete, delete-orphan")
2101 2099
2102 2100 def __init__(self, group_name='', parent_group=None):
2103 2101 self.group_name = group_name
2104 2102 self.parent_group = parent_group
2105 2103
2106 2104 def __unicode__(self):
2107 2105 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2108 2106 self.group_name)
2109 2107
2110 2108 @classmethod
2111 2109 def _generate_choice(cls, repo_group):
2112 2110 from webhelpers.html import literal as _literal
2113 2111 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2114 2112 return repo_group.group_id, _name(repo_group.full_path_splitted)
2115 2113
2116 2114 @classmethod
2117 2115 def groups_choices(cls, groups=None, show_empty_group=True):
2118 2116 if not groups:
2119 2117 groups = cls.query().all()
2120 2118
2121 2119 repo_groups = []
2122 2120 if show_empty_group:
2123 2121 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2124 2122
2125 2123 repo_groups.extend([cls._generate_choice(x) for x in groups])
2126 2124
2127 2125 repo_groups = sorted(
2128 2126 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2129 2127 return repo_groups
2130 2128
2131 2129 @classmethod
2132 2130 def url_sep(cls):
2133 2131 return URL_SEP
2134 2132
2135 2133 @classmethod
2136 2134 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2137 2135 if case_insensitive:
2138 2136 gr = cls.query().filter(func.lower(cls.group_name)
2139 2137 == func.lower(group_name))
2140 2138 else:
2141 2139 gr = cls.query().filter(cls.group_name == group_name)
2142 2140 if cache:
2143 2141 gr = gr.options(FromCache(
2144 2142 "sql_cache_short",
2145 2143 "get_group_%s" % _hash_key(group_name)))
2146 2144 return gr.scalar()
2147 2145
2148 2146 @classmethod
2149 2147 def get_user_personal_repo_group(cls, user_id):
2150 2148 user = User.get(user_id)
2151 2149 return cls.query()\
2152 2150 .filter(cls.personal == true())\
2153 2151 .filter(cls.user == user).scalar()
2154 2152
2155 2153 @classmethod
2156 2154 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2157 2155 case_insensitive=True):
2158 2156 q = RepoGroup.query()
2159 2157
2160 2158 if not isinstance(user_id, Optional):
2161 2159 q = q.filter(RepoGroup.user_id == user_id)
2162 2160
2163 2161 if not isinstance(group_id, Optional):
2164 2162 q = q.filter(RepoGroup.group_parent_id == group_id)
2165 2163
2166 2164 if case_insensitive:
2167 2165 q = q.order_by(func.lower(RepoGroup.group_name))
2168 2166 else:
2169 2167 q = q.order_by(RepoGroup.group_name)
2170 2168 return q.all()
2171 2169
2172 2170 @property
2173 2171 def parents(self):
2174 2172 parents_recursion_limit = 10
2175 2173 groups = []
2176 2174 if self.parent_group is None:
2177 2175 return groups
2178 2176 cur_gr = self.parent_group
2179 2177 groups.insert(0, cur_gr)
2180 2178 cnt = 0
2181 2179 while 1:
2182 2180 cnt += 1
2183 2181 gr = getattr(cur_gr, 'parent_group', None)
2184 2182 cur_gr = cur_gr.parent_group
2185 2183 if gr is None:
2186 2184 break
2187 2185 if cnt == parents_recursion_limit:
2188 2186 # this will prevent accidental infinit loops
2189 2187 log.error(('more than %s parents found for group %s, stopping '
2190 2188 'recursive parent fetching' % (parents_recursion_limit, self)))
2191 2189 break
2192 2190
2193 2191 groups.insert(0, gr)
2194 2192 return groups
2195 2193
2196 2194 @property
2197 2195 def children(self):
2198 2196 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2199 2197
2200 2198 @property
2201 2199 def name(self):
2202 2200 return self.group_name.split(RepoGroup.url_sep())[-1]
2203 2201
2204 2202 @property
2205 2203 def full_path(self):
2206 2204 return self.group_name
2207 2205
2208 2206 @property
2209 2207 def full_path_splitted(self):
2210 2208 return self.group_name.split(RepoGroup.url_sep())
2211 2209
2212 2210 @property
2213 2211 def repositories(self):
2214 2212 return Repository.query()\
2215 2213 .filter(Repository.group == self)\
2216 2214 .order_by(Repository.repo_name)
2217 2215
2218 2216 @property
2219 2217 def repositories_recursive_count(self):
2220 2218 cnt = self.repositories.count()
2221 2219
2222 2220 def children_count(group):
2223 2221 cnt = 0
2224 2222 for child in group.children:
2225 2223 cnt += child.repositories.count()
2226 2224 cnt += children_count(child)
2227 2225 return cnt
2228 2226
2229 2227 return cnt + children_count(self)
2230 2228
2231 2229 def _recursive_objects(self, include_repos=True):
2232 2230 all_ = []
2233 2231
2234 2232 def _get_members(root_gr):
2235 2233 if include_repos:
2236 2234 for r in root_gr.repositories:
2237 2235 all_.append(r)
2238 2236 childs = root_gr.children.all()
2239 2237 if childs:
2240 2238 for gr in childs:
2241 2239 all_.append(gr)
2242 2240 _get_members(gr)
2243 2241
2244 2242 _get_members(self)
2245 2243 return [self] + all_
2246 2244
2247 2245 def recursive_groups_and_repos(self):
2248 2246 """
2249 2247 Recursive return all groups, with repositories in those groups
2250 2248 """
2251 2249 return self._recursive_objects()
2252 2250
2253 2251 def recursive_groups(self):
2254 2252 """
2255 2253 Returns all children groups for this group including children of children
2256 2254 """
2257 2255 return self._recursive_objects(include_repos=False)
2258 2256
2259 2257 def get_new_name(self, group_name):
2260 2258 """
2261 2259 returns new full group name based on parent and new name
2262 2260
2263 2261 :param group_name:
2264 2262 """
2265 2263 path_prefix = (self.parent_group.full_path_splitted if
2266 2264 self.parent_group else [])
2267 2265 return RepoGroup.url_sep().join(path_prefix + [group_name])
2268 2266
2269 2267 def permissions(self, with_admins=True, with_owner=True):
2270 2268 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2271 2269 q = q.options(joinedload(UserRepoGroupToPerm.group),
2272 2270 joinedload(UserRepoGroupToPerm.user),
2273 2271 joinedload(UserRepoGroupToPerm.permission),)
2274 2272
2275 2273 # get owners and admins and permissions. We do a trick of re-writing
2276 2274 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2277 2275 # has a global reference and changing one object propagates to all
2278 2276 # others. This means if admin is also an owner admin_row that change
2279 2277 # would propagate to both objects
2280 2278 perm_rows = []
2281 2279 for _usr in q.all():
2282 2280 usr = AttributeDict(_usr.user.get_dict())
2283 2281 usr.permission = _usr.permission.permission_name
2284 2282 perm_rows.append(usr)
2285 2283
2286 2284 # filter the perm rows by 'default' first and then sort them by
2287 2285 # admin,write,read,none permissions sorted again alphabetically in
2288 2286 # each group
2289 2287 perm_rows = sorted(perm_rows, key=display_sort)
2290 2288
2291 2289 _admin_perm = 'group.admin'
2292 2290 owner_row = []
2293 2291 if with_owner:
2294 2292 usr = AttributeDict(self.user.get_dict())
2295 2293 usr.owner_row = True
2296 2294 usr.permission = _admin_perm
2297 2295 owner_row.append(usr)
2298 2296
2299 2297 super_admin_rows = []
2300 2298 if with_admins:
2301 2299 for usr in User.get_all_super_admins():
2302 2300 # if this admin is also owner, don't double the record
2303 2301 if usr.user_id == owner_row[0].user_id:
2304 2302 owner_row[0].admin_row = True
2305 2303 else:
2306 2304 usr = AttributeDict(usr.get_dict())
2307 2305 usr.admin_row = True
2308 2306 usr.permission = _admin_perm
2309 2307 super_admin_rows.append(usr)
2310 2308
2311 2309 return super_admin_rows + owner_row + perm_rows
2312 2310
2313 2311 def permission_user_groups(self):
2314 2312 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2315 2313 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2316 2314 joinedload(UserGroupRepoGroupToPerm.users_group),
2317 2315 joinedload(UserGroupRepoGroupToPerm.permission),)
2318 2316
2319 2317 perm_rows = []
2320 2318 for _user_group in q.all():
2321 2319 usr = AttributeDict(_user_group.users_group.get_dict())
2322 2320 usr.permission = _user_group.permission.permission_name
2323 2321 perm_rows.append(usr)
2324 2322
2325 2323 return perm_rows
2326 2324
2327 2325 def get_api_data(self):
2328 2326 """
2329 2327 Common function for generating api data
2330 2328
2331 2329 """
2332 2330 group = self
2333 2331 data = {
2334 2332 'group_id': group.group_id,
2335 2333 'group_name': group.group_name,
2336 2334 'group_description': group.group_description,
2337 2335 'parent_group': group.parent_group.group_name if group.parent_group else None,
2338 2336 'repositories': [x.repo_name for x in group.repositories],
2339 2337 'owner': group.user.username,
2340 2338 }
2341 2339 return data
2342 2340
2343 2341
2344 2342 class Permission(Base, BaseModel):
2345 2343 __tablename__ = 'permissions'
2346 2344 __table_args__ = (
2347 2345 Index('p_perm_name_idx', 'permission_name'),
2348 2346 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2349 2347 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2350 2348 )
2351 2349 PERMS = [
2352 2350 ('hg.admin', _('RhodeCode Super Administrator')),
2353 2351
2354 2352 ('repository.none', _('Repository no access')),
2355 2353 ('repository.read', _('Repository read access')),
2356 2354 ('repository.write', _('Repository write access')),
2357 2355 ('repository.admin', _('Repository admin access')),
2358 2356
2359 2357 ('group.none', _('Repository group no access')),
2360 2358 ('group.read', _('Repository group read access')),
2361 2359 ('group.write', _('Repository group write access')),
2362 2360 ('group.admin', _('Repository group admin access')),
2363 2361
2364 2362 ('usergroup.none', _('User group no access')),
2365 2363 ('usergroup.read', _('User group read access')),
2366 2364 ('usergroup.write', _('User group write access')),
2367 2365 ('usergroup.admin', _('User group admin access')),
2368 2366
2369 2367 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2370 2368 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2371 2369
2372 2370 ('hg.usergroup.create.false', _('User Group creation disabled')),
2373 2371 ('hg.usergroup.create.true', _('User Group creation enabled')),
2374 2372
2375 2373 ('hg.create.none', _('Repository creation disabled')),
2376 2374 ('hg.create.repository', _('Repository creation enabled')),
2377 2375 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2378 2376 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2379 2377
2380 2378 ('hg.fork.none', _('Repository forking disabled')),
2381 2379 ('hg.fork.repository', _('Repository forking enabled')),
2382 2380
2383 2381 ('hg.register.none', _('Registration disabled')),
2384 2382 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2385 2383 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2386 2384
2387 2385 ('hg.password_reset.enabled', _('Password reset enabled')),
2388 2386 ('hg.password_reset.hidden', _('Password reset hidden')),
2389 2387 ('hg.password_reset.disabled', _('Password reset disabled')),
2390 2388
2391 2389 ('hg.extern_activate.manual', _('Manual activation of external account')),
2392 2390 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2393 2391
2394 2392 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2395 2393 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2396 2394 ]
2397 2395
2398 2396 # definition of system default permissions for DEFAULT user
2399 2397 DEFAULT_USER_PERMISSIONS = [
2400 2398 'repository.read',
2401 2399 'group.read',
2402 2400 'usergroup.read',
2403 2401 'hg.create.repository',
2404 2402 'hg.repogroup.create.false',
2405 2403 'hg.usergroup.create.false',
2406 2404 'hg.create.write_on_repogroup.true',
2407 2405 'hg.fork.repository',
2408 2406 'hg.register.manual_activate',
2409 2407 'hg.password_reset.enabled',
2410 2408 'hg.extern_activate.auto',
2411 2409 'hg.inherit_default_perms.true',
2412 2410 ]
2413 2411
2414 2412 # defines which permissions are more important higher the more important
2415 2413 # Weight defines which permissions are more important.
2416 2414 # The higher number the more important.
2417 2415 PERM_WEIGHTS = {
2418 2416 'repository.none': 0,
2419 2417 'repository.read': 1,
2420 2418 'repository.write': 3,
2421 2419 'repository.admin': 4,
2422 2420
2423 2421 'group.none': 0,
2424 2422 'group.read': 1,
2425 2423 'group.write': 3,
2426 2424 'group.admin': 4,
2427 2425
2428 2426 'usergroup.none': 0,
2429 2427 'usergroup.read': 1,
2430 2428 'usergroup.write': 3,
2431 2429 'usergroup.admin': 4,
2432 2430
2433 2431 'hg.repogroup.create.false': 0,
2434 2432 'hg.repogroup.create.true': 1,
2435 2433
2436 2434 'hg.usergroup.create.false': 0,
2437 2435 'hg.usergroup.create.true': 1,
2438 2436
2439 2437 'hg.fork.none': 0,
2440 2438 'hg.fork.repository': 1,
2441 2439 'hg.create.none': 0,
2442 2440 'hg.create.repository': 1
2443 2441 }
2444 2442
2445 2443 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2446 2444 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2447 2445 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2448 2446
2449 2447 def __unicode__(self):
2450 2448 return u"<%s('%s:%s')>" % (
2451 2449 self.__class__.__name__, self.permission_id, self.permission_name
2452 2450 )
2453 2451
2454 2452 @classmethod
2455 2453 def get_by_key(cls, key):
2456 2454 return cls.query().filter(cls.permission_name == key).scalar()
2457 2455
2458 2456 @classmethod
2459 2457 def get_default_repo_perms(cls, user_id, repo_id=None):
2460 2458 q = Session().query(UserRepoToPerm, Repository, Permission)\
2461 2459 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2462 2460 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2463 2461 .filter(UserRepoToPerm.user_id == user_id)
2464 2462 if repo_id:
2465 2463 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2466 2464 return q.all()
2467 2465
2468 2466 @classmethod
2469 2467 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2470 2468 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2471 2469 .join(
2472 2470 Permission,
2473 2471 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2474 2472 .join(
2475 2473 Repository,
2476 2474 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2477 2475 .join(
2478 2476 UserGroup,
2479 2477 UserGroupRepoToPerm.users_group_id ==
2480 2478 UserGroup.users_group_id)\
2481 2479 .join(
2482 2480 UserGroupMember,
2483 2481 UserGroupRepoToPerm.users_group_id ==
2484 2482 UserGroupMember.users_group_id)\
2485 2483 .filter(
2486 2484 UserGroupMember.user_id == user_id,
2487 2485 UserGroup.users_group_active == true())
2488 2486 if repo_id:
2489 2487 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2490 2488 return q.all()
2491 2489
2492 2490 @classmethod
2493 2491 def get_default_group_perms(cls, user_id, repo_group_id=None):
2494 2492 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2495 2493 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2496 2494 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2497 2495 .filter(UserRepoGroupToPerm.user_id == user_id)
2498 2496 if repo_group_id:
2499 2497 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2500 2498 return q.all()
2501 2499
2502 2500 @classmethod
2503 2501 def get_default_group_perms_from_user_group(
2504 2502 cls, user_id, repo_group_id=None):
2505 2503 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2506 2504 .join(
2507 2505 Permission,
2508 2506 UserGroupRepoGroupToPerm.permission_id ==
2509 2507 Permission.permission_id)\
2510 2508 .join(
2511 2509 RepoGroup,
2512 2510 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2513 2511 .join(
2514 2512 UserGroup,
2515 2513 UserGroupRepoGroupToPerm.users_group_id ==
2516 2514 UserGroup.users_group_id)\
2517 2515 .join(
2518 2516 UserGroupMember,
2519 2517 UserGroupRepoGroupToPerm.users_group_id ==
2520 2518 UserGroupMember.users_group_id)\
2521 2519 .filter(
2522 2520 UserGroupMember.user_id == user_id,
2523 2521 UserGroup.users_group_active == true())
2524 2522 if repo_group_id:
2525 2523 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2526 2524 return q.all()
2527 2525
2528 2526 @classmethod
2529 2527 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2530 2528 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2531 2529 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2532 2530 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2533 2531 .filter(UserUserGroupToPerm.user_id == user_id)
2534 2532 if user_group_id:
2535 2533 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2536 2534 return q.all()
2537 2535
2538 2536 @classmethod
2539 2537 def get_default_user_group_perms_from_user_group(
2540 2538 cls, user_id, user_group_id=None):
2541 2539 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2542 2540 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2543 2541 .join(
2544 2542 Permission,
2545 2543 UserGroupUserGroupToPerm.permission_id ==
2546 2544 Permission.permission_id)\
2547 2545 .join(
2548 2546 TargetUserGroup,
2549 2547 UserGroupUserGroupToPerm.target_user_group_id ==
2550 2548 TargetUserGroup.users_group_id)\
2551 2549 .join(
2552 2550 UserGroup,
2553 2551 UserGroupUserGroupToPerm.user_group_id ==
2554 2552 UserGroup.users_group_id)\
2555 2553 .join(
2556 2554 UserGroupMember,
2557 2555 UserGroupUserGroupToPerm.user_group_id ==
2558 2556 UserGroupMember.users_group_id)\
2559 2557 .filter(
2560 2558 UserGroupMember.user_id == user_id,
2561 2559 UserGroup.users_group_active == true())
2562 2560 if user_group_id:
2563 2561 q = q.filter(
2564 2562 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2565 2563
2566 2564 return q.all()
2567 2565
2568 2566
2569 2567 class UserRepoToPerm(Base, BaseModel):
2570 2568 __tablename__ = 'repo_to_perm'
2571 2569 __table_args__ = (
2572 2570 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2573 2571 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2574 2572 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2575 2573 )
2576 2574 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2577 2575 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2578 2576 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2579 2577 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2580 2578
2581 2579 user = relationship('User')
2582 2580 repository = relationship('Repository')
2583 2581 permission = relationship('Permission')
2584 2582
2585 2583 @classmethod
2586 2584 def create(cls, user, repository, permission):
2587 2585 n = cls()
2588 2586 n.user = user
2589 2587 n.repository = repository
2590 2588 n.permission = permission
2591 2589 Session().add(n)
2592 2590 return n
2593 2591
2594 2592 def __unicode__(self):
2595 2593 return u'<%s => %s >' % (self.user, self.repository)
2596 2594
2597 2595
2598 2596 class UserUserGroupToPerm(Base, BaseModel):
2599 2597 __tablename__ = 'user_user_group_to_perm'
2600 2598 __table_args__ = (
2601 2599 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2602 2600 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2603 2601 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2604 2602 )
2605 2603 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2606 2604 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2607 2605 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2608 2606 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2609 2607
2610 2608 user = relationship('User')
2611 2609 user_group = relationship('UserGroup')
2612 2610 permission = relationship('Permission')
2613 2611
2614 2612 @classmethod
2615 2613 def create(cls, user, user_group, permission):
2616 2614 n = cls()
2617 2615 n.user = user
2618 2616 n.user_group = user_group
2619 2617 n.permission = permission
2620 2618 Session().add(n)
2621 2619 return n
2622 2620
2623 2621 def __unicode__(self):
2624 2622 return u'<%s => %s >' % (self.user, self.user_group)
2625 2623
2626 2624
2627 2625 class UserToPerm(Base, BaseModel):
2628 2626 __tablename__ = 'user_to_perm'
2629 2627 __table_args__ = (
2630 2628 UniqueConstraint('user_id', 'permission_id'),
2631 2629 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2632 2630 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2633 2631 )
2634 2632 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2635 2633 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2636 2634 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2637 2635
2638 2636 user = relationship('User')
2639 2637 permission = relationship('Permission', lazy='joined')
2640 2638
2641 2639 def __unicode__(self):
2642 2640 return u'<%s => %s >' % (self.user, self.permission)
2643 2641
2644 2642
2645 2643 class UserGroupRepoToPerm(Base, BaseModel):
2646 2644 __tablename__ = 'users_group_repo_to_perm'
2647 2645 __table_args__ = (
2648 2646 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2649 2647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2650 2648 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2651 2649 )
2652 2650 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2653 2651 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2654 2652 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2655 2653 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2656 2654
2657 2655 users_group = relationship('UserGroup')
2658 2656 permission = relationship('Permission')
2659 2657 repository = relationship('Repository')
2660 2658
2661 2659 @classmethod
2662 2660 def create(cls, users_group, repository, permission):
2663 2661 n = cls()
2664 2662 n.users_group = users_group
2665 2663 n.repository = repository
2666 2664 n.permission = permission
2667 2665 Session().add(n)
2668 2666 return n
2669 2667
2670 2668 def __unicode__(self):
2671 2669 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2672 2670
2673 2671
2674 2672 class UserGroupUserGroupToPerm(Base, BaseModel):
2675 2673 __tablename__ = 'user_group_user_group_to_perm'
2676 2674 __table_args__ = (
2677 2675 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2678 2676 CheckConstraint('target_user_group_id != user_group_id'),
2679 2677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2680 2678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2681 2679 )
2682 2680 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)
2683 2681 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684 2682 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2685 2683 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2684
2687 2685 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2688 2686 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2689 2687 permission = relationship('Permission')
2690 2688
2691 2689 @classmethod
2692 2690 def create(cls, target_user_group, user_group, permission):
2693 2691 n = cls()
2694 2692 n.target_user_group = target_user_group
2695 2693 n.user_group = user_group
2696 2694 n.permission = permission
2697 2695 Session().add(n)
2698 2696 return n
2699 2697
2700 2698 def __unicode__(self):
2701 2699 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2702 2700
2703 2701
2704 2702 class UserGroupToPerm(Base, BaseModel):
2705 2703 __tablename__ = 'users_group_to_perm'
2706 2704 __table_args__ = (
2707 2705 UniqueConstraint('users_group_id', 'permission_id',),
2708 2706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2709 2707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2710 2708 )
2711 2709 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2712 2710 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2713 2711 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2714 2712
2715 2713 users_group = relationship('UserGroup')
2716 2714 permission = relationship('Permission')
2717 2715
2718 2716
2719 2717 class UserRepoGroupToPerm(Base, BaseModel):
2720 2718 __tablename__ = 'user_repo_group_to_perm'
2721 2719 __table_args__ = (
2722 2720 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2723 2721 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 2722 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 2723 )
2726 2724
2727 2725 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2726 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2729 2727 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2730 2728 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2731 2729
2732 2730 user = relationship('User')
2733 2731 group = relationship('RepoGroup')
2734 2732 permission = relationship('Permission')
2735 2733
2736 2734 @classmethod
2737 2735 def create(cls, user, repository_group, permission):
2738 2736 n = cls()
2739 2737 n.user = user
2740 2738 n.group = repository_group
2741 2739 n.permission = permission
2742 2740 Session().add(n)
2743 2741 return n
2744 2742
2745 2743
2746 2744 class UserGroupRepoGroupToPerm(Base, BaseModel):
2747 2745 __tablename__ = 'users_group_repo_group_to_perm'
2748 2746 __table_args__ = (
2749 2747 UniqueConstraint('users_group_id', 'group_id'),
2750 2748 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2751 2749 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2752 2750 )
2753 2751
2754 2752 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)
2755 2753 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2756 2754 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2757 2755 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2758 2756
2759 2757 users_group = relationship('UserGroup')
2760 2758 permission = relationship('Permission')
2761 2759 group = relationship('RepoGroup')
2762 2760
2763 2761 @classmethod
2764 2762 def create(cls, user_group, repository_group, permission):
2765 2763 n = cls()
2766 2764 n.users_group = user_group
2767 2765 n.group = repository_group
2768 2766 n.permission = permission
2769 2767 Session().add(n)
2770 2768 return n
2771 2769
2772 2770 def __unicode__(self):
2773 2771 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2774 2772
2775 2773
2776 2774 class Statistics(Base, BaseModel):
2777 2775 __tablename__ = 'statistics'
2778 2776 __table_args__ = (
2779 2777 UniqueConstraint('repository_id'),
2780 2778 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2781 2779 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2782 2780 )
2783 2781 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2784 2782 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2785 2783 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2786 2784 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2787 2785 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2788 2786 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2789 2787
2790 2788 repository = relationship('Repository', single_parent=True)
2791 2789
2792 2790
2793 2791 class UserFollowing(Base, BaseModel):
2794 2792 __tablename__ = 'user_followings'
2795 2793 __table_args__ = (
2796 2794 UniqueConstraint('user_id', 'follows_repository_id'),
2797 2795 UniqueConstraint('user_id', 'follows_user_id'),
2798 2796 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2799 2797 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2800 2798 )
2801 2799
2802 2800 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2803 2801 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2804 2802 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2805 2803 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2806 2804 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2807 2805
2808 2806 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2809 2807
2810 2808 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2811 2809 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2812 2810
2813 2811 @classmethod
2814 2812 def get_repo_followers(cls, repo_id):
2815 2813 return cls.query().filter(cls.follows_repo_id == repo_id)
2816 2814
2817 2815
2818 2816 class CacheKey(Base, BaseModel):
2819 2817 __tablename__ = 'cache_invalidation'
2820 2818 __table_args__ = (
2821 2819 UniqueConstraint('cache_key'),
2822 2820 Index('key_idx', 'cache_key'),
2823 2821 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2824 2822 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2825 2823 )
2826 2824 CACHE_TYPE_ATOM = 'ATOM'
2827 2825 CACHE_TYPE_RSS = 'RSS'
2828 2826 CACHE_TYPE_README = 'README'
2829 2827
2830 2828 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2831 2829 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2832 2830 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2833 2831 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2834 2832
2835 2833 def __init__(self, cache_key, cache_args=''):
2836 2834 self.cache_key = cache_key
2837 2835 self.cache_args = cache_args
2838 2836 self.cache_active = False
2839 2837
2840 2838 def __unicode__(self):
2841 2839 return u"<%s('%s:%s[%s]')>" % (
2842 2840 self.__class__.__name__,
2843 2841 self.cache_id, self.cache_key, self.cache_active)
2844 2842
2845 2843 def _cache_key_partition(self):
2846 2844 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2847 2845 return prefix, repo_name, suffix
2848 2846
2849 2847 def get_prefix(self):
2850 2848 """
2851 2849 Try to extract prefix from existing cache key. The key could consist
2852 2850 of prefix, repo_name, suffix
2853 2851 """
2854 2852 # this returns prefix, repo_name, suffix
2855 2853 return self._cache_key_partition()[0]
2856 2854
2857 2855 def get_suffix(self):
2858 2856 """
2859 2857 get suffix that might have been used in _get_cache_key to
2860 2858 generate self.cache_key. Only used for informational purposes
2861 2859 in repo_edit.mako.
2862 2860 """
2863 2861 # prefix, repo_name, suffix
2864 2862 return self._cache_key_partition()[2]
2865 2863
2866 2864 @classmethod
2867 2865 def delete_all_cache(cls):
2868 2866 """
2869 2867 Delete all cache keys from database.
2870 2868 Should only be run when all instances are down and all entries
2871 2869 thus stale.
2872 2870 """
2873 2871 cls.query().delete()
2874 2872 Session().commit()
2875 2873
2876 2874 @classmethod
2877 2875 def get_cache_key(cls, repo_name, cache_type):
2878 2876 """
2879 2877
2880 2878 Generate a cache key for this process of RhodeCode instance.
2881 2879 Prefix most likely will be process id or maybe explicitly set
2882 2880 instance_id from .ini file.
2883 2881 """
2884 2882 import rhodecode
2885 2883 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2886 2884
2887 2885 repo_as_unicode = safe_unicode(repo_name)
2888 2886 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2889 2887 if cache_type else repo_as_unicode
2890 2888
2891 2889 return u'{}{}'.format(prefix, key)
2892 2890
2893 2891 @classmethod
2894 2892 def set_invalidate(cls, repo_name, delete=False):
2895 2893 """
2896 2894 Mark all caches of a repo as invalid in the database.
2897 2895 """
2898 2896
2899 2897 try:
2900 2898 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2901 2899 if delete:
2902 2900 log.debug('cache objects deleted for repo %s',
2903 2901 safe_str(repo_name))
2904 2902 qry.delete()
2905 2903 else:
2906 2904 log.debug('cache objects marked as invalid for repo %s',
2907 2905 safe_str(repo_name))
2908 2906 qry.update({"cache_active": False})
2909 2907
2910 2908 Session().commit()
2911 2909 except Exception:
2912 2910 log.exception(
2913 2911 'Cache key invalidation failed for repository %s',
2914 2912 safe_str(repo_name))
2915 2913 Session().rollback()
2916 2914
2917 2915 @classmethod
2918 2916 def get_active_cache(cls, cache_key):
2919 2917 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2920 2918 if inv_obj:
2921 2919 return inv_obj
2922 2920 return None
2923 2921
2924 2922 @classmethod
2925 2923 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2926 2924 thread_scoped=False):
2927 2925 """
2928 2926 @cache_region('long_term')
2929 2927 def _heavy_calculation(cache_key):
2930 2928 return 'result'
2931 2929
2932 2930 cache_context = CacheKey.repo_context_cache(
2933 2931 _heavy_calculation, repo_name, cache_type)
2934 2932
2935 2933 with cache_context as context:
2936 2934 context.invalidate()
2937 2935 computed = context.compute()
2938 2936
2939 2937 assert computed == 'result'
2940 2938 """
2941 2939 from rhodecode.lib import caches
2942 2940 return caches.InvalidationContext(
2943 2941 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2944 2942
2945 2943
2946 2944 class ChangesetComment(Base, BaseModel):
2947 2945 __tablename__ = 'changeset_comments'
2948 2946 __table_args__ = (
2949 2947 Index('cc_revision_idx', 'revision'),
2950 2948 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2951 2949 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2952 2950 )
2953 2951
2954 2952 COMMENT_OUTDATED = u'comment_outdated'
2955 2953 COMMENT_TYPE_NOTE = u'note'
2956 2954 COMMENT_TYPE_TODO = u'todo'
2957 2955 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2958 2956
2959 2957 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2960 2958 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2961 2959 revision = Column('revision', String(40), nullable=True)
2962 2960 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2963 2961 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2964 2962 line_no = Column('line_no', Unicode(10), nullable=True)
2965 2963 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2966 2964 f_path = Column('f_path', Unicode(1000), nullable=True)
2967 2965 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2968 2966 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2969 2967 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2970 2968 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2971 2969 renderer = Column('renderer', Unicode(64), nullable=True)
2972 2970 display_state = Column('display_state', Unicode(128), nullable=True)
2973 2971
2974 2972 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2975 2973 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2976 2974 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2977 2975 author = relationship('User', lazy='joined')
2978 2976 repo = relationship('Repository')
2979 2977 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2980 2978 pull_request = relationship('PullRequest', lazy='joined')
2981 2979 pull_request_version = relationship('PullRequestVersion')
2982 2980
2983 2981 @classmethod
2984 2982 def get_users(cls, revision=None, pull_request_id=None):
2985 2983 """
2986 2984 Returns user associated with this ChangesetComment. ie those
2987 2985 who actually commented
2988 2986
2989 2987 :param cls:
2990 2988 :param revision:
2991 2989 """
2992 2990 q = Session().query(User)\
2993 2991 .join(ChangesetComment.author)
2994 2992 if revision:
2995 2993 q = q.filter(cls.revision == revision)
2996 2994 elif pull_request_id:
2997 2995 q = q.filter(cls.pull_request_id == pull_request_id)
2998 2996 return q.all()
2999 2997
3000 2998 @classmethod
3001 2999 def get_index_from_version(cls, pr_version, versions):
3002 3000 num_versions = [x.pull_request_version_id for x in versions]
3003 3001 try:
3004 3002 return num_versions.index(pr_version) +1
3005 3003 except (IndexError, ValueError):
3006 3004 return
3007 3005
3008 3006 @property
3009 3007 def outdated(self):
3010 3008 return self.display_state == self.COMMENT_OUTDATED
3011 3009
3012 3010 def outdated_at_version(self, version):
3013 3011 """
3014 3012 Checks if comment is outdated for given pull request version
3015 3013 """
3016 3014 return self.outdated and self.pull_request_version_id != version
3017 3015
3018 3016 def older_than_version(self, version):
3019 3017 """
3020 3018 Checks if comment is made from previous version than given
3021 3019 """
3022 3020 if version is None:
3023 3021 return self.pull_request_version_id is not None
3024 3022
3025 3023 return self.pull_request_version_id < version
3026 3024
3027 3025 @property
3028 3026 def resolved(self):
3029 3027 return self.resolved_by[0] if self.resolved_by else None
3030 3028
3031 3029 @property
3032 3030 def is_todo(self):
3033 3031 return self.comment_type == self.COMMENT_TYPE_TODO
3034 3032
3035 3033 def get_index_version(self, versions):
3036 3034 return self.get_index_from_version(
3037 3035 self.pull_request_version_id, versions)
3038 3036
3039 3037 def render(self, mentions=False):
3040 3038 from rhodecode.lib import helpers as h
3041 3039 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3042 3040
3043 3041 def __repr__(self):
3044 3042 if self.comment_id:
3045 3043 return '<DB:Comment #%s>' % self.comment_id
3046 3044 else:
3047 3045 return '<DB:Comment at %#x>' % id(self)
3048 3046
3049 3047
3050 3048 class ChangesetStatus(Base, BaseModel):
3051 3049 __tablename__ = 'changeset_statuses'
3052 3050 __table_args__ = (
3053 3051 Index('cs_revision_idx', 'revision'),
3054 3052 Index('cs_version_idx', 'version'),
3055 3053 UniqueConstraint('repo_id', 'revision', 'version'),
3056 3054 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3057 3055 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3058 3056 )
3059 3057 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3060 3058 STATUS_APPROVED = 'approved'
3061 3059 STATUS_REJECTED = 'rejected'
3062 3060 STATUS_UNDER_REVIEW = 'under_review'
3063 3061
3064 3062 STATUSES = [
3065 3063 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3066 3064 (STATUS_APPROVED, _("Approved")),
3067 3065 (STATUS_REJECTED, _("Rejected")),
3068 3066 (STATUS_UNDER_REVIEW, _("Under Review")),
3069 3067 ]
3070 3068
3071 3069 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3072 3070 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3073 3071 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3074 3072 revision = Column('revision', String(40), nullable=False)
3075 3073 status = Column('status', String(128), nullable=False, default=DEFAULT)
3076 3074 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3077 3075 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3078 3076 version = Column('version', Integer(), nullable=False, default=0)
3079 3077 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3080 3078
3081 3079 author = relationship('User', lazy='joined')
3082 3080 repo = relationship('Repository')
3083 3081 comment = relationship('ChangesetComment', lazy='joined')
3084 3082 pull_request = relationship('PullRequest', lazy='joined')
3085 3083
3086 3084 def __unicode__(self):
3087 3085 return u"<%s('%s[v%s]:%s')>" % (
3088 3086 self.__class__.__name__,
3089 3087 self.status, self.version, self.author
3090 3088 )
3091 3089
3092 3090 @classmethod
3093 3091 def get_status_lbl(cls, value):
3094 3092 return dict(cls.STATUSES).get(value)
3095 3093
3096 3094 @property
3097 3095 def status_lbl(self):
3098 3096 return ChangesetStatus.get_status_lbl(self.status)
3099 3097
3100 3098
3101 3099 class _PullRequestBase(BaseModel):
3102 3100 """
3103 3101 Common attributes of pull request and version entries.
3104 3102 """
3105 3103
3106 3104 # .status values
3107 3105 STATUS_NEW = u'new'
3108 3106 STATUS_OPEN = u'open'
3109 3107 STATUS_CLOSED = u'closed'
3110 3108
3111 3109 title = Column('title', Unicode(255), nullable=True)
3112 3110 description = Column(
3113 3111 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3114 3112 nullable=True)
3115 3113 # new/open/closed status of pull request (not approve/reject/etc)
3116 3114 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3117 3115 created_on = Column(
3118 3116 'created_on', DateTime(timezone=False), nullable=False,
3119 3117 default=datetime.datetime.now)
3120 3118 updated_on = Column(
3121 3119 'updated_on', DateTime(timezone=False), nullable=False,
3122 3120 default=datetime.datetime.now)
3123 3121
3124 3122 @declared_attr
3125 3123 def user_id(cls):
3126 3124 return Column(
3127 3125 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3128 3126 unique=None)
3129 3127
3130 3128 # 500 revisions max
3131 3129 _revisions = Column(
3132 3130 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3133 3131
3134 3132 @declared_attr
3135 3133 def source_repo_id(cls):
3136 3134 # TODO: dan: rename column to source_repo_id
3137 3135 return Column(
3138 3136 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3139 3137 nullable=False)
3140 3138
3141 3139 source_ref = Column('org_ref', Unicode(255), nullable=False)
3142 3140
3143 3141 @declared_attr
3144 3142 def target_repo_id(cls):
3145 3143 # TODO: dan: rename column to target_repo_id
3146 3144 return Column(
3147 3145 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3148 3146 nullable=False)
3149 3147
3150 3148 target_ref = Column('other_ref', Unicode(255), nullable=False)
3151 3149 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3152 3150
3153 3151 # TODO: dan: rename column to last_merge_source_rev
3154 3152 _last_merge_source_rev = Column(
3155 3153 'last_merge_org_rev', String(40), nullable=True)
3156 3154 # TODO: dan: rename column to last_merge_target_rev
3157 3155 _last_merge_target_rev = Column(
3158 3156 'last_merge_other_rev', String(40), nullable=True)
3159 3157 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3160 3158 merge_rev = Column('merge_rev', String(40), nullable=True)
3161 3159
3162 3160 @hybrid_property
3163 3161 def revisions(self):
3164 3162 return self._revisions.split(':') if self._revisions else []
3165 3163
3166 3164 @revisions.setter
3167 3165 def revisions(self, val):
3168 3166 self._revisions = ':'.join(val)
3169 3167
3170 3168 @declared_attr
3171 3169 def author(cls):
3172 3170 return relationship('User', lazy='joined')
3173 3171
3174 3172 @declared_attr
3175 3173 def source_repo(cls):
3176 3174 return relationship(
3177 3175 'Repository',
3178 3176 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3179 3177
3180 3178 @property
3181 3179 def source_ref_parts(self):
3182 3180 return self.unicode_to_reference(self.source_ref)
3183 3181
3184 3182 @declared_attr
3185 3183 def target_repo(cls):
3186 3184 return relationship(
3187 3185 'Repository',
3188 3186 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3189 3187
3190 3188 @property
3191 3189 def target_ref_parts(self):
3192 3190 return self.unicode_to_reference(self.target_ref)
3193 3191
3194 3192 @property
3195 3193 def shadow_merge_ref(self):
3196 3194 return self.unicode_to_reference(self._shadow_merge_ref)
3197 3195
3198 3196 @shadow_merge_ref.setter
3199 3197 def shadow_merge_ref(self, ref):
3200 3198 self._shadow_merge_ref = self.reference_to_unicode(ref)
3201 3199
3202 3200 def unicode_to_reference(self, raw):
3203 3201 """
3204 3202 Convert a unicode (or string) to a reference object.
3205 3203 If unicode evaluates to False it returns None.
3206 3204 """
3207 3205 if raw:
3208 3206 refs = raw.split(':')
3209 3207 return Reference(*refs)
3210 3208 else:
3211 3209 return None
3212 3210
3213 3211 def reference_to_unicode(self, ref):
3214 3212 """
3215 3213 Convert a reference object to unicode.
3216 3214 If reference is None it returns None.
3217 3215 """
3218 3216 if ref:
3219 3217 return u':'.join(ref)
3220 3218 else:
3221 3219 return None
3222 3220
3223 3221 def get_api_data(self):
3224 3222 from rhodecode.model.pull_request import PullRequestModel
3225 3223 pull_request = self
3226 3224 merge_status = PullRequestModel().merge_status(pull_request)
3227 3225
3228 3226 pull_request_url = url(
3229 3227 'pullrequest_show', repo_name=self.target_repo.repo_name,
3230 3228 pull_request_id=self.pull_request_id, qualified=True)
3231 3229
3232 3230 merge_data = {
3233 3231 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3234 3232 'reference': (
3235 3233 pull_request.shadow_merge_ref._asdict()
3236 3234 if pull_request.shadow_merge_ref else None),
3237 3235 }
3238 3236
3239 3237 data = {
3240 3238 'pull_request_id': pull_request.pull_request_id,
3241 3239 'url': pull_request_url,
3242 3240 'title': pull_request.title,
3243 3241 'description': pull_request.description,
3244 3242 'status': pull_request.status,
3245 3243 'created_on': pull_request.created_on,
3246 3244 'updated_on': pull_request.updated_on,
3247 3245 'commit_ids': pull_request.revisions,
3248 3246 'review_status': pull_request.calculated_review_status(),
3249 3247 'mergeable': {
3250 3248 'status': merge_status[0],
3251 3249 'message': unicode(merge_status[1]),
3252 3250 },
3253 3251 'source': {
3254 3252 'clone_url': pull_request.source_repo.clone_url(),
3255 3253 'repository': pull_request.source_repo.repo_name,
3256 3254 'reference': {
3257 3255 'name': pull_request.source_ref_parts.name,
3258 3256 'type': pull_request.source_ref_parts.type,
3259 3257 'commit_id': pull_request.source_ref_parts.commit_id,
3260 3258 },
3261 3259 },
3262 3260 'target': {
3263 3261 'clone_url': pull_request.target_repo.clone_url(),
3264 3262 'repository': pull_request.target_repo.repo_name,
3265 3263 'reference': {
3266 3264 'name': pull_request.target_ref_parts.name,
3267 3265 'type': pull_request.target_ref_parts.type,
3268 3266 'commit_id': pull_request.target_ref_parts.commit_id,
3269 3267 },
3270 3268 },
3271 3269 'merge': merge_data,
3272 3270 'author': pull_request.author.get_api_data(include_secrets=False,
3273 3271 details='basic'),
3274 3272 'reviewers': [
3275 3273 {
3276 3274 'user': reviewer.get_api_data(include_secrets=False,
3277 3275 details='basic'),
3278 3276 'reasons': reasons,
3279 3277 'review_status': st[0][1].status if st else 'not_reviewed',
3280 3278 }
3281 3279 for reviewer, reasons, st in pull_request.reviewers_statuses()
3282 3280 ]
3283 3281 }
3284 3282
3285 3283 return data
3286 3284
3287 3285
3288 3286 class PullRequest(Base, _PullRequestBase):
3289 3287 __tablename__ = 'pull_requests'
3290 3288 __table_args__ = (
3291 3289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3292 3290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3293 3291 )
3294 3292
3295 3293 pull_request_id = Column(
3296 3294 'pull_request_id', Integer(), nullable=False, primary_key=True)
3297 3295
3298 3296 def __repr__(self):
3299 3297 if self.pull_request_id:
3300 3298 return '<DB:PullRequest #%s>' % self.pull_request_id
3301 3299 else:
3302 3300 return '<DB:PullRequest at %#x>' % id(self)
3303 3301
3304 3302 reviewers = relationship('PullRequestReviewers',
3305 3303 cascade="all, delete, delete-orphan")
3306 3304 statuses = relationship('ChangesetStatus')
3307 3305 comments = relationship('ChangesetComment',
3308 3306 cascade="all, delete, delete-orphan")
3309 3307 versions = relationship('PullRequestVersion',
3310 3308 cascade="all, delete, delete-orphan",
3311 3309 lazy='dynamic')
3312 3310
3313 3311 @classmethod
3314 3312 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3315 3313 internal_methods=None):
3316 3314
3317 3315 class PullRequestDisplay(object):
3318 3316 """
3319 3317 Special object wrapper for showing PullRequest data via Versions
3320 3318 It mimics PR object as close as possible. This is read only object
3321 3319 just for display
3322 3320 """
3323 3321
3324 3322 def __init__(self, attrs, internal=None):
3325 3323 self.attrs = attrs
3326 3324 # internal have priority over the given ones via attrs
3327 3325 self.internal = internal or ['versions']
3328 3326
3329 3327 def __getattr__(self, item):
3330 3328 if item in self.internal:
3331 3329 return getattr(self, item)
3332 3330 try:
3333 3331 return self.attrs[item]
3334 3332 except KeyError:
3335 3333 raise AttributeError(
3336 3334 '%s object has no attribute %s' % (self, item))
3337 3335
3338 3336 def __repr__(self):
3339 3337 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3340 3338
3341 3339 def versions(self):
3342 3340 return pull_request_obj.versions.order_by(
3343 3341 PullRequestVersion.pull_request_version_id).all()
3344 3342
3345 3343 def is_closed(self):
3346 3344 return pull_request_obj.is_closed()
3347 3345
3348 3346 @property
3349 3347 def pull_request_version_id(self):
3350 3348 return getattr(pull_request_obj, 'pull_request_version_id', None)
3351 3349
3352 3350 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3353 3351
3354 3352 attrs.author = StrictAttributeDict(
3355 3353 pull_request_obj.author.get_api_data())
3356 3354 if pull_request_obj.target_repo:
3357 3355 attrs.target_repo = StrictAttributeDict(
3358 3356 pull_request_obj.target_repo.get_api_data())
3359 3357 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3360 3358
3361 3359 if pull_request_obj.source_repo:
3362 3360 attrs.source_repo = StrictAttributeDict(
3363 3361 pull_request_obj.source_repo.get_api_data())
3364 3362 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3365 3363
3366 3364 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3367 3365 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3368 3366 attrs.revisions = pull_request_obj.revisions
3369 3367
3370 3368 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3371 3369
3372 3370 return PullRequestDisplay(attrs, internal=internal_methods)
3373 3371
3374 3372 def is_closed(self):
3375 3373 return self.status == self.STATUS_CLOSED
3376 3374
3377 3375 def __json__(self):
3378 3376 return {
3379 3377 'revisions': self.revisions,
3380 3378 }
3381 3379
3382 3380 def calculated_review_status(self):
3383 3381 from rhodecode.model.changeset_status import ChangesetStatusModel
3384 3382 return ChangesetStatusModel().calculated_review_status(self)
3385 3383
3386 3384 def reviewers_statuses(self):
3387 3385 from rhodecode.model.changeset_status import ChangesetStatusModel
3388 3386 return ChangesetStatusModel().reviewers_statuses(self)
3389 3387
3390 3388 @property
3391 3389 def workspace_id(self):
3392 3390 from rhodecode.model.pull_request import PullRequestModel
3393 3391 return PullRequestModel()._workspace_id(self)
3394 3392
3395 3393 def get_shadow_repo(self):
3396 3394 workspace_id = self.workspace_id
3397 3395 vcs_obj = self.target_repo.scm_instance()
3398 3396 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3399 3397 workspace_id)
3400 3398 return vcs_obj._get_shadow_instance(shadow_repository_path)
3401 3399
3402 3400
3403 3401 class PullRequestVersion(Base, _PullRequestBase):
3404 3402 __tablename__ = 'pull_request_versions'
3405 3403 __table_args__ = (
3406 3404 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3407 3405 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3408 3406 )
3409 3407
3410 3408 pull_request_version_id = Column(
3411 3409 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3412 3410 pull_request_id = Column(
3413 3411 'pull_request_id', Integer(),
3414 3412 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3415 3413 pull_request = relationship('PullRequest')
3416 3414
3417 3415 def __repr__(self):
3418 3416 if self.pull_request_version_id:
3419 3417 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3420 3418 else:
3421 3419 return '<DB:PullRequestVersion at %#x>' % id(self)
3422 3420
3423 3421 @property
3424 3422 def reviewers(self):
3425 3423 return self.pull_request.reviewers
3426 3424
3427 3425 @property
3428 3426 def versions(self):
3429 3427 return self.pull_request.versions
3430 3428
3431 3429 def is_closed(self):
3432 3430 # calculate from original
3433 3431 return self.pull_request.status == self.STATUS_CLOSED
3434 3432
3435 3433 def calculated_review_status(self):
3436 3434 return self.pull_request.calculated_review_status()
3437 3435
3438 3436 def reviewers_statuses(self):
3439 3437 return self.pull_request.reviewers_statuses()
3440 3438
3441 3439
3442 3440 class PullRequestReviewers(Base, BaseModel):
3443 3441 __tablename__ = 'pull_request_reviewers'
3444 3442 __table_args__ = (
3445 3443 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3446 3444 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3447 3445 )
3448 3446
3449 3447 def __init__(self, user=None, pull_request=None, reasons=None):
3450 3448 self.user = user
3451 3449 self.pull_request = pull_request
3452 3450 self.reasons = reasons or []
3453 3451
3454 3452 @hybrid_property
3455 3453 def reasons(self):
3456 3454 if not self._reasons:
3457 3455 return []
3458 3456 return self._reasons
3459 3457
3460 3458 @reasons.setter
3461 3459 def reasons(self, val):
3462 3460 val = val or []
3463 3461 if any(not isinstance(x, basestring) for x in val):
3464 3462 raise Exception('invalid reasons type, must be list of strings')
3465 3463 self._reasons = val
3466 3464
3467 3465 pull_requests_reviewers_id = Column(
3468 3466 'pull_requests_reviewers_id', Integer(), nullable=False,
3469 3467 primary_key=True)
3470 3468 pull_request_id = Column(
3471 3469 "pull_request_id", Integer(),
3472 3470 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3473 3471 user_id = Column(
3474 3472 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3475 3473 _reasons = Column(
3476 3474 'reason', MutationList.as_mutable(
3477 3475 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3478 3476
3479 3477 user = relationship('User')
3480 3478 pull_request = relationship('PullRequest')
3481 3479
3482 3480
3483 3481 class Notification(Base, BaseModel):
3484 3482 __tablename__ = 'notifications'
3485 3483 __table_args__ = (
3486 3484 Index('notification_type_idx', 'type'),
3487 3485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3488 3486 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3489 3487 )
3490 3488
3491 3489 TYPE_CHANGESET_COMMENT = u'cs_comment'
3492 3490 TYPE_MESSAGE = u'message'
3493 3491 TYPE_MENTION = u'mention'
3494 3492 TYPE_REGISTRATION = u'registration'
3495 3493 TYPE_PULL_REQUEST = u'pull_request'
3496 3494 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3497 3495
3498 3496 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3499 3497 subject = Column('subject', Unicode(512), nullable=True)
3500 3498 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3501 3499 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3502 3500 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3503 3501 type_ = Column('type', Unicode(255))
3504 3502
3505 3503 created_by_user = relationship('User')
3506 3504 notifications_to_users = relationship('UserNotification', lazy='joined',
3507 3505 cascade="all, delete, delete-orphan")
3508 3506
3509 3507 @property
3510 3508 def recipients(self):
3511 3509 return [x.user for x in UserNotification.query()\
3512 3510 .filter(UserNotification.notification == self)\
3513 3511 .order_by(UserNotification.user_id.asc()).all()]
3514 3512
3515 3513 @classmethod
3516 3514 def create(cls, created_by, subject, body, recipients, type_=None):
3517 3515 if type_ is None:
3518 3516 type_ = Notification.TYPE_MESSAGE
3519 3517
3520 3518 notification = cls()
3521 3519 notification.created_by_user = created_by
3522 3520 notification.subject = subject
3523 3521 notification.body = body
3524 3522 notification.type_ = type_
3525 3523 notification.created_on = datetime.datetime.now()
3526 3524
3527 3525 for u in recipients:
3528 3526 assoc = UserNotification()
3529 3527 assoc.notification = notification
3530 3528
3531 3529 # if created_by is inside recipients mark his notification
3532 3530 # as read
3533 3531 if u.user_id == created_by.user_id:
3534 3532 assoc.read = True
3535 3533
3536 3534 u.notifications.append(assoc)
3537 3535 Session().add(notification)
3538 3536
3539 3537 return notification
3540 3538
3541 3539 @property
3542 3540 def description(self):
3543 3541 from rhodecode.model.notification import NotificationModel
3544 3542 return NotificationModel().make_description(self)
3545 3543
3546 3544
3547 3545 class UserNotification(Base, BaseModel):
3548 3546 __tablename__ = 'user_to_notification'
3549 3547 __table_args__ = (
3550 3548 UniqueConstraint('user_id', 'notification_id'),
3551 3549 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3552 3550 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3553 3551 )
3554 3552 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3555 3553 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3556 3554 read = Column('read', Boolean, default=False)
3557 3555 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3558 3556
3559 3557 user = relationship('User', lazy="joined")
3560 3558 notification = relationship('Notification', lazy="joined",
3561 3559 order_by=lambda: Notification.created_on.desc(),)
3562 3560
3563 3561 def mark_as_read(self):
3564 3562 self.read = True
3565 3563 Session().add(self)
3566 3564
3567 3565
3568 3566 class Gist(Base, BaseModel):
3569 3567 __tablename__ = 'gists'
3570 3568 __table_args__ = (
3571 3569 Index('g_gist_access_id_idx', 'gist_access_id'),
3572 3570 Index('g_created_on_idx', 'created_on'),
3573 3571 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3574 3572 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3575 3573 )
3576 3574 GIST_PUBLIC = u'public'
3577 3575 GIST_PRIVATE = u'private'
3578 3576 DEFAULT_FILENAME = u'gistfile1.txt'
3579 3577
3580 3578 ACL_LEVEL_PUBLIC = u'acl_public'
3581 3579 ACL_LEVEL_PRIVATE = u'acl_private'
3582 3580
3583 3581 gist_id = Column('gist_id', Integer(), primary_key=True)
3584 3582 gist_access_id = Column('gist_access_id', Unicode(250))
3585 3583 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3586 3584 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3587 3585 gist_expires = Column('gist_expires', Float(53), nullable=False)
3588 3586 gist_type = Column('gist_type', Unicode(128), nullable=False)
3589 3587 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3590 3588 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3591 3589 acl_level = Column('acl_level', Unicode(128), nullable=True)
3592 3590
3593 3591 owner = relationship('User')
3594 3592
3595 3593 def __repr__(self):
3596 3594 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3597 3595
3598 3596 @classmethod
3599 3597 def get_or_404(cls, id_):
3600 3598 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3601 3599 if not res:
3602 3600 raise HTTPNotFound
3603 3601 return res
3604 3602
3605 3603 @classmethod
3606 3604 def get_by_access_id(cls, gist_access_id):
3607 3605 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3608 3606
3609 3607 def gist_url(self):
3610 3608 import rhodecode
3611 3609 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3612 3610 if alias_url:
3613 3611 return alias_url.replace('{gistid}', self.gist_access_id)
3614 3612
3615 3613 return url('gist', gist_id=self.gist_access_id, qualified=True)
3616 3614
3617 3615 @classmethod
3618 3616 def base_path(cls):
3619 3617 """
3620 3618 Returns base path when all gists are stored
3621 3619
3622 3620 :param cls:
3623 3621 """
3624 3622 from rhodecode.model.gist import GIST_STORE_LOC
3625 3623 q = Session().query(RhodeCodeUi)\
3626 3624 .filter(RhodeCodeUi.ui_key == URL_SEP)
3627 3625 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3628 3626 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3629 3627
3630 3628 def get_api_data(self):
3631 3629 """
3632 3630 Common function for generating gist related data for API
3633 3631 """
3634 3632 gist = self
3635 3633 data = {
3636 3634 'gist_id': gist.gist_id,
3637 3635 'type': gist.gist_type,
3638 3636 'access_id': gist.gist_access_id,
3639 3637 'description': gist.gist_description,
3640 3638 'url': gist.gist_url(),
3641 3639 'expires': gist.gist_expires,
3642 3640 'created_on': gist.created_on,
3643 3641 'modified_at': gist.modified_at,
3644 3642 'content': None,
3645 3643 'acl_level': gist.acl_level,
3646 3644 }
3647 3645 return data
3648 3646
3649 3647 def __json__(self):
3650 3648 data = dict(
3651 3649 )
3652 3650 data.update(self.get_api_data())
3653 3651 return data
3654 3652 # SCM functions
3655 3653
3656 3654 def scm_instance(self, **kwargs):
3657 3655 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3658 3656 return get_vcs_instance(
3659 3657 repo_path=safe_str(full_repo_path), create=False)
3660 3658
3661 3659
3662 3660 class ExternalIdentity(Base, BaseModel):
3663 3661 __tablename__ = 'external_identities'
3664 3662 __table_args__ = (
3665 3663 Index('local_user_id_idx', 'local_user_id'),
3666 3664 Index('external_id_idx', 'external_id'),
3667 3665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3668 3666 'mysql_charset': 'utf8'})
3669 3667
3670 3668 external_id = Column('external_id', Unicode(255), default=u'',
3671 3669 primary_key=True)
3672 3670 external_username = Column('external_username', Unicode(1024), default=u'')
3673 3671 local_user_id = Column('local_user_id', Integer(),
3674 3672 ForeignKey('users.user_id'), primary_key=True)
3675 3673 provider_name = Column('provider_name', Unicode(255), default=u'',
3676 3674 primary_key=True)
3677 3675 access_token = Column('access_token', String(1024), default=u'')
3678 3676 alt_token = Column('alt_token', String(1024), default=u'')
3679 3677 token_secret = Column('token_secret', String(1024), default=u'')
3680 3678
3681 3679 @classmethod
3682 3680 def by_external_id_and_provider(cls, external_id, provider_name,
3683 3681 local_user_id=None):
3684 3682 """
3685 3683 Returns ExternalIdentity instance based on search params
3686 3684
3687 3685 :param external_id:
3688 3686 :param provider_name:
3689 3687 :return: ExternalIdentity
3690 3688 """
3691 3689 query = cls.query()
3692 3690 query = query.filter(cls.external_id == external_id)
3693 3691 query = query.filter(cls.provider_name == provider_name)
3694 3692 if local_user_id:
3695 3693 query = query.filter(cls.local_user_id == local_user_id)
3696 3694 return query.first()
3697 3695
3698 3696 @classmethod
3699 3697 def user_by_external_id_and_provider(cls, external_id, provider_name):
3700 3698 """
3701 3699 Returns User instance based on search params
3702 3700
3703 3701 :param external_id:
3704 3702 :param provider_name:
3705 3703 :return: User
3706 3704 """
3707 3705 query = User.query()
3708 3706 query = query.filter(cls.external_id == external_id)
3709 3707 query = query.filter(cls.provider_name == provider_name)
3710 3708 query = query.filter(User.user_id == cls.local_user_id)
3711 3709 return query.first()
3712 3710
3713 3711 @classmethod
3714 3712 def by_local_user_id(cls, local_user_id):
3715 3713 """
3716 3714 Returns all tokens for user
3717 3715
3718 3716 :param local_user_id:
3719 3717 :return: ExternalIdentity
3720 3718 """
3721 3719 query = cls.query()
3722 3720 query = query.filter(cls.local_user_id == local_user_id)
3723 3721 return query
3724 3722
3725 3723
3726 3724 class Integration(Base, BaseModel):
3727 3725 __tablename__ = 'integrations'
3728 3726 __table_args__ = (
3729 3727 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3730 3728 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3731 3729 )
3732 3730
3733 3731 integration_id = Column('integration_id', Integer(), primary_key=True)
3734 3732 integration_type = Column('integration_type', String(255))
3735 3733 enabled = Column('enabled', Boolean(), nullable=False)
3736 3734 name = Column('name', String(255), nullable=False)
3737 3735 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3738 3736 default=False)
3739 3737
3740 3738 settings = Column(
3741 3739 'settings_json', MutationObj.as_mutable(
3742 3740 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3743 3741 repo_id = Column(
3744 3742 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3745 3743 nullable=True, unique=None, default=None)
3746 3744 repo = relationship('Repository', lazy='joined')
3747 3745
3748 3746 repo_group_id = Column(
3749 3747 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3750 3748 nullable=True, unique=None, default=None)
3751 3749 repo_group = relationship('RepoGroup', lazy='joined')
3752 3750
3753 3751 @property
3754 3752 def scope(self):
3755 3753 if self.repo:
3756 3754 return repr(self.repo)
3757 3755 if self.repo_group:
3758 3756 if self.child_repos_only:
3759 3757 return repr(self.repo_group) + ' (child repos only)'
3760 3758 else:
3761 3759 return repr(self.repo_group) + ' (recursive)'
3762 3760 if self.child_repos_only:
3763 3761 return 'root_repos'
3764 3762 return 'global'
3765 3763
3766 3764 def __repr__(self):
3767 3765 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3768 3766
3769 3767
3770 3768 class RepoReviewRuleUser(Base, BaseModel):
3771 3769 __tablename__ = 'repo_review_rules_users'
3772 3770 __table_args__ = (
3773 3771 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3774 3772 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3775 3773 )
3776 3774 repo_review_rule_user_id = Column(
3777 3775 'repo_review_rule_user_id', Integer(), primary_key=True)
3778 3776 repo_review_rule_id = Column("repo_review_rule_id",
3779 3777 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3780 3778 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3781 3779 nullable=False)
3782 3780 user = relationship('User')
3783 3781
3784 3782
3785 3783 class RepoReviewRuleUserGroup(Base, BaseModel):
3786 3784 __tablename__ = 'repo_review_rules_users_groups'
3787 3785 __table_args__ = (
3788 3786 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3789 3787 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3790 3788 )
3791 3789 repo_review_rule_users_group_id = Column(
3792 3790 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3793 3791 repo_review_rule_id = Column("repo_review_rule_id",
3794 3792 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3795 3793 users_group_id = Column("users_group_id", Integer(),
3796 3794 ForeignKey('users_groups.users_group_id'), nullable=False)
3797 3795 users_group = relationship('UserGroup')
3798 3796
3799 3797
3800 3798 class RepoReviewRule(Base, BaseModel):
3801 3799 __tablename__ = 'repo_review_rules'
3802 3800 __table_args__ = (
3803 3801 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3804 3802 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3805 3803 )
3806 3804
3807 3805 repo_review_rule_id = Column(
3808 3806 'repo_review_rule_id', Integer(), primary_key=True)
3809 3807 repo_id = Column(
3810 3808 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3811 3809 repo = relationship('Repository', backref='review_rules')
3812 3810
3813 3811 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3814 3812 default=u'*') # glob
3815 3813 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3816 3814 default=u'*') # glob
3817 3815
3818 3816 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3819 3817 nullable=False, default=False)
3820 3818 rule_users = relationship('RepoReviewRuleUser')
3821 3819 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3822 3820
3823 3821 @hybrid_property
3824 3822 def branch_pattern(self):
3825 3823 return self._branch_pattern or '*'
3826 3824
3827 3825 def _validate_glob(self, value):
3828 3826 re.compile('^' + glob2re(value) + '$')
3829 3827
3830 3828 @branch_pattern.setter
3831 3829 def branch_pattern(self, value):
3832 3830 self._validate_glob(value)
3833 3831 self._branch_pattern = value or '*'
3834 3832
3835 3833 @hybrid_property
3836 3834 def file_pattern(self):
3837 3835 return self._file_pattern or '*'
3838 3836
3839 3837 @file_pattern.setter
3840 3838 def file_pattern(self, value):
3841 3839 self._validate_glob(value)
3842 3840 self._file_pattern = value or '*'
3843 3841
3844 3842 def matches(self, branch, files_changed):
3845 3843 """
3846 3844 Check if this review rule matches a branch/files in a pull request
3847 3845
3848 3846 :param branch: branch name for the commit
3849 3847 :param files_changed: list of file paths changed in the pull request
3850 3848 """
3851 3849
3852 3850 branch = branch or ''
3853 3851 files_changed = files_changed or []
3854 3852
3855 3853 branch_matches = True
3856 3854 if branch:
3857 3855 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3858 3856 branch_matches = bool(branch_regex.search(branch))
3859 3857
3860 3858 files_matches = True
3861 3859 if self.file_pattern != '*':
3862 3860 files_matches = False
3863 3861 file_regex = re.compile(glob2re(self.file_pattern))
3864 3862 for filename in files_changed:
3865 3863 if file_regex.search(filename):
3866 3864 files_matches = True
3867 3865 break
3868 3866
3869 3867 return branch_matches and files_matches
3870 3868
3871 3869 @property
3872 3870 def review_users(self):
3873 3871 """ Returns the users which this rule applies to """
3874 3872
3875 3873 users = set()
3876 3874 users |= set([
3877 3875 rule_user.user for rule_user in self.rule_users
3878 3876 if rule_user.user.active])
3879 3877 users |= set(
3880 3878 member.user
3881 3879 for rule_user_group in self.rule_user_groups
3882 3880 for member in rule_user_group.users_group.members
3883 3881 if member.user.active
3884 3882 )
3885 3883 return users
3886 3884
3887 3885 def __repr__(self):
3888 3886 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3889 3887 self.repo_review_rule_id, self.repo)
3890 3888
3891 3889
3892 3890 class DbMigrateVersion(Base, BaseModel):
3893 3891 __tablename__ = 'db_migrate_version'
3894 3892 __table_args__ = (
3895 3893 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3896 3894 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3897 3895 )
3898 3896 repository_id = Column('repository_id', String(250), primary_key=True)
3899 3897 repository_path = Column('repository_path', Text)
3900 3898 version = Column('version', Integer)
3901 3899
3902 3900
3903 3901 class DbSession(Base, BaseModel):
3904 3902 __tablename__ = 'db_session'
3905 3903 __table_args__ = (
3906 3904 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3907 3905 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3908 3906 )
3909 3907
3910 3908 def __repr__(self):
3911 3909 return '<DB:DbSession({})>'.format(self.id)
3912 3910
3913 3911 id = Column('id', Integer())
3914 3912 namespace = Column('namespace', String(255), primary_key=True)
3915 3913 accessed = Column('accessed', DateTime, nullable=False)
3916 3914 created = Column('created', DateTime, nullable=False)
3917 3915 data = Column('data', PickleType, nullable=False)
@@ -1,852 +1,849 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict, str2bool)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
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(FromCache("sql_cache_short",
61 61 "get_user_%s" % 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 get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 if 'create_repo_group' in form_data:
108 108 user_data['create_repo_group'] = str2bool(
109 109 form_data.get('create_repo_group'))
110 110
111 111 try:
112 112 if form_data.get('password_change'):
113 113 user_data['force_password_change'] = True
114 114 return UserModel().create_or_update(**user_data)
115 115 except Exception:
116 116 log.error(traceback.format_exc())
117 117 raise
118 118
119 119 def update_user(self, user, skip_attrs=None, **kwargs):
120 120 from rhodecode.lib.auth import get_crypt_password
121 121
122 122 user = self._get_user(user)
123 123 if user.username == User.DEFAULT_USER:
124 124 raise DefaultUserException(
125 125 _("You can't Edit this user since it's"
126 126 " crucial for entire application"))
127 127
128 128 # first store only defaults
129 129 user_attrs = {
130 130 'updating_user_id': user.user_id,
131 131 'username': user.username,
132 132 'password': user.password,
133 133 'email': user.email,
134 134 'firstname': user.name,
135 135 'lastname': user.lastname,
136 136 'active': user.active,
137 137 'admin': user.admin,
138 138 'extern_name': user.extern_name,
139 139 'extern_type': user.extern_type,
140 140 'language': user.user_data.get('language')
141 141 }
142 142
143 143 # in case there's new_password, that comes from form, use it to
144 144 # store password
145 145 if kwargs.get('new_password'):
146 146 kwargs['password'] = kwargs['new_password']
147 147
148 148 # cleanups, my_account password change form
149 149 kwargs.pop('current_password', None)
150 150 kwargs.pop('new_password', None)
151 151
152 152 # cleanups, user edit password change form
153 153 kwargs.pop('password_confirmation', None)
154 154 kwargs.pop('password_change', None)
155 155
156 156 # create repo group on user creation
157 157 kwargs.pop('create_repo_group', None)
158 158
159 159 # legacy forms send name, which is the firstname
160 160 firstname = kwargs.pop('name', None)
161 161 if firstname:
162 162 kwargs['firstname'] = firstname
163 163
164 164 for k, v in kwargs.items():
165 165 # skip if we don't want to update this
166 166 if skip_attrs and k in skip_attrs:
167 167 continue
168 168
169 169 user_attrs[k] = v
170 170
171 171 try:
172 172 return self.create_or_update(**user_attrs)
173 173 except Exception:
174 174 log.error(traceback.format_exc())
175 175 raise
176 176
177 177 def create_or_update(
178 178 self, username, password, email, firstname='', lastname='',
179 179 active=True, admin=False, extern_type=None, extern_name=None,
180 180 cur_user=None, plugin=None, force_password_change=False,
181 181 allow_to_create_user=True, create_repo_group=None,
182 182 updating_user_id=None, language=None, strict_creation_check=True):
183 183 """
184 184 Creates a new instance if not found, or updates current one
185 185
186 186 :param username:
187 187 :param password:
188 188 :param email:
189 189 :param firstname:
190 190 :param lastname:
191 191 :param active:
192 192 :param admin:
193 193 :param extern_type:
194 194 :param extern_name:
195 195 :param cur_user:
196 196 :param plugin: optional plugin this method was called from
197 197 :param force_password_change: toggles new or existing user flag
198 198 for password change
199 199 :param allow_to_create_user: Defines if the method can actually create
200 200 new users
201 201 :param create_repo_group: Defines if the method should also
202 202 create an repo group with user name, and owner
203 203 :param updating_user_id: if we set it up this is the user we want to
204 204 update this allows to editing username.
205 205 :param language: language of user from interface.
206 206
207 207 :returns: new User object with injected `is_new_user` attribute.
208 208 """
209 209 if not cur_user:
210 210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 211
212 212 from rhodecode.lib.auth import (
213 213 get_crypt_password, check_password, generate_auth_token)
214 214 from rhodecode.lib.hooks_base import (
215 215 log_create_user, check_allowed_create_user)
216 216
217 217 def _password_change(new_user, password):
218 218 # empty password
219 219 if not new_user.password:
220 220 return False
221 221
222 222 # password check is only needed for RhodeCode internal auth calls
223 223 # in case it's a plugin we don't care
224 224 if not plugin:
225 225
226 226 # first check if we gave crypted password back, and if it
227 227 # matches it's not password change
228 228 if new_user.password == password:
229 229 return False
230 230
231 231 password_match = check_password(password, new_user.password)
232 232 if not password_match:
233 233 return True
234 234
235 235 return False
236 236
237 237 # read settings on default personal repo group creation
238 238 if create_repo_group is None:
239 239 default_create_repo_group = RepoGroupModel()\
240 240 .get_default_create_personal_repo_group()
241 241 create_repo_group = default_create_repo_group
242 242
243 243 user_data = {
244 244 'username': username,
245 245 'password': password,
246 246 'email': email,
247 247 'firstname': firstname,
248 248 'lastname': lastname,
249 249 'active': active,
250 250 'admin': admin
251 251 }
252 252
253 253 if updating_user_id:
254 254 log.debug('Checking for existing account in RhodeCode '
255 255 'database with user_id `%s` ' % (updating_user_id,))
256 256 user = User.get(updating_user_id)
257 257 else:
258 258 log.debug('Checking for existing account in RhodeCode '
259 259 'database with username `%s` ' % (username,))
260 260 user = User.get_by_username(username, case_insensitive=True)
261 261
262 262 if user is None:
263 263 # we check internal flag if this method is actually allowed to
264 264 # create new user
265 265 if not allow_to_create_user:
266 266 msg = ('Method wants to create new user, but it is not '
267 267 'allowed to do so')
268 268 log.warning(msg)
269 269 raise NotAllowedToCreateUserError(msg)
270 270
271 271 log.debug('Creating new user %s', username)
272 272
273 273 # only if we create user that is active
274 274 new_active_user = active
275 275 if new_active_user and strict_creation_check:
276 276 # raises UserCreationError if it's not allowed for any reason to
277 277 # create new active user, this also executes pre-create hooks
278 278 check_allowed_create_user(user_data, cur_user, strict_check=True)
279 279 events.trigger(events.UserPreCreate(user_data))
280 280 new_user = User()
281 281 edit = False
282 282 else:
283 283 log.debug('updating user %s', username)
284 284 events.trigger(events.UserPreUpdate(user, user_data))
285 285 new_user = user
286 286 edit = True
287 287
288 288 # we're not allowed to edit default user
289 289 if user.username == User.DEFAULT_USER:
290 290 raise DefaultUserException(
291 291 _("You can't edit this user (`%(username)s`) since it's "
292 292 "crucial for entire application") % {'username': user.username})
293 293
294 294 # inject special attribute that will tell us if User is new or old
295 295 new_user.is_new_user = not edit
296 296 # for users that didn's specify auth type, we use RhodeCode built in
297 297 from rhodecode.authentication.plugins import auth_rhodecode
298 298 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
299 299 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
300 300
301 301 try:
302 302 new_user.username = username
303 303 new_user.admin = admin
304 304 new_user.email = email
305 305 new_user.active = active
306 306 new_user.extern_name = safe_unicode(extern_name)
307 307 new_user.extern_type = safe_unicode(extern_type)
308 308 new_user.name = firstname
309 309 new_user.lastname = lastname
310 310
311 if not edit:
312 new_user.api_key = generate_auth_token(username)
313
314 311 # set password only if creating an user or password is changed
315 312 if not edit or _password_change(new_user, password):
316 313 reason = 'new password' if edit else 'new user'
317 314 log.debug('Updating password reason=>%s', reason)
318 315 new_user.password = get_crypt_password(password) if password else None
319 316
320 317 if force_password_change:
321 318 new_user.update_userdata(force_password_change=True)
322 319 if language:
323 320 new_user.update_userdata(language=language)
324 321 new_user.update_userdata(notification_status=True)
325 322
326 323 self.sa.add(new_user)
327 324
328 325 if not edit and create_repo_group:
329 326 RepoGroupModel().create_personal_repo_group(
330 327 new_user, commit_early=False)
331 328
332 329 if not edit:
333 330 # add the RSS token
334 331 AuthTokenModel().create(username,
335 332 description='Generated feed token',
336 333 role=AuthTokenModel.cls.ROLE_FEED)
337 334 log_create_user(created_by=cur_user, **new_user.get_dict())
338 335 events.trigger(events.UserPostCreate(user_data))
339 336 return new_user
340 337 except (DatabaseError,):
341 338 log.error(traceback.format_exc())
342 339 raise
343 340
344 341 def create_registration(self, form_data):
345 342 from rhodecode.model.notification import NotificationModel
346 343 from rhodecode.model.notification import EmailNotificationModel
347 344
348 345 try:
349 346 form_data['admin'] = False
350 347 form_data['extern_name'] = 'rhodecode'
351 348 form_data['extern_type'] = 'rhodecode'
352 349 new_user = self.create(form_data)
353 350
354 351 self.sa.add(new_user)
355 352 self.sa.flush()
356 353
357 354 user_data = new_user.get_dict()
358 355 kwargs = {
359 356 # use SQLALCHEMY safe dump of user data
360 357 'user': AttributeDict(user_data),
361 358 'date': datetime.datetime.now()
362 359 }
363 360 notification_type = EmailNotificationModel.TYPE_REGISTRATION
364 361 # pre-generate the subject for notification itself
365 362 (subject,
366 363 _h, _e, # we don't care about those
367 364 body_plaintext) = EmailNotificationModel().render_email(
368 365 notification_type, **kwargs)
369 366
370 367 # create notification objects, and emails
371 368 NotificationModel().create(
372 369 created_by=new_user,
373 370 notification_subject=subject,
374 371 notification_body=body_plaintext,
375 372 notification_type=notification_type,
376 373 recipients=None, # all admins
377 374 email_kwargs=kwargs,
378 375 )
379 376
380 377 return new_user
381 378 except Exception:
382 379 log.error(traceback.format_exc())
383 380 raise
384 381
385 382 def _handle_user_repos(self, username, repositories, handle_mode=None):
386 383 _superadmin = self.cls.get_first_super_admin()
387 384 left_overs = True
388 385
389 386 from rhodecode.model.repo import RepoModel
390 387
391 388 if handle_mode == 'detach':
392 389 for obj in repositories:
393 390 obj.user = _superadmin
394 391 # set description we know why we super admin now owns
395 392 # additional repositories that were orphaned !
396 393 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
397 394 self.sa.add(obj)
398 395 left_overs = False
399 396 elif handle_mode == 'delete':
400 397 for obj in repositories:
401 398 RepoModel().delete(obj, forks='detach')
402 399 left_overs = False
403 400
404 401 # if nothing is done we have left overs left
405 402 return left_overs
406 403
407 404 def _handle_user_repo_groups(self, username, repository_groups,
408 405 handle_mode=None):
409 406 _superadmin = self.cls.get_first_super_admin()
410 407 left_overs = True
411 408
412 409 from rhodecode.model.repo_group import RepoGroupModel
413 410
414 411 if handle_mode == 'detach':
415 412 for r in repository_groups:
416 413 r.user = _superadmin
417 414 # set description we know why we super admin now owns
418 415 # additional repositories that were orphaned !
419 416 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
420 417 self.sa.add(r)
421 418 left_overs = False
422 419 elif handle_mode == 'delete':
423 420 for r in repository_groups:
424 421 RepoGroupModel().delete(r)
425 422 left_overs = False
426 423
427 424 # if nothing is done we have left overs left
428 425 return left_overs
429 426
430 427 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
431 428 _superadmin = self.cls.get_first_super_admin()
432 429 left_overs = True
433 430
434 431 from rhodecode.model.user_group import UserGroupModel
435 432
436 433 if handle_mode == 'detach':
437 434 for r in user_groups:
438 435 for user_user_group_to_perm in r.user_user_group_to_perm:
439 436 if user_user_group_to_perm.user.username == username:
440 437 user_user_group_to_perm.user = _superadmin
441 438 r.user = _superadmin
442 439 # set description we know why we super admin now owns
443 440 # additional repositories that were orphaned !
444 441 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
445 442 self.sa.add(r)
446 443 left_overs = False
447 444 elif handle_mode == 'delete':
448 445 for r in user_groups:
449 446 UserGroupModel().delete(r)
450 447 left_overs = False
451 448
452 449 # if nothing is done we have left overs left
453 450 return left_overs
454 451
455 452 def delete(self, user, cur_user=None, handle_repos=None,
456 453 handle_repo_groups=None, handle_user_groups=None):
457 454 if not cur_user:
458 455 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
459 456 user = self._get_user(user)
460 457
461 458 try:
462 459 if user.username == User.DEFAULT_USER:
463 460 raise DefaultUserException(
464 461 _(u"You can't remove this user since it's"
465 462 u" crucial for entire application"))
466 463
467 464 left_overs = self._handle_user_repos(
468 465 user.username, user.repositories, handle_repos)
469 466 if left_overs and user.repositories:
470 467 repos = [x.repo_name for x in user.repositories]
471 468 raise UserOwnsReposException(
472 469 _(u'user "%s" still owns %s repositories and cannot be '
473 470 u'removed. Switch owners or remove those repositories:%s')
474 471 % (user.username, len(repos), ', '.join(repos)))
475 472
476 473 left_overs = self._handle_user_repo_groups(
477 474 user.username, user.repository_groups, handle_repo_groups)
478 475 if left_overs and user.repository_groups:
479 476 repo_groups = [x.group_name for x in user.repository_groups]
480 477 raise UserOwnsRepoGroupsException(
481 478 _(u'user "%s" still owns %s repository groups and cannot be '
482 479 u'removed. Switch owners or remove those repository groups:%s')
483 480 % (user.username, len(repo_groups), ', '.join(repo_groups)))
484 481
485 482 left_overs = self._handle_user_user_groups(
486 483 user.username, user.user_groups, handle_user_groups)
487 484 if left_overs and user.user_groups:
488 485 user_groups = [x.users_group_name for x in user.user_groups]
489 486 raise UserOwnsUserGroupsException(
490 487 _(u'user "%s" still owns %s user groups and cannot be '
491 488 u'removed. Switch owners or remove those user groups:%s')
492 489 % (user.username, len(user_groups), ', '.join(user_groups)))
493 490
494 491 # we might change the user data with detach/delete, make sure
495 492 # the object is marked as expired before actually deleting !
496 493 self.sa.expire(user)
497 494 self.sa.delete(user)
498 495 from rhodecode.lib.hooks_base import log_delete_user
499 496 log_delete_user(deleted_by=cur_user, **user.get_dict())
500 497 except Exception:
501 498 log.error(traceback.format_exc())
502 499 raise
503 500
504 501 def reset_password_link(self, data, pwd_reset_url):
505 502 from rhodecode.lib.celerylib import tasks, run_task
506 503 from rhodecode.model.notification import EmailNotificationModel
507 504 user_email = data['email']
508 505 try:
509 506 user = User.get_by_email(user_email)
510 507 if user:
511 508 log.debug('password reset user found %s', user)
512 509
513 510 email_kwargs = {
514 511 'password_reset_url': pwd_reset_url,
515 512 'user': user,
516 513 'email': user_email,
517 514 'date': datetime.datetime.now()
518 515 }
519 516
520 517 (subject, headers, email_body,
521 518 email_body_plaintext) = EmailNotificationModel().render_email(
522 519 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
523 520
524 521 recipients = [user_email]
525 522
526 523 action_logger_generic(
527 524 'sending password reset email to user: {}'.format(
528 525 user), namespace='security.password_reset')
529 526
530 527 run_task(tasks.send_email, recipients, subject,
531 528 email_body_plaintext, email_body)
532 529
533 530 else:
534 531 log.debug("password reset email %s not found", user_email)
535 532 except Exception:
536 533 log.error(traceback.format_exc())
537 534 return False
538 535
539 536 return True
540 537
541 538 def reset_password(self, data):
542 539 from rhodecode.lib.celerylib import tasks, run_task
543 540 from rhodecode.model.notification import EmailNotificationModel
544 541 from rhodecode.lib import auth
545 542 user_email = data['email']
546 543 pre_db = True
547 544 try:
548 545 user = User.get_by_email(user_email)
549 546 new_passwd = auth.PasswordGenerator().gen_password(
550 547 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
551 548 if user:
552 549 user.password = auth.get_crypt_password(new_passwd)
553 550 # also force this user to reset his password !
554 551 user.update_userdata(force_password_change=True)
555 552
556 553 Session().add(user)
557 554
558 555 # now delete the token in question
559 556 UserApiKeys = AuthTokenModel.cls
560 557 UserApiKeys().query().filter(
561 558 UserApiKeys.api_key == data['token']).delete()
562 559
563 560 Session().commit()
564 561 log.info('successfully reset password for `%s`', user_email)
565 562
566 563 if new_passwd is None:
567 564 raise Exception('unable to generate new password')
568 565
569 566 pre_db = False
570 567
571 568 email_kwargs = {
572 569 'new_password': new_passwd,
573 570 'user': user,
574 571 'email': user_email,
575 572 'date': datetime.datetime.now()
576 573 }
577 574
578 575 (subject, headers, email_body,
579 576 email_body_plaintext) = EmailNotificationModel().render_email(
580 577 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
581 578 **email_kwargs)
582 579
583 580 recipients = [user_email]
584 581
585 582 action_logger_generic(
586 583 'sent new password to user: {} with email: {}'.format(
587 584 user, user_email), namespace='security.password_reset')
588 585
589 586 run_task(tasks.send_email, recipients, subject,
590 587 email_body_plaintext, email_body)
591 588
592 589 except Exception:
593 590 log.error('Failed to update user password')
594 591 log.error(traceback.format_exc())
595 592 if pre_db:
596 593 # we rollback only if local db stuff fails. If it goes into
597 594 # run_task, we're pass rollback state this wouldn't work then
598 595 Session().rollback()
599 596
600 597 return True
601 598
602 599 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
603 600 """
604 601 Fetches auth_user by user_id,or api_key if present.
605 602 Fills auth_user attributes with those taken from database.
606 603 Additionally set's is_authenitated if lookup fails
607 604 present in database
608 605
609 606 :param auth_user: instance of user to set attributes
610 607 :param user_id: user id to fetch by
611 608 :param api_key: api key to fetch by
612 609 :param username: username to fetch by
613 610 """
614 611 if user_id is None and api_key is None and username is None:
615 612 raise Exception('You need to pass user_id, api_key or username')
616 613
617 614 log.debug(
618 615 'doing fill data based on: user_id:%s api_key:%s username:%s',
619 616 user_id, api_key, username)
620 617 try:
621 618 dbuser = None
622 619 if user_id:
623 620 dbuser = self.get(user_id)
624 621 elif api_key:
625 622 dbuser = self.get_by_auth_token(api_key)
626 623 elif username:
627 624 dbuser = self.get_by_username(username)
628 625
629 626 if not dbuser:
630 627 log.warning(
631 628 'Unable to lookup user by id:%s api_key:%s username:%s',
632 629 user_id, api_key, username)
633 630 return False
634 631 if not dbuser.active:
635 632 log.debug('User `%s` is inactive, skipping fill data', username)
636 633 return False
637 634
638 635 log.debug('filling user:%s data', dbuser)
639 636
640 637 # TODO: johbo: Think about this and find a clean solution
641 638 user_data = dbuser.get_dict()
642 639 user_data.update(dbuser.get_api_data(include_secrets=True))
643 640
644 641 for k, v in user_data.iteritems():
645 642 # properties of auth user we dont update
646 643 if k not in ['auth_tokens', 'permissions']:
647 644 setattr(auth_user, k, v)
648 645
649 646 # few extras
650 647 setattr(auth_user, 'feed_token', dbuser.feed_token)
651 648 except Exception:
652 649 log.error(traceback.format_exc())
653 650 auth_user.is_authenticated = False
654 651 return False
655 652
656 653 return True
657 654
658 655 def has_perm(self, user, perm):
659 656 perm = self._get_perm(perm)
660 657 user = self._get_user(user)
661 658
662 659 return UserToPerm.query().filter(UserToPerm.user == user)\
663 660 .filter(UserToPerm.permission == perm).scalar() is not None
664 661
665 662 def grant_perm(self, user, perm):
666 663 """
667 664 Grant user global permissions
668 665
669 666 :param user:
670 667 :param perm:
671 668 """
672 669 user = self._get_user(user)
673 670 perm = self._get_perm(perm)
674 671 # if this permission is already granted skip it
675 672 _perm = UserToPerm.query()\
676 673 .filter(UserToPerm.user == user)\
677 674 .filter(UserToPerm.permission == perm)\
678 675 .scalar()
679 676 if _perm:
680 677 return
681 678 new = UserToPerm()
682 679 new.user = user
683 680 new.permission = perm
684 681 self.sa.add(new)
685 682 return new
686 683
687 684 def revoke_perm(self, user, perm):
688 685 """
689 686 Revoke users global permissions
690 687
691 688 :param user:
692 689 :param perm:
693 690 """
694 691 user = self._get_user(user)
695 692 perm = self._get_perm(perm)
696 693
697 694 obj = UserToPerm.query()\
698 695 .filter(UserToPerm.user == user)\
699 696 .filter(UserToPerm.permission == perm)\
700 697 .scalar()
701 698 if obj:
702 699 self.sa.delete(obj)
703 700
704 701 def add_extra_email(self, user, email):
705 702 """
706 703 Adds email address to UserEmailMap
707 704
708 705 :param user:
709 706 :param email:
710 707 """
711 708 from rhodecode.model import forms
712 709 form = forms.UserExtraEmailForm()()
713 710 data = form.to_python({'email': email})
714 711 user = self._get_user(user)
715 712
716 713 obj = UserEmailMap()
717 714 obj.user = user
718 715 obj.email = data['email']
719 716 self.sa.add(obj)
720 717 return obj
721 718
722 719 def delete_extra_email(self, user, email_id):
723 720 """
724 721 Removes email address from UserEmailMap
725 722
726 723 :param user:
727 724 :param email_id:
728 725 """
729 726 user = self._get_user(user)
730 727 obj = UserEmailMap.query().get(email_id)
731 728 if obj:
732 729 self.sa.delete(obj)
733 730
734 731 def parse_ip_range(self, ip_range):
735 732 ip_list = []
736 733 def make_unique(value):
737 734 seen = []
738 735 return [c for c in value if not (c in seen or seen.append(c))]
739 736
740 737 # firsts split by commas
741 738 for ip_range in ip_range.split(','):
742 739 if not ip_range:
743 740 continue
744 741 ip_range = ip_range.strip()
745 742 if '-' in ip_range:
746 743 start_ip, end_ip = ip_range.split('-', 1)
747 744 start_ip = ipaddress.ip_address(start_ip.strip())
748 745 end_ip = ipaddress.ip_address(end_ip.strip())
749 746 parsed_ip_range = []
750 747
751 748 for index in xrange(int(start_ip), int(end_ip) + 1):
752 749 new_ip = ipaddress.ip_address(index)
753 750 parsed_ip_range.append(str(new_ip))
754 751 ip_list.extend(parsed_ip_range)
755 752 else:
756 753 ip_list.append(ip_range)
757 754
758 755 return make_unique(ip_list)
759 756
760 757 def add_extra_ip(self, user, ip, description=None):
761 758 """
762 759 Adds ip address to UserIpMap
763 760
764 761 :param user:
765 762 :param ip:
766 763 """
767 764 from rhodecode.model import forms
768 765 form = forms.UserExtraIpForm()()
769 766 data = form.to_python({'ip': ip})
770 767 user = self._get_user(user)
771 768
772 769 obj = UserIpMap()
773 770 obj.user = user
774 771 obj.ip_addr = data['ip']
775 772 obj.description = description
776 773 self.sa.add(obj)
777 774 return obj
778 775
779 776 def delete_extra_ip(self, user, ip_id):
780 777 """
781 778 Removes ip address from UserIpMap
782 779
783 780 :param user:
784 781 :param ip_id:
785 782 """
786 783 user = self._get_user(user)
787 784 obj = UserIpMap.query().get(ip_id)
788 785 if obj:
789 786 self.sa.delete(obj)
790 787
791 788 def get_accounts_in_creation_order(self, current_user=None):
792 789 """
793 790 Get accounts in order of creation for deactivation for license limits
794 791
795 792 pick currently logged in user, and append to the list in position 0
796 793 pick all super-admins in order of creation date and add it to the list
797 794 pick all other accounts in order of creation and add it to the list.
798 795
799 796 Based on that list, the last accounts can be disabled as they are
800 797 created at the end and don't include any of the super admins as well
801 798 as the current user.
802 799
803 800 :param current_user: optionally current user running this operation
804 801 """
805 802
806 803 if not current_user:
807 804 current_user = get_current_rhodecode_user()
808 805 active_super_admins = [
809 806 x.user_id for x in User.query()
810 807 .filter(User.user_id != current_user.user_id)
811 808 .filter(User.active == true())
812 809 .filter(User.admin == true())
813 810 .order_by(User.created_on.asc())]
814 811
815 812 active_regular_users = [
816 813 x.user_id for x in User.query()
817 814 .filter(User.user_id != current_user.user_id)
818 815 .filter(User.active == true())
819 816 .filter(User.admin == false())
820 817 .order_by(User.created_on.asc())]
821 818
822 819 list_of_accounts = [current_user.user_id]
823 820 list_of_accounts += active_super_admins
824 821 list_of_accounts += active_regular_users
825 822
826 823 return list_of_accounts
827 824
828 825 def deactivate_last_users(self, expected_users):
829 826 """
830 827 Deactivate accounts that are over the license limits.
831 828 Algorithm of which accounts to disabled is based on the formula:
832 829
833 830 Get current user, then super admins in creation order, then regular
834 831 active users in creation order.
835 832
836 833 Using that list we mark all accounts from the end of it as inactive.
837 834 This way we block only latest created accounts.
838 835
839 836 :param expected_users: list of users in special order, we deactivate
840 837 the end N ammoun of users from that list
841 838 """
842 839
843 840 list_of_accounts = self.get_accounts_in_creation_order()
844 841
845 842 for acc_id in list_of_accounts[expected_users + 1:]:
846 843 user = User.get(acc_id)
847 844 log.info('Deactivating account %s for license unlock', user)
848 845 user.active = False
849 846 Session().add(user)
850 847 Session().commit()
851 848
852 849 return
General Comments 0
You need to be logged in to leave comments. Login now