##// END OF EJS Templates
integrations: refactor/cleanup + features, fixes #4181...
dan -
r731:7a6d3636 default
parent child
Show More
This diff has been collapsed as it changes many lines, (3529 lines changed) Show them Hide them
@@ -0,0 +1,3529
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import os
26 import sys
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37
38 from sqlalchemy import *
39 from sqlalchemy.exc import IntegrityError
40 from sqlalchemy.ext.declarative import declared_attr
41 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.orm import (
43 relationship, joinedload, class_mapper, validates, aliased)
44 from sqlalchemy.sql.expression import true
45 from beaker.cache import cache_region, region_invalidate
46 from webob.exc import HTTPNotFound
47 from zope.cachedescriptors.property import Lazy as LazyProperty
48
49 from pylons import url
50 from pylons.i18n.translation import lazy_ugettext as _
51
52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 from rhodecode.lib.vcs.exceptions import VCSError
55 from rhodecode.lib.vcs.backends.base import (
56 EmptyCommit, Reference, MergeFailureReason)
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 from rhodecode.lib.ext_json import json
62 from rhodecode.lib.caching_query import FromCache
63 from rhodecode.lib.encrypt import AESCipher
64
65 from rhodecode.model.meta import Base, Session
66
67 URL_SEP = '/'
68 log = logging.getLogger(__name__)
69
70 # =============================================================================
71 # BASE CLASSES
72 # =============================================================================
73
74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 # beaker.session.secret if first is not set.
76 # and initialized at environment.py
77 ENCRYPTION_KEY = None
78
79 # used to sort permissions by types, '#' used here is not allowed to be in
80 # usernames, and it's very early in sorted string.printable table.
81 PERMISSION_TYPE_SORT = {
82 'admin': '####',
83 'write': '###',
84 'read': '##',
85 'none': '#',
86 }
87
88
89 def display_sort(obj):
90 """
91 Sort function used to sort permissions in .permissions() function of
92 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 of all other resources
94 """
95
96 if obj.username == User.DEFAULT_USER:
97 return '#####'
98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 return prefix + obj.username
100
101
102 def _hash_key(k):
103 return md5_safe(k)
104
105
106 class EncryptedTextValue(TypeDecorator):
107 """
108 Special column for encrypted long text data, use like::
109
110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111
112 This column is intelligent so if value is in unencrypted form it return
113 unencrypted form, but on save it always encrypts
114 """
115 impl = Text
116
117 def process_bind_param(self, value, dialect):
118 if not value:
119 return value
120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 # protect against double encrypting if someone manually starts
122 # doing
123 raise ValueError('value needs to be in unencrypted format, ie. '
124 'not starting with enc$aes')
125 return 'enc$aes_hmac$%s' % AESCipher(
126 ENCRYPTION_KEY, hmac=True).encrypt(value)
127
128 def process_result_value(self, value, dialect):
129 import rhodecode
130
131 if not value:
132 return value
133
134 parts = value.split('$', 3)
135 if not len(parts) == 3:
136 # probably not encrypted values
137 return value
138 else:
139 if parts[0] != 'enc':
140 # parts ok but without our header ?
141 return value
142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 'rhodecode.encrypted_values.strict') or True)
144 # at that stage we know it's our encryption
145 if parts[1] == 'aes':
146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 elif parts[1] == 'aes_hmac':
148 decrypted_data = AESCipher(
149 ENCRYPTION_KEY, hmac=True,
150 strict_verification=enc_strict_mode).decrypt(parts[2])
151 else:
152 raise ValueError(
153 'Encryption type part is wrong, must be `aes` '
154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 return decrypted_data
156
157
158 class BaseModel(object):
159 """
160 Base Model for all classes
161 """
162
163 @classmethod
164 def _get_keys(cls):
165 """return column names for this model """
166 return class_mapper(cls).c.keys()
167
168 def get_dict(self):
169 """
170 return dict with keys and values corresponding
171 to this model data """
172
173 d = {}
174 for k in self._get_keys():
175 d[k] = getattr(self, k)
176
177 # also use __json__() if present to get additional fields
178 _json_attr = getattr(self, '__json__', None)
179 if _json_attr:
180 # update with attributes from __json__
181 if callable(_json_attr):
182 _json_attr = _json_attr()
183 for k, val in _json_attr.iteritems():
184 d[k] = val
185 return d
186
187 def get_appstruct(self):
188 """return list with keys and values tuples corresponding
189 to this model data """
190
191 l = []
192 for k in self._get_keys():
193 l.append((k, getattr(self, k),))
194 return l
195
196 def populate_obj(self, populate_dict):
197 """populate model with data from given populate_dict"""
198
199 for k in self._get_keys():
200 if k in populate_dict:
201 setattr(self, k, populate_dict[k])
202
203 @classmethod
204 def query(cls):
205 return Session().query(cls)
206
207 @classmethod
208 def get(cls, id_):
209 if id_:
210 return cls.query().get(id_)
211
212 @classmethod
213 def get_or_404(cls, id_):
214 try:
215 id_ = int(id_)
216 except (TypeError, ValueError):
217 raise HTTPNotFound
218
219 res = cls.query().get(id_)
220 if not res:
221 raise HTTPNotFound
222 return res
223
224 @classmethod
225 def getAll(cls):
226 # deprecated and left for backward compatibility
227 return cls.get_all()
228
229 @classmethod
230 def get_all(cls):
231 return cls.query().all()
232
233 @classmethod
234 def delete(cls, id_):
235 obj = cls.query().get(id_)
236 Session().delete(obj)
237
238 @classmethod
239 def identity_cache(cls, session, attr_name, value):
240 exist_in_session = []
241 for (item_cls, pkey), instance in session.identity_map.items():
242 if cls == item_cls and getattr(instance, attr_name) == value:
243 exist_in_session.append(instance)
244 if exist_in_session:
245 if len(exist_in_session) == 1:
246 return exist_in_session[0]
247 log.exception(
248 'multiple objects with attr %s and '
249 'value %s found with same name: %r',
250 attr_name, value, exist_in_session)
251
252 def __repr__(self):
253 if hasattr(self, '__unicode__'):
254 # python repr needs to return str
255 try:
256 return safe_str(self.__unicode__())
257 except UnicodeDecodeError:
258 pass
259 return '<DB:%s>' % (self.__class__.__name__)
260
261
262 class RhodeCodeSetting(Base, BaseModel):
263 __tablename__ = 'rhodecode_settings'
264 __table_args__ = (
265 UniqueConstraint('app_settings_name'),
266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 )
269
270 SETTINGS_TYPES = {
271 'str': safe_str,
272 'int': safe_int,
273 'unicode': safe_unicode,
274 'bool': str2bool,
275 'list': functools.partial(aslist, sep=',')
276 }
277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 GLOBAL_CONF_KEY = 'app_settings'
279
280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284
285 def __init__(self, key='', val='', type='unicode'):
286 self.app_settings_name = key
287 self.app_settings_type = type
288 self.app_settings_value = val
289
290 @validates('_app_settings_value')
291 def validate_settings_value(self, key, val):
292 assert type(val) == unicode
293 return val
294
295 @hybrid_property
296 def app_settings_value(self):
297 v = self._app_settings_value
298 _type = self.app_settings_type
299 if _type:
300 _type = self.app_settings_type.split('.')[0]
301 # decode the encrypted value
302 if 'encrypted' in self.app_settings_type:
303 cipher = EncryptedTextValue()
304 v = safe_unicode(cipher.process_result_value(v, None))
305
306 converter = self.SETTINGS_TYPES.get(_type) or \
307 self.SETTINGS_TYPES['unicode']
308 return converter(v)
309
310 @app_settings_value.setter
311 def app_settings_value(self, val):
312 """
313 Setter that will always make sure we use unicode in app_settings_value
314
315 :param val:
316 """
317 val = safe_unicode(val)
318 # encode the encrypted value
319 if 'encrypted' in self.app_settings_type:
320 cipher = EncryptedTextValue()
321 val = safe_unicode(cipher.process_bind_param(val, None))
322 self._app_settings_value = val
323
324 @hybrid_property
325 def app_settings_type(self):
326 return self._app_settings_type
327
328 @app_settings_type.setter
329 def app_settings_type(self, val):
330 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 raise Exception('type must be one of %s got %s'
332 % (self.SETTINGS_TYPES.keys(), val))
333 self._app_settings_type = val
334
335 def __unicode__(self):
336 return u"<%s('%s:%s[%s]')>" % (
337 self.__class__.__name__,
338 self.app_settings_name, self.app_settings_value,
339 self.app_settings_type
340 )
341
342
343 class RhodeCodeUi(Base, BaseModel):
344 __tablename__ = 'rhodecode_ui'
345 __table_args__ = (
346 UniqueConstraint('ui_key'),
347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 )
350
351 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 # HG
353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 HOOK_PULL = 'outgoing.pull_logger'
355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 HOOK_PUSH = 'changegroup.push_logger'
357
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 # git part is currently hardcoded.
360
361 # SVN PATTERNS
362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 SVN_TAG_ID = 'vcs_svn_tag'
364
365 ui_id = Column(
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 primary_key=True)
368 ui_section = Column(
369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 ui_key = Column(
371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 ui_value = Column(
373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 ui_active = Column(
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376
377 def __repr__(self):
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 self.ui_key, self.ui_value)
380
381
382 class RepoRhodeCodeSetting(Base, BaseModel):
383 __tablename__ = 'repo_rhodecode_settings'
384 __table_args__ = (
385 UniqueConstraint(
386 'app_settings_name', 'repository_id',
387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 )
391
392 repository_id = Column(
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 nullable=False)
395 app_settings_id = Column(
396 "app_settings_id", Integer(), nullable=False, unique=True,
397 default=None, primary_key=True)
398 app_settings_name = Column(
399 "app_settings_name", String(255), nullable=True, unique=None,
400 default=None)
401 _app_settings_value = Column(
402 "app_settings_value", String(4096), nullable=True, unique=None,
403 default=None)
404 _app_settings_type = Column(
405 "app_settings_type", String(255), nullable=True, unique=None,
406 default=None)
407
408 repository = relationship('Repository')
409
410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 self.repository_id = repository_id
412 self.app_settings_name = key
413 self.app_settings_type = type
414 self.app_settings_value = val
415
416 @validates('_app_settings_value')
417 def validate_settings_value(self, key, val):
418 assert type(val) == unicode
419 return val
420
421 @hybrid_property
422 def app_settings_value(self):
423 v = self._app_settings_value
424 type_ = self.app_settings_type
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 return converter(v)
428
429 @app_settings_value.setter
430 def app_settings_value(self, val):
431 """
432 Setter that will always make sure we use unicode in app_settings_value
433
434 :param val:
435 """
436 self._app_settings_value = safe_unicode(val)
437
438 @hybrid_property
439 def app_settings_type(self):
440 return self._app_settings_type
441
442 @app_settings_type.setter
443 def app_settings_type(self, val):
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 if val not in SETTINGS_TYPES:
446 raise Exception('type must be one of %s got %s'
447 % (SETTINGS_TYPES.keys(), val))
448 self._app_settings_type = val
449
450 def __unicode__(self):
451 return u"<%s('%s:%s:%s[%s]')>" % (
452 self.__class__.__name__, self.repository.repo_name,
453 self.app_settings_name, self.app_settings_value,
454 self.app_settings_type
455 )
456
457
458 class RepoRhodeCodeUi(Base, BaseModel):
459 __tablename__ = 'repo_rhodecode_ui'
460 __table_args__ = (
461 UniqueConstraint(
462 'repository_id', 'ui_section', 'ui_key',
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 )
467
468 repository_id = Column(
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 nullable=False)
471 ui_id = Column(
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 primary_key=True)
474 ui_section = Column(
475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 ui_key = Column(
477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 ui_value = Column(
479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 ui_active = Column(
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482
483 repository = relationship('Repository')
484
485 def __repr__(self):
486 return '<%s[%s:%s]%s=>%s]>' % (
487 self.__class__.__name__, self.repository.repo_name,
488 self.ui_section, self.ui_key, self.ui_value)
489
490
491 class User(Base, BaseModel):
492 __tablename__ = 'users'
493 __table_args__ = (
494 UniqueConstraint('username'), UniqueConstraint('email'),
495 Index('u_username_idx', 'username'),
496 Index('u_email_idx', 'email'),
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 )
500 DEFAULT_USER = 'default'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519
520 user_log = relationship('UserLog')
521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522
523 repositories = relationship('Repository')
524 repository_groups = relationship('RepoGroup')
525 user_groups = relationship('UserGroup')
526
527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529
530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533
534 group_member = relationship('UserGroupMember', cascade='all')
535
536 notifications = relationship('UserNotification', cascade='all')
537 # notifications assigned to this user
538 user_created_notifications = relationship('Notification', cascade='all')
539 # comments created by this user
540 user_comments = relationship('ChangesetComment', cascade='all')
541 # user profile extra info
542 user_emails = relationship('UserEmailMap', cascade='all')
543 user_ip_map = relationship('UserIpMap', cascade='all')
544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 # gists
546 user_gists = relationship('Gist', cascade='all')
547 # user pull requests
548 user_pull_requests = relationship('PullRequest', cascade='all')
549 # external identities
550 extenal_identities = relationship(
551 'ExternalIdentity',
552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 cascade='all')
554
555 def __unicode__(self):
556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 self.user_id, self.username)
558
559 @hybrid_property
560 def email(self):
561 return self._email
562
563 @email.setter
564 def email(self, val):
565 self._email = val.lower() if val else None
566
567 @property
568 def firstname(self):
569 # alias for future
570 return self.name
571
572 @property
573 def emails(self):
574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 return [self.email] + [x.email for x in other]
576
577 @property
578 def auth_tokens(self):
579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580
581 @property
582 def extra_auth_tokens(self):
583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584
585 @property
586 def feed_token(self):
587 feed_tokens = UserApiKeys.query()\
588 .filter(UserApiKeys.user == self)\
589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 .all()
591 if feed_tokens:
592 return feed_tokens[0].api_key
593 else:
594 # use the main token so we don't end up with nothing...
595 return self.api_key
596
597 @classmethod
598 def extra_valid_auth_tokens(cls, user, role=None):
599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 .filter(or_(UserApiKeys.expires == -1,
601 UserApiKeys.expires >= time.time()))
602 if role:
603 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 return tokens.all()
606
607 @property
608 def ip_addresses(self):
609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 return [x.ip_addr for x in ret]
611
612 @property
613 def username_and_name(self):
614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615
616 @property
617 def username_or_name_or_email(self):
618 full_name = self.full_name if self.full_name is not ' ' else None
619 return self.username or full_name or self.email
620
621 @property
622 def full_name(self):
623 return '%s %s' % (self.firstname, self.lastname)
624
625 @property
626 def full_name_or_username(self):
627 return ('%s %s' % (self.firstname, self.lastname)
628 if (self.firstname and self.lastname) else self.username)
629
630 @property
631 def full_contact(self):
632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633
634 @property
635 def short_contact(self):
636 return '%s %s' % (self.firstname, self.lastname)
637
638 @property
639 def is_admin(self):
640 return self.admin
641
642 @property
643 def AuthUser(self):
644 """
645 Returns instance of AuthUser for this user
646 """
647 from rhodecode.lib.auth import AuthUser
648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 username=self.username)
650
651 @hybrid_property
652 def user_data(self):
653 if not self._user_data:
654 return {}
655
656 try:
657 return json.loads(self._user_data)
658 except TypeError:
659 return {}
660
661 @user_data.setter
662 def user_data(self, val):
663 if not isinstance(val, dict):
664 raise Exception('user_data must be dict, got %s' % type(val))
665 try:
666 self._user_data = json.dumps(val)
667 except Exception:
668 log.error(traceback.format_exc())
669
670 @classmethod
671 def get_by_username(cls, username, case_insensitive=False,
672 cache=False, identity_cache=False):
673 session = Session()
674
675 if case_insensitive:
676 q = cls.query().filter(
677 func.lower(cls.username) == func.lower(username))
678 else:
679 q = cls.query().filter(cls.username == username)
680
681 if cache:
682 if identity_cache:
683 val = cls.identity_cache(session, 'username', username)
684 if val:
685 return val
686 else:
687 q = q.options(
688 FromCache("sql_cache_short",
689 "get_user_by_name_%s" % _hash_key(username)))
690
691 return q.scalar()
692
693 @classmethod
694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 q = cls.query().filter(cls.api_key == auth_token)
696
697 if cache:
698 q = q.options(FromCache("sql_cache_short",
699 "get_auth_token_%s" % auth_token))
700 res = q.scalar()
701
702 if fallback and not res:
703 #fallback to additional keys
704 _res = UserApiKeys.query()\
705 .filter(UserApiKeys.api_key == auth_token)\
706 .filter(or_(UserApiKeys.expires == -1,
707 UserApiKeys.expires >= time.time()))\
708 .first()
709 if _res:
710 res = _res.user
711 return res
712
713 @classmethod
714 def get_by_email(cls, email, case_insensitive=False, cache=False):
715
716 if case_insensitive:
717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718
719 else:
720 q = cls.query().filter(cls.email == email)
721
722 if cache:
723 q = q.options(FromCache("sql_cache_short",
724 "get_email_key_%s" % _hash_key(email)))
725
726 ret = q.scalar()
727 if ret is None:
728 q = UserEmailMap.query()
729 # try fetching in alternate email map
730 if case_insensitive:
731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 else:
733 q = q.filter(UserEmailMap.email == email)
734 q = q.options(joinedload(UserEmailMap.user))
735 if cache:
736 q = q.options(FromCache("sql_cache_short",
737 "get_email_map_key_%s" % email))
738 ret = getattr(q.scalar(), 'user', None)
739
740 return ret
741
742 @classmethod
743 def get_from_cs_author(cls, author):
744 """
745 Tries to get User objects out of commit author string
746
747 :param author:
748 """
749 from rhodecode.lib.helpers import email, author_name
750 # Valid email in the attribute passed, see if they're in the system
751 _email = email(author)
752 if _email:
753 user = cls.get_by_email(_email, case_insensitive=True)
754 if user:
755 return user
756 # Maybe we can match by username?
757 _author = author_name(author)