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