##// END OF EJS Templates
fix(sqlalchemy): fixed deprecated methods for sqlalchemy
super-admin -
r5577:94285907 default
parent child Browse files
Show More
@@ -1,6055 +1,6055 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 import pyotp
36 import pyotp
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 true, false, null, union_all,
39 true, false, null, union_all,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Text, Float, PickleType, BigInteger)
42 Text, Float, PickleType, BigInteger)
43 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.expression import case
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.orm import (
45 from sqlalchemy.orm import (
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.dialects.mysql import LONGTEXT
50 from sqlalchemy.dialects.mysql import LONGTEXT
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53 from webhelpers2.text import remove_formatting
53 from webhelpers2.text import remove_formatting
54
54
55 from rhodecode import ConfigGet
55 from rhodecode import ConfigGet
56 from rhodecode.lib.str_utils import safe_bytes
56 from rhodecode.lib.str_utils import safe_bytes
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, sha1_safe,
62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 from rhodecode.lib.jsonalchemy import (
65 from rhodecode.lib.jsonalchemy import (
66 MutationObj, MutationList, JsonType, JsonRaw)
66 MutationObj, MutationList, JsonType, JsonRaw)
67 from rhodecode.lib.hash_utils import sha1
67 from rhodecode.lib.hash_utils import sha1
68 from rhodecode.lib import ext_json
68 from rhodecode.lib import ext_json
69 from rhodecode.lib import enc_utils
69 from rhodecode.lib import enc_utils
70 from rhodecode.lib.ext_json import json, str_json
70 from rhodecode.lib.ext_json import json, str_json
71 from rhodecode.lib.caching_query import FromCache
71 from rhodecode.lib.caching_query import FromCache
72 from rhodecode.lib.exceptions import (
72 from rhodecode.lib.exceptions import (
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 from rhodecode.model.meta import Base, Session
74 from rhodecode.model.meta import Base, Session
75
75
76 URL_SEP = '/'
76 URL_SEP = '/'
77 log = logging.getLogger(__name__)
77 log = logging.getLogger(__name__)
78
78
79 # =============================================================================
79 # =============================================================================
80 # BASE CLASSES
80 # BASE CLASSES
81 # =============================================================================
81 # =============================================================================
82
82
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 # beaker.session.secret if first is not set.
84 # beaker.session.secret if first is not set.
85 # and initialized at environment.py
85 # and initialized at environment.py
86 ENCRYPTION_KEY: bytes = b''
86 ENCRYPTION_KEY: bytes = b''
87
87
88 # used to sort permissions by types, '#' used here is not allowed to be in
88 # used to sort permissions by types, '#' used here is not allowed to be in
89 # usernames, and it's very early in sorted string.printable table.
89 # usernames, and it's very early in sorted string.printable table.
90 PERMISSION_TYPE_SORT = {
90 PERMISSION_TYPE_SORT = {
91 'admin': '####',
91 'admin': '####',
92 'write': '###',
92 'write': '###',
93 'read': '##',
93 'read': '##',
94 'none': '#',
94 'none': '#',
95 }
95 }
96
96
97
97
98 def display_user_sort(obj):
98 def display_user_sort(obj):
99 """
99 """
100 Sort function used to sort permissions in .permissions() function of
100 Sort function used to sort permissions in .permissions() function of
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 of all other resources
102 of all other resources
103 """
103 """
104
104
105 if obj.username == User.DEFAULT_USER:
105 if obj.username == User.DEFAULT_USER:
106 return '#####'
106 return '#####'
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 extra_sort_num = '1' # default
108 extra_sort_num = '1' # default
109
109
110 # NOTE(dan): inactive duplicates goes last
110 # NOTE(dan): inactive duplicates goes last
111 if getattr(obj, 'duplicate_perm', None):
111 if getattr(obj, 'duplicate_perm', None):
112 extra_sort_num = '9'
112 extra_sort_num = '9'
113 return prefix + extra_sort_num + obj.username
113 return prefix + extra_sort_num + obj.username
114
114
115
115
116 def display_user_group_sort(obj):
116 def display_user_group_sort(obj):
117 """
117 """
118 Sort function used to sort permissions in .permissions() function of
118 Sort function used to sort permissions in .permissions() function of
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 of all other resources
120 of all other resources
121 """
121 """
122
122
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 return prefix + obj.users_group_name
124 return prefix + obj.users_group_name
125
125
126
126
127 def _hash_key(k):
127 def _hash_key(k):
128 return sha1_safe(k)
128 return sha1_safe(k)
129
129
130
130
131 def description_escaper(desc):
131 def description_escaper(desc):
132 from rhodecode.lib import helpers as h
132 from rhodecode.lib import helpers as h
133 return h.escape(desc)
133 return h.escape(desc)
134
134
135
135
136 def in_filter_generator(qry, items, limit=500):
136 def in_filter_generator(qry, items, limit=500):
137 """
137 """
138 Splits IN() into multiple with OR
138 Splits IN() into multiple with OR
139 e.g.::
139 e.g.::
140 cnt = Repository.query().filter(
140 cnt = Repository.query().filter(
141 or_(
141 or_(
142 *in_filter_generator(Repository.repo_id, range(100000))
142 *in_filter_generator(Repository.repo_id, range(100000))
143 )).count()
143 )).count()
144 """
144 """
145 if not items:
145 if not items:
146 # empty list will cause empty query which might cause security issues
146 # empty list will cause empty query which might cause security issues
147 # this can lead to hidden unpleasant results
147 # this can lead to hidden unpleasant results
148 items = [-1]
148 items = [-1]
149
149
150 parts = []
150 parts = []
151 for chunk in range(0, len(items), limit):
151 for chunk in range(0, len(items), limit):
152 parts.append(
152 parts.append(
153 qry.in_(items[chunk: chunk + limit])
153 qry.in_(items[chunk: chunk + limit])
154 )
154 )
155
155
156 return parts
156 return parts
157
157
158
158
159 base_table_args = {
159 base_table_args = {
160 'extend_existing': True,
160 'extend_existing': True,
161 'mysql_engine': 'InnoDB',
161 'mysql_engine': 'InnoDB',
162 'mysql_charset': 'utf8',
162 'mysql_charset': 'utf8',
163 'sqlite_autoincrement': True
163 'sqlite_autoincrement': True
164 }
164 }
165
165
166
166
167 class EncryptedTextValue(TypeDecorator):
167 class EncryptedTextValue(TypeDecorator):
168 """
168 """
169 Special column for encrypted long text data, use like::
169 Special column for encrypted long text data, use like::
170
170
171 value = Column("encrypted_value", EncryptedValue(), nullable=False)
171 value = Column("encrypted_value", EncryptedValue(), nullable=False)
172
172
173 This column is intelligent so if value is in unencrypted form it return
173 This column is intelligent so if value is in unencrypted form it return
174 unencrypted form, but on save it always encrypts
174 unencrypted form, but on save it always encrypts
175 """
175 """
176 cache_ok = True
176 cache_ok = True
177 impl = Text
177 impl = Text
178
178
179 def process_bind_param(self, value, dialect):
179 def process_bind_param(self, value, dialect):
180 """
180 """
181 Setter for storing value
181 Setter for storing value
182 """
182 """
183 import rhodecode
183 import rhodecode
184 if not value:
184 if not value:
185 return value
185 return value
186
186
187 # protect against double encrypting if values is already encrypted
187 # protect against double encrypting if values is already encrypted
188 if value.startswith('enc$aes$') \
188 if value.startswith('enc$aes$') \
189 or value.startswith('enc$aes_hmac$') \
189 or value.startswith('enc$aes_hmac$') \
190 or value.startswith('enc2$'):
190 or value.startswith('enc2$'):
191 raise ValueError('value needs to be in unencrypted format, '
191 raise ValueError('value needs to be in unencrypted format, '
192 'ie. not starting with enc$ or enc2$')
192 'ie. not starting with enc$ or enc2$')
193
193
194 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
194 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
195 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
195 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
196 return safe_str(bytes_val)
196 return safe_str(bytes_val)
197
197
198 def process_result_value(self, value, dialect):
198 def process_result_value(self, value, dialect):
199 """
199 """
200 Getter for retrieving value
200 Getter for retrieving value
201 """
201 """
202
202
203 import rhodecode
203 import rhodecode
204 if not value:
204 if not value:
205 return value
205 return value
206
206
207 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
207 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
208
208
209 return safe_str(bytes_val)
209 return safe_str(bytes_val)
210
210
211
211
212 class BaseModel(object):
212 class BaseModel(object):
213 """
213 """
214 Base Model for all classes
214 Base Model for all classes
215 """
215 """
216
216
217 @classmethod
217 @classmethod
218 def _get_keys(cls):
218 def _get_keys(cls):
219 """return column names for this model """
219 """return column names for this model """
220 return class_mapper(cls).c.keys()
220 return class_mapper(cls).c.keys()
221
221
222 def get_dict(self):
222 def get_dict(self):
223 """
223 """
224 return dict with keys and values corresponding
224 return dict with keys and values corresponding
225 to this model data """
225 to this model data """
226
226
227 d = {}
227 d = {}
228 for k in self._get_keys():
228 for k in self._get_keys():
229 d[k] = getattr(self, k)
229 d[k] = getattr(self, k)
230
230
231 # also use __json__() if present to get additional fields
231 # also use __json__() if present to get additional fields
232 _json_attr = getattr(self, '__json__', None)
232 _json_attr = getattr(self, '__json__', None)
233 if _json_attr:
233 if _json_attr:
234 # update with attributes from __json__
234 # update with attributes from __json__
235 if callable(_json_attr):
235 if callable(_json_attr):
236 _json_attr = _json_attr()
236 _json_attr = _json_attr()
237 for k, val in _json_attr.items():
237 for k, val in _json_attr.items():
238 d[k] = val
238 d[k] = val
239 return d
239 return d
240
240
241 def get_appstruct(self):
241 def get_appstruct(self):
242 """return list with keys and values tuples corresponding
242 """return list with keys and values tuples corresponding
243 to this model data """
243 to this model data """
244
244
245 lst = []
245 lst = []
246 for k in self._get_keys():
246 for k in self._get_keys():
247 lst.append((k, getattr(self, k),))
247 lst.append((k, getattr(self, k),))
248 return lst
248 return lst
249
249
250 def populate_obj(self, populate_dict):
250 def populate_obj(self, populate_dict):
251 """populate model with data from given populate_dict"""
251 """populate model with data from given populate_dict"""
252
252
253 for k in self._get_keys():
253 for k in self._get_keys():
254 if k in populate_dict:
254 if k in populate_dict:
255 setattr(self, k, populate_dict[k])
255 setattr(self, k, populate_dict[k])
256
256
257 @classmethod
257 @classmethod
258 def query(cls):
258 def query(cls):
259 return Session().query(cls)
259 return Session().query(cls)
260
260
261 @classmethod
261 @classmethod
262 def select(cls, custom_cls=None):
262 def select(cls, custom_cls=None):
263 """
263 """
264 stmt = cls.select().where(cls.user_id==1)
264 stmt = cls.select().where(cls.user_id==1)
265 # optionally
265 # optionally
266 stmt = cls.select(User.user_id).where(cls.user_id==1)
266 stmt = cls.select(User.user_id).where(cls.user_id==1)
267 result = cls.execute(stmt) | cls.scalars(stmt)
267 result = cls.execute(stmt) | cls.scalars(stmt)
268 """
268 """
269
269
270 if custom_cls:
270 if custom_cls:
271 stmt = select(custom_cls)
271 stmt = select(custom_cls)
272 else:
272 else:
273 stmt = select(cls)
273 stmt = select(cls)
274 return stmt
274 return stmt
275
275
276 @classmethod
276 @classmethod
277 def execute(cls, stmt):
277 def execute(cls, stmt):
278 return Session().execute(stmt)
278 return Session().execute(stmt)
279
279
280 @classmethod
280 @classmethod
281 def scalars(cls, stmt):
281 def scalars(cls, stmt):
282 return Session().scalars(stmt)
282 return Session().scalars(stmt)
283
283
284 @classmethod
284 @classmethod
285 def get(cls, id_):
285 def get(cls, id_):
286 if id_:
286 if id_:
287 return cls.query().get(id_)
287 return Session().get(cls, id_)
288
288
289 @classmethod
289 @classmethod
290 def get_or_404(cls, id_):
290 def get_or_404(cls, id_):
291 from pyramid.httpexceptions import HTTPNotFound
291 from pyramid.httpexceptions import HTTPNotFound
292
292
293 try:
293 try:
294 id_ = int(id_)
294 id_ = int(id_)
295 except (TypeError, ValueError):
295 except (TypeError, ValueError):
296 raise HTTPNotFound()
296 raise HTTPNotFound()
297
297
298 res = cls.query().get(id_)
298 res = cls.query().get(id_)
299 if not res:
299 if not res:
300 raise HTTPNotFound()
300 raise HTTPNotFound()
301 return res
301 return res
302
302
303 @classmethod
303 @classmethod
304 def getAll(cls):
304 def getAll(cls):
305 # deprecated and left for backward compatibility
305 # deprecated and left for backward compatibility
306 return cls.get_all()
306 return cls.get_all()
307
307
308 @classmethod
308 @classmethod
309 def get_all(cls):
309 def get_all(cls):
310 return cls.query().all()
310 return cls.query().all()
311
311
312 @classmethod
312 @classmethod
313 def delete(cls, id_):
313 def delete(cls, id_):
314 obj = cls.query().get(id_)
314 obj = cls.query().get(id_)
315 Session().delete(obj)
315 Session().delete(obj)
316
316
317 @classmethod
317 @classmethod
318 def identity_cache(cls, session, attr_name, value):
318 def identity_cache(cls, session, attr_name, value):
319 exist_in_session = []
319 exist_in_session = []
320 for (item_cls, pkey), instance in session.identity_map.items():
320 for (item_cls, pkey), instance in session.identity_map.items():
321 if cls == item_cls and getattr(instance, attr_name) == value:
321 if cls == item_cls and getattr(instance, attr_name) == value:
322 exist_in_session.append(instance)
322 exist_in_session.append(instance)
323 if exist_in_session:
323 if exist_in_session:
324 if len(exist_in_session) == 1:
324 if len(exist_in_session) == 1:
325 return exist_in_session[0]
325 return exist_in_session[0]
326 log.exception(
326 log.exception(
327 'multiple objects with attr %s and '
327 'multiple objects with attr %s and '
328 'value %s found with same name: %r',
328 'value %s found with same name: %r',
329 attr_name, value, exist_in_session)
329 attr_name, value, exist_in_session)
330
330
331 @property
331 @property
332 def cls_name(self):
332 def cls_name(self):
333 return self.__class__.__name__
333 return self.__class__.__name__
334
334
335 def __repr__(self):
335 def __repr__(self):
336 return f'<DB:{self.cls_name}>'
336 return f'<DB:{self.cls_name}>'
337
337
338
338
339 class RhodeCodeSetting(Base, BaseModel):
339 class RhodeCodeSetting(Base, BaseModel):
340 __tablename__ = 'rhodecode_settings'
340 __tablename__ = 'rhodecode_settings'
341 __table_args__ = (
341 __table_args__ = (
342 UniqueConstraint('app_settings_name'),
342 UniqueConstraint('app_settings_name'),
343 base_table_args
343 base_table_args
344 )
344 )
345
345
346 SETTINGS_TYPES = {
346 SETTINGS_TYPES = {
347 'str': safe_str,
347 'str': safe_str,
348 'int': safe_int,
348 'int': safe_int,
349 'unicode': safe_str,
349 'unicode': safe_str,
350 'bool': str2bool,
350 'bool': str2bool,
351 'list': functools.partial(aslist, sep=',')
351 'list': functools.partial(aslist, sep=',')
352 }
352 }
353 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
353 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
354 GLOBAL_CONF_KEY = 'app_settings'
354 GLOBAL_CONF_KEY = 'app_settings'
355
355
356 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
356 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
357 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
357 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
358 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
358 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
359 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
359 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
360
360
361 def __init__(self, key='', val='', type='unicode'):
361 def __init__(self, key='', val='', type='unicode'):
362 self.app_settings_name = key
362 self.app_settings_name = key
363 self.app_settings_type = type
363 self.app_settings_type = type
364 self.app_settings_value = val
364 self.app_settings_value = val
365
365
366 @validates('_app_settings_value')
366 @validates('_app_settings_value')
367 def validate_settings_value(self, key, val):
367 def validate_settings_value(self, key, val):
368 assert type(val) == str
368 assert type(val) == str
369 return val
369 return val
370
370
371 @hybrid_property
371 @hybrid_property
372 def app_settings_value(self):
372 def app_settings_value(self):
373 v = self._app_settings_value
373 v = self._app_settings_value
374 _type = self.app_settings_type
374 _type = self.app_settings_type
375 if _type:
375 if _type:
376 _type = self.app_settings_type.split('.')[0]
376 _type = self.app_settings_type.split('.')[0]
377 # decode the encrypted value
377 # decode the encrypted value
378 if 'encrypted' in self.app_settings_type:
378 if 'encrypted' in self.app_settings_type:
379 cipher = EncryptedTextValue()
379 cipher = EncryptedTextValue()
380 v = safe_str(cipher.process_result_value(v, None))
380 v = safe_str(cipher.process_result_value(v, None))
381
381
382 converter = self.SETTINGS_TYPES.get(_type) or \
382 converter = self.SETTINGS_TYPES.get(_type) or \
383 self.SETTINGS_TYPES['unicode']
383 self.SETTINGS_TYPES['unicode']
384 return converter(v)
384 return converter(v)
385
385
386 @app_settings_value.setter
386 @app_settings_value.setter
387 def app_settings_value(self, val):
387 def app_settings_value(self, val):
388 """
388 """
389 Setter that will always make sure we use unicode in app_settings_value
389 Setter that will always make sure we use unicode in app_settings_value
390
390
391 :param val:
391 :param val:
392 """
392 """
393 val = safe_str(val)
393 val = safe_str(val)
394 # encode the encrypted value
394 # encode the encrypted value
395 if 'encrypted' in self.app_settings_type:
395 if 'encrypted' in self.app_settings_type:
396 cipher = EncryptedTextValue()
396 cipher = EncryptedTextValue()
397 val = safe_str(cipher.process_bind_param(val, None))
397 val = safe_str(cipher.process_bind_param(val, None))
398 self._app_settings_value = val
398 self._app_settings_value = val
399
399
400 @hybrid_property
400 @hybrid_property
401 def app_settings_type(self):
401 def app_settings_type(self):
402 return self._app_settings_type
402 return self._app_settings_type
403
403
404 @app_settings_type.setter
404 @app_settings_type.setter
405 def app_settings_type(self, val):
405 def app_settings_type(self, val):
406 if val.split('.')[0] not in self.SETTINGS_TYPES:
406 if val.split('.')[0] not in self.SETTINGS_TYPES:
407 raise Exception('type must be one of %s got %s'
407 raise Exception('type must be one of %s got %s'
408 % (self.SETTINGS_TYPES.keys(), val))
408 % (self.SETTINGS_TYPES.keys(), val))
409 self._app_settings_type = val
409 self._app_settings_type = val
410
410
411 @classmethod
411 @classmethod
412 def get_by_prefix(cls, prefix):
412 def get_by_prefix(cls, prefix):
413 return RhodeCodeSetting.query()\
413 return RhodeCodeSetting.query()\
414 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
414 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
415 .all()
415 .all()
416
416
417 def __repr__(self):
417 def __repr__(self):
418 return "<%s('%s:%s[%s]')>" % (
418 return "<%s('%s:%s[%s]')>" % (
419 self.cls_name,
419 self.cls_name,
420 self.app_settings_name, self.app_settings_value,
420 self.app_settings_name, self.app_settings_value,
421 self.app_settings_type
421 self.app_settings_type
422 )
422 )
423
423
424
424
425 class RhodeCodeUi(Base, BaseModel):
425 class RhodeCodeUi(Base, BaseModel):
426 __tablename__ = 'rhodecode_ui'
426 __tablename__ = 'rhodecode_ui'
427 __table_args__ = (
427 __table_args__ = (
428 UniqueConstraint('ui_key'),
428 UniqueConstraint('ui_key'),
429 base_table_args
429 base_table_args
430 )
430 )
431 # Sync those values with vcsserver.config.hooks
431 # Sync those values with vcsserver.config.hooks
432
432
433 HOOK_REPO_SIZE = 'changegroup.repo_size'
433 HOOK_REPO_SIZE = 'changegroup.repo_size'
434 # HG
434 # HG
435 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
435 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
436 HOOK_PULL = 'outgoing.pull_logger'
436 HOOK_PULL = 'outgoing.pull_logger'
437 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
437 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
438 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
438 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
439 HOOK_PUSH = 'changegroup.push_logger'
439 HOOK_PUSH = 'changegroup.push_logger'
440 HOOK_PUSH_KEY = 'pushkey.key_push'
440 HOOK_PUSH_KEY = 'pushkey.key_push'
441
441
442 HOOKS_BUILTIN = [
442 HOOKS_BUILTIN = [
443 HOOK_PRE_PULL,
443 HOOK_PRE_PULL,
444 HOOK_PULL,
444 HOOK_PULL,
445 HOOK_PRE_PUSH,
445 HOOK_PRE_PUSH,
446 HOOK_PRETX_PUSH,
446 HOOK_PRETX_PUSH,
447 HOOK_PUSH,
447 HOOK_PUSH,
448 HOOK_PUSH_KEY,
448 HOOK_PUSH_KEY,
449 ]
449 ]
450
450
451 # TODO: johbo: Unify way how hooks are configured for git and hg,
451 # TODO: johbo: Unify way how hooks are configured for git and hg,
452 # git part is currently hardcoded.
452 # git part is currently hardcoded.
453
453
454 # SVN PATTERNS
454 # SVN PATTERNS
455 SVN_BRANCH_ID = 'vcs_svn_branch'
455 SVN_BRANCH_ID = 'vcs_svn_branch'
456 SVN_TAG_ID = 'vcs_svn_tag'
456 SVN_TAG_ID = 'vcs_svn_tag'
457
457
458 ui_id = Column(
458 ui_id = Column(
459 "ui_id", Integer(), nullable=False, unique=True, default=None,
459 "ui_id", Integer(), nullable=False, unique=True, default=None,
460 primary_key=True)
460 primary_key=True)
461 ui_section = Column(
461 ui_section = Column(
462 "ui_section", String(255), nullable=True, unique=None, default=None)
462 "ui_section", String(255), nullable=True, unique=None, default=None)
463 ui_key = Column(
463 ui_key = Column(
464 "ui_key", String(255), nullable=True, unique=None, default=None)
464 "ui_key", String(255), nullable=True, unique=None, default=None)
465 ui_value = Column(
465 ui_value = Column(
466 "ui_value", String(255), nullable=True, unique=None, default=None)
466 "ui_value", String(255), nullable=True, unique=None, default=None)
467 ui_active = Column(
467 ui_active = Column(
468 "ui_active", Boolean(), nullable=True, unique=None, default=True)
468 "ui_active", Boolean(), nullable=True, unique=None, default=True)
469
469
470 def __repr__(self):
470 def __repr__(self):
471 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
471 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
472 self.ui_key, self.ui_value)
472 self.ui_key, self.ui_value)
473
473
474
474
475 class RepoRhodeCodeSetting(Base, BaseModel):
475 class RepoRhodeCodeSetting(Base, BaseModel):
476 __tablename__ = 'repo_rhodecode_settings'
476 __tablename__ = 'repo_rhodecode_settings'
477 __table_args__ = (
477 __table_args__ = (
478 UniqueConstraint(
478 UniqueConstraint(
479 'app_settings_name', 'repository_id',
479 'app_settings_name', 'repository_id',
480 name='uq_repo_rhodecode_setting_name_repo_id'),
480 name='uq_repo_rhodecode_setting_name_repo_id'),
481 base_table_args
481 base_table_args
482 )
482 )
483
483
484 repository_id = Column(
484 repository_id = Column(
485 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
485 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
486 nullable=False)
486 nullable=False)
487 app_settings_id = Column(
487 app_settings_id = Column(
488 "app_settings_id", Integer(), nullable=False, unique=True,
488 "app_settings_id", Integer(), nullable=False, unique=True,
489 default=None, primary_key=True)
489 default=None, primary_key=True)
490 app_settings_name = Column(
490 app_settings_name = Column(
491 "app_settings_name", String(255), nullable=True, unique=None,
491 "app_settings_name", String(255), nullable=True, unique=None,
492 default=None)
492 default=None)
493 _app_settings_value = Column(
493 _app_settings_value = Column(
494 "app_settings_value", String(4096), nullable=True, unique=None,
494 "app_settings_value", String(4096), nullable=True, unique=None,
495 default=None)
495 default=None)
496 _app_settings_type = Column(
496 _app_settings_type = Column(
497 "app_settings_type", String(255), nullable=True, unique=None,
497 "app_settings_type", String(255), nullable=True, unique=None,
498 default=None)
498 default=None)
499
499
500 repository = relationship('Repository', viewonly=True)
500 repository = relationship('Repository', viewonly=True)
501
501
502 def __init__(self, repository_id, key='', val='', type='unicode'):
502 def __init__(self, repository_id, key='', val='', type='unicode'):
503 self.repository_id = repository_id
503 self.repository_id = repository_id
504 self.app_settings_name = key
504 self.app_settings_name = key
505 self.app_settings_type = type
505 self.app_settings_type = type
506 self.app_settings_value = val
506 self.app_settings_value = val
507
507
508 @validates('_app_settings_value')
508 @validates('_app_settings_value')
509 def validate_settings_value(self, key, val):
509 def validate_settings_value(self, key, val):
510 assert type(val) == str
510 assert type(val) == str
511 return val
511 return val
512
512
513 @hybrid_property
513 @hybrid_property
514 def app_settings_value(self):
514 def app_settings_value(self):
515 v = self._app_settings_value
515 v = self._app_settings_value
516 type_ = self.app_settings_type
516 type_ = self.app_settings_type
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
518 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
519 return converter(v)
519 return converter(v)
520
520
521 @app_settings_value.setter
521 @app_settings_value.setter
522 def app_settings_value(self, val):
522 def app_settings_value(self, val):
523 """
523 """
524 Setter that will always make sure we use unicode in app_settings_value
524 Setter that will always make sure we use unicode in app_settings_value
525
525
526 :param val:
526 :param val:
527 """
527 """
528 self._app_settings_value = safe_str(val)
528 self._app_settings_value = safe_str(val)
529
529
530 @hybrid_property
530 @hybrid_property
531 def app_settings_type(self):
531 def app_settings_type(self):
532 return self._app_settings_type
532 return self._app_settings_type
533
533
534 @app_settings_type.setter
534 @app_settings_type.setter
535 def app_settings_type(self, val):
535 def app_settings_type(self, val):
536 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
536 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
537 if val not in SETTINGS_TYPES:
537 if val not in SETTINGS_TYPES:
538 raise Exception('type must be one of %s got %s'
538 raise Exception('type must be one of %s got %s'
539 % (SETTINGS_TYPES.keys(), val))
539 % (SETTINGS_TYPES.keys(), val))
540 self._app_settings_type = val
540 self._app_settings_type = val
541
541
542 def __repr__(self):
542 def __repr__(self):
543 return "<%s('%s:%s:%s[%s]')>" % (
543 return "<%s('%s:%s:%s[%s]')>" % (
544 self.cls_name, self.repository.repo_name,
544 self.cls_name, self.repository.repo_name,
545 self.app_settings_name, self.app_settings_value,
545 self.app_settings_name, self.app_settings_value,
546 self.app_settings_type
546 self.app_settings_type
547 )
547 )
548
548
549
549
550 class RepoRhodeCodeUi(Base, BaseModel):
550 class RepoRhodeCodeUi(Base, BaseModel):
551 __tablename__ = 'repo_rhodecode_ui'
551 __tablename__ = 'repo_rhodecode_ui'
552 __table_args__ = (
552 __table_args__ = (
553 UniqueConstraint(
553 UniqueConstraint(
554 'repository_id', 'ui_section', 'ui_key',
554 'repository_id', 'ui_section', 'ui_key',
555 name='uq_repo_rhodecode_ui_repository_id_section_key'),
555 name='uq_repo_rhodecode_ui_repository_id_section_key'),
556 base_table_args
556 base_table_args
557 )
557 )
558
558
559 repository_id = Column(
559 repository_id = Column(
560 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
560 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
561 nullable=False)
561 nullable=False)
562 ui_id = Column(
562 ui_id = Column(
563 "ui_id", Integer(), nullable=False, unique=True, default=None,
563 "ui_id", Integer(), nullable=False, unique=True, default=None,
564 primary_key=True)
564 primary_key=True)
565 ui_section = Column(
565 ui_section = Column(
566 "ui_section", String(255), nullable=True, unique=None, default=None)
566 "ui_section", String(255), nullable=True, unique=None, default=None)
567 ui_key = Column(
567 ui_key = Column(
568 "ui_key", String(255), nullable=True, unique=None, default=None)
568 "ui_key", String(255), nullable=True, unique=None, default=None)
569 ui_value = Column(
569 ui_value = Column(
570 "ui_value", String(255), nullable=True, unique=None, default=None)
570 "ui_value", String(255), nullable=True, unique=None, default=None)
571 ui_active = Column(
571 ui_active = Column(
572 "ui_active", Boolean(), nullable=True, unique=None, default=True)
572 "ui_active", Boolean(), nullable=True, unique=None, default=True)
573
573
574 repository = relationship('Repository', viewonly=True)
574 repository = relationship('Repository', viewonly=True)
575
575
576 def __repr__(self):
576 def __repr__(self):
577 return '<%s[%s:%s]%s=>%s]>' % (
577 return '<%s[%s:%s]%s=>%s]>' % (
578 self.cls_name, self.repository.repo_name,
578 self.cls_name, self.repository.repo_name,
579 self.ui_section, self.ui_key, self.ui_value)
579 self.ui_section, self.ui_key, self.ui_value)
580
580
581
581
582 class User(Base, BaseModel):
582 class User(Base, BaseModel):
583 __tablename__ = 'users'
583 __tablename__ = 'users'
584 __table_args__ = (
584 __table_args__ = (
585 UniqueConstraint('username'), UniqueConstraint('email'),
585 UniqueConstraint('username'), UniqueConstraint('email'),
586 Index('u_username_idx', 'username'),
586 Index('u_username_idx', 'username'),
587 Index('u_email_idx', 'email'),
587 Index('u_email_idx', 'email'),
588 base_table_args
588 base_table_args
589 )
589 )
590
590
591 DEFAULT_USER = 'default'
591 DEFAULT_USER = 'default'
592 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
592 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
593 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
593 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
594 RECOVERY_CODES_COUNT = 10
594 RECOVERY_CODES_COUNT = 10
595
595
596 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
596 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
597 username = Column("username", String(255), nullable=True, unique=None, default=None)
597 username = Column("username", String(255), nullable=True, unique=None, default=None)
598 password = Column("password", String(255), nullable=True, unique=None, default=None)
598 password = Column("password", String(255), nullable=True, unique=None, default=None)
599 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
599 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
600 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
600 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
601 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
601 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
602 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
602 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
603 _email = Column("email", String(255), nullable=True, unique=None, default=None)
603 _email = Column("email", String(255), nullable=True, unique=None, default=None)
604 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
604 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
605 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
605 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
606 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
606 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
607
607
608 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
608 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
609 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
609 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
610 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
610 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
611 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
611 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
612 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
612 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
613 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
613 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
614
614
615 user_log = relationship('UserLog', back_populates='user')
615 user_log = relationship('UserLog', back_populates='user')
616 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
616 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
617
617
618 repositories = relationship('Repository', back_populates='user')
618 repositories = relationship('Repository', back_populates='user')
619 repository_groups = relationship('RepoGroup', back_populates='user')
619 repository_groups = relationship('RepoGroup', back_populates='user')
620 user_groups = relationship('UserGroup', back_populates='user')
620 user_groups = relationship('UserGroup', back_populates='user')
621
621
622 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
622 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
623 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
623 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
624
624
625 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
625 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
626 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
626 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
627 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
627 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
628
628
629 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
629 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
630
630
631 notifications = relationship('UserNotification', cascade='all', back_populates='user')
631 notifications = relationship('UserNotification', cascade='all', back_populates='user')
632 # notifications assigned to this user
632 # notifications assigned to this user
633 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
633 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
634 # comments created by this user
634 # comments created by this user
635 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
635 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
636 # user profile extra info
636 # user profile extra info
637 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
637 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
638 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
638 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
639 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
639 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
640 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
640 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
641
641
642 # gists
642 # gists
643 user_gists = relationship('Gist', cascade='all', back_populates='owner')
643 user_gists = relationship('Gist', cascade='all', back_populates='owner')
644 # user pull requests
644 # user pull requests
645 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
645 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
646
646
647 # external identities
647 # external identities
648 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
648 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
649 # review rules
649 # review rules
650 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
650 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
651
651
652 # artifacts owned
652 # artifacts owned
653 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
653 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
654
654
655 # no cascade, set NULL
655 # no cascade, set NULL
656 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
656 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
657
657
658 def __repr__(self):
658 def __repr__(self):
659 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
659 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
660
660
661 @hybrid_property
661 @hybrid_property
662 def email(self):
662 def email(self):
663 return self._email
663 return self._email
664
664
665 @email.setter
665 @email.setter
666 def email(self, val):
666 def email(self, val):
667 self._email = val.lower() if val else None
667 self._email = val.lower() if val else None
668
668
669 @hybrid_property
669 @hybrid_property
670 def first_name(self):
670 def first_name(self):
671 if self.name:
671 if self.name:
672 return description_escaper(self.name)
672 return description_escaper(self.name)
673 return self.name
673 return self.name
674
674
675 @hybrid_property
675 @hybrid_property
676 def last_name(self):
676 def last_name(self):
677 if self.lastname:
677 if self.lastname:
678 return description_escaper(self.lastname)
678 return description_escaper(self.lastname)
679 return self.lastname
679 return self.lastname
680
680
681 @hybrid_property
681 @hybrid_property
682 def api_key(self):
682 def api_key(self):
683 """
683 """
684 Fetch if exist an auth-token with role ALL connected to this user
684 Fetch if exist an auth-token with role ALL connected to this user
685 """
685 """
686 user_auth_token = UserApiKeys.query()\
686 user_auth_token = UserApiKeys.query()\
687 .filter(UserApiKeys.user_id == self.user_id)\
687 .filter(UserApiKeys.user_id == self.user_id)\
688 .filter(or_(UserApiKeys.expires == -1,
688 .filter(or_(UserApiKeys.expires == -1,
689 UserApiKeys.expires >= time.time()))\
689 UserApiKeys.expires >= time.time()))\
690 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
690 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
691 if user_auth_token:
691 if user_auth_token:
692 user_auth_token = user_auth_token.api_key
692 user_auth_token = user_auth_token.api_key
693
693
694 return user_auth_token
694 return user_auth_token
695
695
696 @api_key.setter
696 @api_key.setter
697 def api_key(self, val):
697 def api_key(self, val):
698 # don't allow to set API key this is deprecated for now
698 # don't allow to set API key this is deprecated for now
699 self._api_key = None
699 self._api_key = None
700
700
701 @property
701 @property
702 def reviewer_pull_requests(self):
702 def reviewer_pull_requests(self):
703 return PullRequestReviewers.query() \
703 return PullRequestReviewers.query() \
704 .options(joinedload(PullRequestReviewers.pull_request)) \
704 .options(joinedload(PullRequestReviewers.pull_request)) \
705 .filter(PullRequestReviewers.user_id == self.user_id) \
705 .filter(PullRequestReviewers.user_id == self.user_id) \
706 .all()
706 .all()
707
707
708 @property
708 @property
709 def firstname(self):
709 def firstname(self):
710 # alias for future
710 # alias for future
711 return self.name
711 return self.name
712
712
713 @property
713 @property
714 def emails(self):
714 def emails(self):
715 other = UserEmailMap.query()\
715 other = UserEmailMap.query()\
716 .filter(UserEmailMap.user == self) \
716 .filter(UserEmailMap.user == self) \
717 .order_by(UserEmailMap.email_id.asc()) \
717 .order_by(UserEmailMap.email_id.asc()) \
718 .all()
718 .all()
719 return [self.email] + [x.email for x in other]
719 return [self.email] + [x.email for x in other]
720
720
721 def emails_cached(self):
721 def emails_cached(self):
722 emails = []
722 emails = []
723 if self.user_id != self.get_default_user_id():
723 if self.user_id != self.get_default_user_id():
724 emails = UserEmailMap.query()\
724 emails = UserEmailMap.query()\
725 .filter(UserEmailMap.user == self) \
725 .filter(UserEmailMap.user == self) \
726 .order_by(UserEmailMap.email_id.asc())
726 .order_by(UserEmailMap.email_id.asc())
727
727
728 emails = emails.options(
728 emails = emails.options(
729 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
729 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
730 )
730 )
731
731
732 return [self.email] + [x.email for x in emails]
732 return [self.email] + [x.email for x in emails]
733
733
734 @property
734 @property
735 def auth_tokens(self):
735 def auth_tokens(self):
736 auth_tokens = self.get_auth_tokens()
736 auth_tokens = self.get_auth_tokens()
737 return [x.api_key for x in auth_tokens]
737 return [x.api_key for x in auth_tokens]
738
738
739 def get_auth_tokens(self):
739 def get_auth_tokens(self):
740 return UserApiKeys.query()\
740 return UserApiKeys.query()\
741 .filter(UserApiKeys.user == self)\
741 .filter(UserApiKeys.user == self)\
742 .order_by(UserApiKeys.user_api_key_id.asc())\
742 .order_by(UserApiKeys.user_api_key_id.asc())\
743 .all()
743 .all()
744
744
745 @LazyProperty
745 @LazyProperty
746 def feed_token(self):
746 def feed_token(self):
747 return self.get_feed_token()
747 return self.get_feed_token()
748
748
749 def get_feed_token(self, cache=True):
749 def get_feed_token(self, cache=True):
750 feed_tokens = UserApiKeys.query()\
750 feed_tokens = UserApiKeys.query()\
751 .filter(UserApiKeys.user == self)\
751 .filter(UserApiKeys.user == self)\
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
753 if cache:
753 if cache:
754 feed_tokens = feed_tokens.options(
754 feed_tokens = feed_tokens.options(
755 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
755 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
756
756
757 feed_tokens = feed_tokens.all()
757 feed_tokens = feed_tokens.all()
758 if feed_tokens:
758 if feed_tokens:
759 return feed_tokens[0].api_key
759 return feed_tokens[0].api_key
760 return 'NO_FEED_TOKEN_AVAILABLE'
760 return 'NO_FEED_TOKEN_AVAILABLE'
761
761
762 @LazyProperty
762 @LazyProperty
763 def artifact_token(self):
763 def artifact_token(self):
764 return self.get_artifact_token()
764 return self.get_artifact_token()
765
765
766 def get_artifact_token(self, cache=True):
766 def get_artifact_token(self, cache=True):
767 artifacts_tokens = UserApiKeys.query()\
767 artifacts_tokens = UserApiKeys.query()\
768 .filter(UserApiKeys.user == self) \
768 .filter(UserApiKeys.user == self) \
769 .filter(or_(UserApiKeys.expires == -1,
769 .filter(or_(UserApiKeys.expires == -1,
770 UserApiKeys.expires >= time.time())) \
770 UserApiKeys.expires >= time.time())) \
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772
772
773 if cache:
773 if cache:
774 artifacts_tokens = artifacts_tokens.options(
774 artifacts_tokens = artifacts_tokens.options(
775 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
775 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
776
776
777 artifacts_tokens = artifacts_tokens.all()
777 artifacts_tokens = artifacts_tokens.all()
778 if artifacts_tokens:
778 if artifacts_tokens:
779 return artifacts_tokens[0].api_key
779 return artifacts_tokens[0].api_key
780 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
780 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
781
781
782 def get_or_create_artifact_token(self):
782 def get_or_create_artifact_token(self):
783 artifacts_tokens = UserApiKeys.query()\
783 artifacts_tokens = UserApiKeys.query()\
784 .filter(UserApiKeys.user == self) \
784 .filter(UserApiKeys.user == self) \
785 .filter(or_(UserApiKeys.expires == -1,
785 .filter(or_(UserApiKeys.expires == -1,
786 UserApiKeys.expires >= time.time())) \
786 UserApiKeys.expires >= time.time())) \
787 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
787 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
788
788
789 artifacts_tokens = artifacts_tokens.all()
789 artifacts_tokens = artifacts_tokens.all()
790 if artifacts_tokens:
790 if artifacts_tokens:
791 return artifacts_tokens[0].api_key
791 return artifacts_tokens[0].api_key
792 else:
792 else:
793 from rhodecode.model.auth_token import AuthTokenModel
793 from rhodecode.model.auth_token import AuthTokenModel
794 artifact_token = AuthTokenModel().create(
794 artifact_token = AuthTokenModel().create(
795 self, 'auto-generated-artifact-token',
795 self, 'auto-generated-artifact-token',
796 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
796 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
797 Session.commit()
797 Session.commit()
798 return artifact_token.api_key
798 return artifact_token.api_key
799
799
800 def is_totp_valid(self, received_code, secret):
800 def is_totp_valid(self, received_code, secret):
801 totp = pyotp.TOTP(secret)
801 totp = pyotp.TOTP(secret)
802 return totp.verify(received_code)
802 return totp.verify(received_code)
803
803
804 def is_2fa_recovery_code_valid(self, received_code, secret):
804 def is_2fa_recovery_code_valid(self, received_code, secret):
805 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
805 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
806 recovery_codes = self.get_2fa_recovery_codes()
806 recovery_codes = self.get_2fa_recovery_codes()
807 if received_code in recovery_codes:
807 if received_code in recovery_codes:
808 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
808 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
809 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
809 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
810 return True
810 return True
811 return False
811 return False
812
812
813 @hybrid_property
813 @hybrid_property
814 def has_forced_2fa(self):
814 def has_forced_2fa(self):
815 """
815 """
816 Checks if 2fa was forced for current user
816 Checks if 2fa was forced for current user
817 """
817 """
818 from rhodecode.model.settings import SettingsModel
818 from rhodecode.model.settings import SettingsModel
819 if value := SettingsModel().get_setting_by_name(f'auth_{self.extern_type}_global_2fa'):
819 if value := SettingsModel().get_setting_by_name(f'auth_{self.extern_type}_global_2fa'):
820 return value.app_settings_value
820 return value.app_settings_value
821 return False
821 return False
822
822
823 @hybrid_property
823 @hybrid_property
824 def has_enabled_2fa(self):
824 def has_enabled_2fa(self):
825 """
825 """
826 Checks if user enabled 2fa
826 Checks if user enabled 2fa
827 """
827 """
828 if value := self.has_forced_2fa:
828 if value := self.has_forced_2fa:
829 return value
829 return value
830 return self.user_data.get('enabled_2fa', False)
830 return self.user_data.get('enabled_2fa', False)
831
831
832 @has_enabled_2fa.setter
832 @has_enabled_2fa.setter
833 def has_enabled_2fa(self, val):
833 def has_enabled_2fa(self, val):
834 val = str2bool(val)
834 val = str2bool(val)
835 self.update_userdata(enabled_2fa=val)
835 self.update_userdata(enabled_2fa=val)
836 if not val:
836 if not val:
837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
839 Session().commit()
839 Session().commit()
840
840
841 @hybrid_property
841 @hybrid_property
842 def check_2fa_required(self):
842 def check_2fa_required(self):
843 """
843 """
844 Check if check 2fa flag is set for this user
844 Check if check 2fa flag is set for this user
845 """
845 """
846 value = self.user_data.get('check_2fa', False)
846 value = self.user_data.get('check_2fa', False)
847 return value
847 return value
848
848
849 @check_2fa_required.setter
849 @check_2fa_required.setter
850 def check_2fa_required(self, val):
850 def check_2fa_required(self, val):
851 val = str2bool(val)
851 val = str2bool(val)
852 self.update_userdata(check_2fa=val)
852 self.update_userdata(check_2fa=val)
853 Session().commit()
853 Session().commit()
854
854
855 @hybrid_property
855 @hybrid_property
856 def has_seen_2fa_codes(self):
856 def has_seen_2fa_codes(self):
857 """
857 """
858 get the flag about if user has seen 2fa recovery codes
858 get the flag about if user has seen 2fa recovery codes
859 """
859 """
860 value = self.user_data.get('recovery_codes_2fa_seen', False)
860 value = self.user_data.get('recovery_codes_2fa_seen', False)
861 return value
861 return value
862
862
863 @has_seen_2fa_codes.setter
863 @has_seen_2fa_codes.setter
864 def has_seen_2fa_codes(self, val):
864 def has_seen_2fa_codes(self, val):
865 val = str2bool(val)
865 val = str2bool(val)
866 self.update_userdata(recovery_codes_2fa_seen=val)
866 self.update_userdata(recovery_codes_2fa_seen=val)
867 Session().commit()
867 Session().commit()
868
868
869 @hybrid_property
869 @hybrid_property
870 def needs_2fa_configure(self):
870 def needs_2fa_configure(self):
871 """
871 """
872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
873
873
874 Currently this is 2fa enabled and secret exists
874 Currently this is 2fa enabled and secret exists
875 """
875 """
876 if self.has_enabled_2fa:
876 if self.has_enabled_2fa:
877 return not self.user_data.get('secret_2fa')
877 return not self.user_data.get('secret_2fa')
878 return False
878 return False
879
879
880 def init_2fa_recovery_codes(self, persist=True, force=False):
880 def init_2fa_recovery_codes(self, persist=True, force=False):
881 """
881 """
882 Creates 2fa recovery codes
882 Creates 2fa recovery codes
883 """
883 """
884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
885 encrypted_codes = []
885 encrypted_codes = []
886 if not recovery_codes or force:
886 if not recovery_codes or force:
887 for _ in range(self.RECOVERY_CODES_COUNT):
887 for _ in range(self.RECOVERY_CODES_COUNT):
888 recovery_code = pyotp.random_base32()
888 recovery_code = pyotp.random_base32()
889 recovery_codes.append(recovery_code)
889 recovery_codes.append(recovery_code)
890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
891 encrypted_codes.append(safe_str(encrypted_code))
891 encrypted_codes.append(safe_str(encrypted_code))
892 if persist:
892 if persist:
893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
894 return recovery_codes
894 return recovery_codes
895 # User should not check the same recovery codes more than once
895 # User should not check the same recovery codes more than once
896 return []
896 return []
897
897
898 def get_2fa_recovery_codes(self):
898 def get_2fa_recovery_codes(self):
899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
900
900
901 recovery_codes = list(map(
901 recovery_codes = list(map(
902 lambda val: safe_str(
902 lambda val: safe_str(
903 enc_utils.decrypt_value(
903 enc_utils.decrypt_value(
904 val,
904 val,
905 enc_key=ENCRYPTION_KEY
905 enc_key=ENCRYPTION_KEY
906 )),
906 )),
907 encrypted_recovery_codes))
907 encrypted_recovery_codes))
908 return recovery_codes
908 return recovery_codes
909
909
910 def init_secret_2fa(self, persist=True, force=False):
910 def init_secret_2fa(self, persist=True, force=False):
911 secret_2fa = self.user_data.get('secret_2fa')
911 secret_2fa = self.user_data.get('secret_2fa')
912 if not secret_2fa or force:
912 if not secret_2fa or force:
913 secret = pyotp.random_base32()
913 secret = pyotp.random_base32()
914 if persist:
914 if persist:
915 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
915 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
916 return secret
916 return secret
917 return ''
917 return ''
918
918
919 @hybrid_property
919 @hybrid_property
920 def secret_2fa(self) -> str:
920 def secret_2fa(self) -> str:
921 """
921 """
922 get stored secret for 2fa
922 get stored secret for 2fa
923 """
923 """
924 secret_2fa = self.user_data.get('secret_2fa')
924 secret_2fa = self.user_data.get('secret_2fa')
925 if secret_2fa:
925 if secret_2fa:
926 return safe_str(
926 return safe_str(
927 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
927 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
928 return ''
928 return ''
929
929
930 @secret_2fa.setter
930 @secret_2fa.setter
931 def secret_2fa(self, value: str) -> None:
931 def secret_2fa(self, value: str) -> None:
932 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
932 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
933 self.update_userdata(secret_2fa=safe_str(encrypted_value))
933 self.update_userdata(secret_2fa=safe_str(encrypted_value))
934
934
935 def regenerate_2fa_recovery_codes(self):
935 def regenerate_2fa_recovery_codes(self):
936 """
936 """
937 Regenerates 2fa recovery codes upon request
937 Regenerates 2fa recovery codes upon request
938 """
938 """
939 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
939 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
940 Session().commit()
940 Session().commit()
941 return new_recovery_codes
941 return new_recovery_codes
942
942
943 @classmethod
943 @classmethod
944 def extra_valid_auth_tokens(cls, user, role=None):
944 def extra_valid_auth_tokens(cls, user, role=None):
945 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
945 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
946 .filter(or_(UserApiKeys.expires == -1,
946 .filter(or_(UserApiKeys.expires == -1,
947 UserApiKeys.expires >= time.time()))
947 UserApiKeys.expires >= time.time()))
948 if role:
948 if role:
949 tokens = tokens.filter(or_(UserApiKeys.role == role,
949 tokens = tokens.filter(or_(UserApiKeys.role == role,
950 UserApiKeys.role == UserApiKeys.ROLE_ALL))
950 UserApiKeys.role == UserApiKeys.ROLE_ALL))
951 return tokens.all()
951 return tokens.all()
952
952
953 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
953 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
954 from rhodecode.lib import auth
954 from rhodecode.lib import auth
955
955
956 log.debug('Trying to authenticate user: %s via auth-token, '
956 log.debug('Trying to authenticate user: %s via auth-token, '
957 'and roles: %s', self, roles)
957 'and roles: %s', self, roles)
958
958
959 if not auth_token:
959 if not auth_token:
960 return False
960 return False
961
961
962 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
962 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
963 tokens_q = UserApiKeys.query()\
963 tokens_q = UserApiKeys.query()\
964 .filter(UserApiKeys.user_id == self.user_id)\
964 .filter(UserApiKeys.user_id == self.user_id)\
965 .filter(or_(UserApiKeys.expires == -1,
965 .filter(or_(UserApiKeys.expires == -1,
966 UserApiKeys.expires >= time.time()))
966 UserApiKeys.expires >= time.time()))
967
967
968 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
968 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
969
969
970 crypto_backend = auth.crypto_backend()
970 crypto_backend = auth.crypto_backend()
971 enc_token_map = {}
971 enc_token_map = {}
972 plain_token_map = {}
972 plain_token_map = {}
973 for token in tokens_q:
973 for token in tokens_q:
974 if token.api_key.startswith(crypto_backend.ENC_PREF):
974 if token.api_key.startswith(crypto_backend.ENC_PREF):
975 enc_token_map[token.api_key] = token
975 enc_token_map[token.api_key] = token
976 else:
976 else:
977 plain_token_map[token.api_key] = token
977 plain_token_map[token.api_key] = token
978 log.debug(
978 log.debug(
979 'Found %s plain and %s encrypted tokens to check for authentication for this user',
979 'Found %s plain and %s encrypted tokens to check for authentication for this user',
980 len(plain_token_map), len(enc_token_map))
980 len(plain_token_map), len(enc_token_map))
981
981
982 # plain token match comes first
982 # plain token match comes first
983 match = plain_token_map.get(auth_token)
983 match = plain_token_map.get(auth_token)
984
984
985 # check encrypted tokens now
985 # check encrypted tokens now
986 if not match:
986 if not match:
987 for token_hash, token in enc_token_map.items():
987 for token_hash, token in enc_token_map.items():
988 # NOTE(marcink): this is expensive to calculate, but most secure
988 # NOTE(marcink): this is expensive to calculate, but most secure
989 if crypto_backend.hash_check(auth_token, token_hash):
989 if crypto_backend.hash_check(auth_token, token_hash):
990 match = token
990 match = token
991 break
991 break
992
992
993 if match:
993 if match:
994 log.debug('Found matching token %s', match)
994 log.debug('Found matching token %s', match)
995 if match.repo_id:
995 if match.repo_id:
996 log.debug('Found scope, checking for scope match of token %s', match)
996 log.debug('Found scope, checking for scope match of token %s', match)
997 if match.repo_id == scope_repo_id:
997 if match.repo_id == scope_repo_id:
998 return True
998 return True
999 else:
999 else:
1000 log.debug(
1000 log.debug(
1001 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1001 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1002 'and calling scope is:%s, skipping further checks',
1002 'and calling scope is:%s, skipping further checks',
1003 match.repo, scope_repo_id)
1003 match.repo, scope_repo_id)
1004 return False
1004 return False
1005 else:
1005 else:
1006 return True
1006 return True
1007
1007
1008 return False
1008 return False
1009
1009
1010 @property
1010 @property
1011 def ip_addresses(self):
1011 def ip_addresses(self):
1012 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1012 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1013 return [x.ip_addr for x in ret]
1013 return [x.ip_addr for x in ret]
1014
1014
1015 @property
1015 @property
1016 def username_and_name(self):
1016 def username_and_name(self):
1017 return f'{self.username} ({self.first_name} {self.last_name})'
1017 return f'{self.username} ({self.first_name} {self.last_name})'
1018
1018
1019 @property
1019 @property
1020 def username_or_name_or_email(self):
1020 def username_or_name_or_email(self):
1021 full_name = self.full_name if self.full_name != ' ' else None
1021 full_name = self.full_name if self.full_name != ' ' else None
1022 return self.username or full_name or self.email
1022 return self.username or full_name or self.email
1023
1023
1024 @property
1024 @property
1025 def full_name(self):
1025 def full_name(self):
1026 return f'{self.first_name} {self.last_name}'
1026 return f'{self.first_name} {self.last_name}'
1027
1027
1028 @property
1028 @property
1029 def full_name_or_username(self):
1029 def full_name_or_username(self):
1030 return (f'{self.first_name} {self.last_name}'
1030 return (f'{self.first_name} {self.last_name}'
1031 if (self.first_name and self.last_name) else self.username)
1031 if (self.first_name and self.last_name) else self.username)
1032
1032
1033 @property
1033 @property
1034 def full_contact(self):
1034 def full_contact(self):
1035 return f'{self.first_name} {self.last_name} <{self.email}>'
1035 return f'{self.first_name} {self.last_name} <{self.email}>'
1036
1036
1037 @property
1037 @property
1038 def short_contact(self):
1038 def short_contact(self):
1039 return f'{self.first_name} {self.last_name}'
1039 return f'{self.first_name} {self.last_name}'
1040
1040
1041 @property
1041 @property
1042 def is_admin(self):
1042 def is_admin(self):
1043 return self.admin
1043 return self.admin
1044
1044
1045 @property
1045 @property
1046 def language(self):
1046 def language(self):
1047 return self.user_data.get('language')
1047 return self.user_data.get('language')
1048
1048
1049 def AuthUser(self, **kwargs):
1049 def AuthUser(self, **kwargs):
1050 """
1050 """
1051 Returns instance of AuthUser for this user
1051 Returns instance of AuthUser for this user
1052 """
1052 """
1053 from rhodecode.lib.auth import AuthUser
1053 from rhodecode.lib.auth import AuthUser
1054 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1054 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1055
1055
1056 @hybrid_property
1056 @hybrid_property
1057 def user_data(self):
1057 def user_data(self):
1058 if not self._user_data:
1058 if not self._user_data:
1059 return {}
1059 return {}
1060
1060
1061 try:
1061 try:
1062 return json.loads(self._user_data) or {}
1062 return json.loads(self._user_data) or {}
1063 except TypeError:
1063 except TypeError:
1064 return {}
1064 return {}
1065
1065
1066 @user_data.setter
1066 @user_data.setter
1067 def user_data(self, val):
1067 def user_data(self, val):
1068 if not isinstance(val, dict):
1068 if not isinstance(val, dict):
1069 raise Exception(f'user_data must be dict, got {type(val)}')
1069 raise Exception(f'user_data must be dict, got {type(val)}')
1070 try:
1070 try:
1071 self._user_data = safe_bytes(json.dumps(val))
1071 self._user_data = safe_bytes(json.dumps(val))
1072 except Exception:
1072 except Exception:
1073 log.error(traceback.format_exc())
1073 log.error(traceback.format_exc())
1074
1074
1075 @classmethod
1075 @classmethod
1076 def get(cls, user_id, cache=False):
1076 def get(cls, user_id, cache=False):
1077 if not user_id:
1077 if not user_id:
1078 return
1078 return
1079
1079
1080 user = cls.query()
1080 q = cls.select().where(cls.user_id == user_id)
1081 if cache:
1081 if cache:
1082 user = user.options(
1082 q = q.options(
1083 FromCache("sql_cache_short", f"get_users_{user_id}"))
1083 FromCache("sql_cache_short", f"get_users_{user_id}"))
1084 return user.get(user_id)
1084 return cls.execute(q).scalar_one_or_none()
1085
1085
1086 @classmethod
1086 @classmethod
1087 def get_by_username(cls, username, case_insensitive=False,
1087 def get_by_username(cls, username, case_insensitive=False,
1088 cache=False):
1088 cache=False):
1089
1089
1090 if case_insensitive:
1090 if case_insensitive:
1091 q = cls.select().where(
1091 q = cls.select().where(
1092 func.lower(cls.username) == func.lower(username))
1092 func.lower(cls.username) == func.lower(username))
1093 else:
1093 else:
1094 q = cls.select().where(cls.username == username)
1094 q = cls.select().where(cls.username == username)
1095
1095
1096 if cache:
1096 if cache:
1097 hash_key = _hash_key(username)
1097 hash_key = _hash_key(username)
1098 q = q.options(
1098 q = q.options(
1099 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1099 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1100
1100
1101 return cls.execute(q).scalar_one_or_none()
1101 return cls.execute(q).scalar_one_or_none()
1102
1102
1103 @classmethod
1103 @classmethod
1104 def get_by_username_or_primary_email(cls, user_identifier):
1104 def get_by_username_or_primary_email(cls, user_identifier):
1105 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1105 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1106 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1106 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1107 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1107 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1108
1108
1109 @classmethod
1109 @classmethod
1110 def get_by_auth_token(cls, auth_token, cache=False):
1110 def get_by_auth_token(cls, auth_token, cache=False):
1111
1111
1112 q = cls.select(User)\
1112 q = cls.select(User)\
1113 .join(UserApiKeys)\
1113 .join(UserApiKeys)\
1114 .where(UserApiKeys.api_key == auth_token)\
1114 .where(UserApiKeys.api_key == auth_token)\
1115 .where(or_(UserApiKeys.expires == -1,
1115 .where(or_(UserApiKeys.expires == -1,
1116 UserApiKeys.expires >= time.time()))
1116 UserApiKeys.expires >= time.time()))
1117
1117
1118 if cache:
1118 if cache:
1119 q = q.options(
1119 q = q.options(
1120 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1120 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1121
1121
1122 matched_user = cls.execute(q).scalar_one_or_none()
1122 matched_user = cls.execute(q).scalar_one_or_none()
1123
1123
1124 return matched_user
1124 return matched_user
1125
1125
1126 @classmethod
1126 @classmethod
1127 def get_by_email(cls, email, case_insensitive=False, cache=False):
1127 def get_by_email(cls, email, case_insensitive=False, cache=False):
1128
1128
1129 if case_insensitive:
1129 if case_insensitive:
1130 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1130 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1131 else:
1131 else:
1132 q = cls.select().where(cls.email == email)
1132 q = cls.select().where(cls.email == email)
1133
1133
1134 if cache:
1134 if cache:
1135 email_key = _hash_key(email)
1135 email_key = _hash_key(email)
1136 q = q.options(
1136 q = q.options(
1137 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1137 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1138
1138
1139 ret = cls.execute(q).scalar_one_or_none()
1139 ret = cls.execute(q).scalar_one_or_none()
1140
1140
1141 if ret is None:
1141 if ret is None:
1142 q = cls.select(UserEmailMap)
1142 q = cls.select(UserEmailMap)
1143 # try fetching in alternate email map
1143 # try fetching in alternate email map
1144 if case_insensitive:
1144 if case_insensitive:
1145 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1145 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1146 else:
1146 else:
1147 q = q.where(UserEmailMap.email == email)
1147 q = q.where(UserEmailMap.email == email)
1148 q = q.options(joinedload(UserEmailMap.user))
1148 q = q.options(joinedload(UserEmailMap.user))
1149 if cache:
1149 if cache:
1150 q = q.options(
1150 q = q.options(
1151 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1151 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1152
1152
1153 result = cls.execute(q).scalar_one_or_none()
1153 result = cls.execute(q).scalar_one_or_none()
1154 ret = getattr(result, 'user', None)
1154 ret = getattr(result, 'user', None)
1155
1155
1156 return ret
1156 return ret
1157
1157
1158 @classmethod
1158 @classmethod
1159 def get_from_cs_author(cls, author):
1159 def get_from_cs_author(cls, author):
1160 """
1160 """
1161 Tries to get User objects out of commit author string
1161 Tries to get User objects out of commit author string
1162
1162
1163 :param author:
1163 :param author:
1164 """
1164 """
1165 from rhodecode.lib.helpers import email, author_name
1165 from rhodecode.lib.helpers import email, author_name
1166 # Valid email in the attribute passed, see if they're in the system
1166 # Valid email in the attribute passed, see if they're in the system
1167 _email = email(author)
1167 _email = email(author)
1168 if _email:
1168 if _email:
1169 user = cls.get_by_email(_email, case_insensitive=True)
1169 user = cls.get_by_email(_email, case_insensitive=True)
1170 if user:
1170 if user:
1171 return user
1171 return user
1172 # Maybe we can match by username?
1172 # Maybe we can match by username?
1173 _author = author_name(author)
1173 _author = author_name(author)
1174 user = cls.get_by_username(_author, case_insensitive=True)
1174 user = cls.get_by_username(_author, case_insensitive=True)
1175 if user:
1175 if user:
1176 return user
1176 return user
1177
1177
1178 def update_userdata(self, **kwargs):
1178 def update_userdata(self, **kwargs):
1179 usr = self
1179 usr = self
1180 old = usr.user_data
1180 old = usr.user_data
1181 old.update(**kwargs)
1181 old.update(**kwargs)
1182 usr.user_data = old
1182 usr.user_data = old
1183 Session().add(usr)
1183 Session().add(usr)
1184 log.debug('updated userdata with %s', kwargs)
1184 log.debug('updated userdata with %s', kwargs)
1185
1185
1186 def update_lastlogin(self):
1186 def update_lastlogin(self):
1187 """Update user lastlogin"""
1187 """Update user lastlogin"""
1188 self.last_login = datetime.datetime.now()
1188 self.last_login = datetime.datetime.now()
1189 Session().add(self)
1189 Session().add(self)
1190 log.debug('updated user %s lastlogin', self.username)
1190 log.debug('updated user %s lastlogin', self.username)
1191
1191
1192 def update_password(self, new_password):
1192 def update_password(self, new_password):
1193 from rhodecode.lib.auth import get_crypt_password
1193 from rhodecode.lib.auth import get_crypt_password
1194
1194
1195 self.password = get_crypt_password(new_password)
1195 self.password = get_crypt_password(new_password)
1196 Session().add(self)
1196 Session().add(self)
1197
1197
1198 @classmethod
1198 @classmethod
1199 def get_first_super_admin(cls):
1199 def get_first_super_admin(cls):
1200 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1200 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1201 user = cls.scalars(stmt).first()
1201 user = cls.scalars(stmt).first()
1202
1202
1203 if user is None:
1203 if user is None:
1204 raise Exception('FATAL: Missing administrative account!')
1204 raise Exception('FATAL: Missing administrative account!')
1205 return user
1205 return user
1206
1206
1207 @classmethod
1207 @classmethod
1208 def get_all_super_admins(cls, only_active=False):
1208 def get_all_super_admins(cls, only_active=False):
1209 """
1209 """
1210 Returns all admin accounts sorted by username
1210 Returns all admin accounts sorted by username
1211 """
1211 """
1212 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1212 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1213 if only_active:
1213 if only_active:
1214 qry = qry.filter(User.active == true())
1214 qry = qry.filter(User.active == true())
1215 return qry.all()
1215 return qry.all()
1216
1216
1217 @classmethod
1217 @classmethod
1218 def get_all_user_ids(cls, only_active=True):
1218 def get_all_user_ids(cls, only_active=True):
1219 """
1219 """
1220 Returns all users IDs
1220 Returns all users IDs
1221 """
1221 """
1222 qry = Session().query(User.user_id)
1222 qry = Session().query(User.user_id)
1223
1223
1224 if only_active:
1224 if only_active:
1225 qry = qry.filter(User.active == true())
1225 qry = qry.filter(User.active == true())
1226 return [x.user_id for x in qry]
1226 return [x.user_id for x in qry]
1227
1227
1228 @classmethod
1228 @classmethod
1229 def get_default_user(cls, cache=False, refresh=False):
1229 def get_default_user(cls, cache=False, refresh=False):
1230 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1230 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1231 if user is None:
1231 if user is None:
1232 raise Exception('FATAL: Missing default account!')
1232 raise Exception('FATAL: Missing default account!')
1233 if refresh:
1233 if refresh:
1234 # The default user might be based on outdated state which
1234 # The default user might be based on outdated state which
1235 # has been loaded from the cache.
1235 # has been loaded from the cache.
1236 # A call to refresh() ensures that the
1236 # A call to refresh() ensures that the
1237 # latest state from the database is used.
1237 # latest state from the database is used.
1238 Session().refresh(user)
1238 Session().refresh(user)
1239
1239
1240 return user
1240 return user
1241
1241
1242 @classmethod
1242 @classmethod
1243 def get_default_user_id(cls):
1243 def get_default_user_id(cls):
1244 import rhodecode
1244 import rhodecode
1245 return rhodecode.CONFIG['default_user_id']
1245 return rhodecode.CONFIG['default_user_id']
1246
1246
1247 def _get_default_perms(self, user, suffix=''):
1247 def _get_default_perms(self, user, suffix=''):
1248 from rhodecode.model.permission import PermissionModel
1248 from rhodecode.model.permission import PermissionModel
1249 return PermissionModel().get_default_perms(user.user_perms, suffix)
1249 return PermissionModel().get_default_perms(user.user_perms, suffix)
1250
1250
1251 def get_default_perms(self, suffix=''):
1251 def get_default_perms(self, suffix=''):
1252 return self._get_default_perms(self, suffix)
1252 return self._get_default_perms(self, suffix)
1253
1253
1254 def get_api_data(self, include_secrets=False, details='full'):
1254 def get_api_data(self, include_secrets=False, details='full'):
1255 """
1255 """
1256 Common function for generating user related data for API
1256 Common function for generating user related data for API
1257
1257
1258 :param include_secrets: By default secrets in the API data will be replaced
1258 :param include_secrets: By default secrets in the API data will be replaced
1259 by a placeholder value to prevent exposing this data by accident. In case
1259 by a placeholder value to prevent exposing this data by accident. In case
1260 this data shall be exposed, set this flag to ``True``.
1260 this data shall be exposed, set this flag to ``True``.
1261
1261
1262 :param details: details can be 'basic|full' basic gives only a subset of
1262 :param details: details can be 'basic|full' basic gives only a subset of
1263 the available user information that includes user_id, name and emails.
1263 the available user information that includes user_id, name and emails.
1264 """
1264 """
1265 user = self
1265 user = self
1266 user_data = self.user_data
1266 user_data = self.user_data
1267 data = {
1267 data = {
1268 'user_id': user.user_id,
1268 'user_id': user.user_id,
1269 'username': user.username,
1269 'username': user.username,
1270 'firstname': user.name,
1270 'firstname': user.name,
1271 'lastname': user.lastname,
1271 'lastname': user.lastname,
1272 'description': user.description,
1272 'description': user.description,
1273 'email': user.email,
1273 'email': user.email,
1274 'emails': user.emails,
1274 'emails': user.emails,
1275 }
1275 }
1276 if details == 'basic':
1276 if details == 'basic':
1277 return data
1277 return data
1278
1278
1279 auth_token_length = 40
1279 auth_token_length = 40
1280 auth_token_replacement = '*' * auth_token_length
1280 auth_token_replacement = '*' * auth_token_length
1281
1281
1282 extras = {
1282 extras = {
1283 'auth_tokens': [auth_token_replacement],
1283 'auth_tokens': [auth_token_replacement],
1284 'active': user.active,
1284 'active': user.active,
1285 'admin': user.admin,
1285 'admin': user.admin,
1286 'extern_type': user.extern_type,
1286 'extern_type': user.extern_type,
1287 'extern_name': user.extern_name,
1287 'extern_name': user.extern_name,
1288 'last_login': user.last_login,
1288 'last_login': user.last_login,
1289 'last_activity': user.last_activity,
1289 'last_activity': user.last_activity,
1290 'ip_addresses': user.ip_addresses,
1290 'ip_addresses': user.ip_addresses,
1291 'language': user_data.get('language')
1291 'language': user_data.get('language')
1292 }
1292 }
1293 data.update(extras)
1293 data.update(extras)
1294
1294
1295 if include_secrets:
1295 if include_secrets:
1296 data['auth_tokens'] = user.auth_tokens
1296 data['auth_tokens'] = user.auth_tokens
1297 return data
1297 return data
1298
1298
1299 def __json__(self):
1299 def __json__(self):
1300 data = {
1300 data = {
1301 'full_name': self.full_name,
1301 'full_name': self.full_name,
1302 'full_name_or_username': self.full_name_or_username,
1302 'full_name_or_username': self.full_name_or_username,
1303 'short_contact': self.short_contact,
1303 'short_contact': self.short_contact,
1304 'full_contact': self.full_contact,
1304 'full_contact': self.full_contact,
1305 }
1305 }
1306 data.update(self.get_api_data())
1306 data.update(self.get_api_data())
1307 return data
1307 return data
1308
1308
1309
1309
1310 class UserApiKeys(Base, BaseModel):
1310 class UserApiKeys(Base, BaseModel):
1311 __tablename__ = 'user_api_keys'
1311 __tablename__ = 'user_api_keys'
1312 __table_args__ = (
1312 __table_args__ = (
1313 Index('uak_api_key_idx', 'api_key'),
1313 Index('uak_api_key_idx', 'api_key'),
1314 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1314 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1315 base_table_args
1315 base_table_args
1316 )
1316 )
1317
1317
1318 # ApiKey role
1318 # ApiKey role
1319 ROLE_ALL = 'token_role_all'
1319 ROLE_ALL = 'token_role_all'
1320 ROLE_VCS = 'token_role_vcs'
1320 ROLE_VCS = 'token_role_vcs'
1321 ROLE_API = 'token_role_api'
1321 ROLE_API = 'token_role_api'
1322 ROLE_HTTP = 'token_role_http'
1322 ROLE_HTTP = 'token_role_http'
1323 ROLE_FEED = 'token_role_feed'
1323 ROLE_FEED = 'token_role_feed'
1324 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1324 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1325 # The last one is ignored in the list as we only
1325 # The last one is ignored in the list as we only
1326 # use it for one action, and cannot be created by users
1326 # use it for one action, and cannot be created by users
1327 ROLE_PASSWORD_RESET = 'token_password_reset'
1327 ROLE_PASSWORD_RESET = 'token_password_reset'
1328
1328
1329 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1329 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1330
1330
1331 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1331 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1332 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1332 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1333 api_key = Column("api_key", String(255), nullable=False, unique=True)
1333 api_key = Column("api_key", String(255), nullable=False, unique=True)
1334 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1334 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1335 expires = Column('expires', Float(53), nullable=False)
1335 expires = Column('expires', Float(53), nullable=False)
1336 role = Column('role', String(255), nullable=True)
1336 role = Column('role', String(255), nullable=True)
1337 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1337 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1338
1338
1339 # scope columns
1339 # scope columns
1340 repo_id = Column(
1340 repo_id = Column(
1341 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1341 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1342 nullable=True, unique=None, default=None)
1342 nullable=True, unique=None, default=None)
1343 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1343 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1344
1344
1345 repo_group_id = Column(
1345 repo_group_id = Column(
1346 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1346 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1347 nullable=True, unique=None, default=None)
1347 nullable=True, unique=None, default=None)
1348 repo_group = relationship('RepoGroup', lazy='joined')
1348 repo_group = relationship('RepoGroup', lazy='joined')
1349
1349
1350 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1350 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1351
1351
1352 def __repr__(self):
1352 def __repr__(self):
1353 return f"<{self.cls_name}('{self.role}')>"
1353 return f"<{self.cls_name}('{self.role}')>"
1354
1354
1355 def __json__(self):
1355 def __json__(self):
1356 data = {
1356 data = {
1357 'auth_token': self.api_key,
1357 'auth_token': self.api_key,
1358 'role': self.role,
1358 'role': self.role,
1359 'scope': self.scope_humanized,
1359 'scope': self.scope_humanized,
1360 'expired': self.expired
1360 'expired': self.expired
1361 }
1361 }
1362 return data
1362 return data
1363
1363
1364 def get_api_data(self, include_secrets=False):
1364 def get_api_data(self, include_secrets=False):
1365 data = self.__json__()
1365 data = self.__json__()
1366 if include_secrets:
1366 if include_secrets:
1367 return data
1367 return data
1368 else:
1368 else:
1369 data['auth_token'] = self.token_obfuscated
1369 data['auth_token'] = self.token_obfuscated
1370 return data
1370 return data
1371
1371
1372 @hybrid_property
1372 @hybrid_property
1373 def description_safe(self):
1373 def description_safe(self):
1374 return description_escaper(self.description)
1374 return description_escaper(self.description)
1375
1375
1376 @property
1376 @property
1377 def expired(self):
1377 def expired(self):
1378 if self.expires == -1:
1378 if self.expires == -1:
1379 return False
1379 return False
1380 return time.time() > self.expires
1380 return time.time() > self.expires
1381
1381
1382 @classmethod
1382 @classmethod
1383 def _get_role_name(cls, role):
1383 def _get_role_name(cls, role):
1384 return {
1384 return {
1385 cls.ROLE_ALL: _('all'),
1385 cls.ROLE_ALL: _('all'),
1386 cls.ROLE_HTTP: _('http/web interface'),
1386 cls.ROLE_HTTP: _('http/web interface'),
1387 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1387 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1388 cls.ROLE_API: _('api calls'),
1388 cls.ROLE_API: _('api calls'),
1389 cls.ROLE_FEED: _('feed access'),
1389 cls.ROLE_FEED: _('feed access'),
1390 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1390 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1391 }.get(role, role)
1391 }.get(role, role)
1392
1392
1393 @classmethod
1393 @classmethod
1394 def _get_role_description(cls, role):
1394 def _get_role_description(cls, role):
1395 return {
1395 return {
1396 cls.ROLE_ALL: _('Token for all actions.'),
1396 cls.ROLE_ALL: _('Token for all actions.'),
1397 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1397 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1398 'login using `api_access_controllers_whitelist` functionality.'),
1398 'login using `api_access_controllers_whitelist` functionality.'),
1399 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1399 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1400 'Requires auth_token authentication plugin to be active. <br/>'
1400 'Requires auth_token authentication plugin to be active. <br/>'
1401 'Such Token should be used then instead of a password to '
1401 'Such Token should be used then instead of a password to '
1402 'interact with a repository, and additionally can be '
1402 'interact with a repository, and additionally can be '
1403 'limited to single repository using repo scope.'),
1403 'limited to single repository using repo scope.'),
1404 cls.ROLE_API: _('Token limited to api calls.'),
1404 cls.ROLE_API: _('Token limited to api calls.'),
1405 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1405 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1406 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1406 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1407 }.get(role, role)
1407 }.get(role, role)
1408
1408
1409 @property
1409 @property
1410 def role_humanized(self):
1410 def role_humanized(self):
1411 return self._get_role_name(self.role)
1411 return self._get_role_name(self.role)
1412
1412
1413 def _get_scope(self):
1413 def _get_scope(self):
1414 if self.repo:
1414 if self.repo:
1415 return 'Repository: {}'.format(self.repo.repo_name)
1415 return 'Repository: {}'.format(self.repo.repo_name)
1416 if self.repo_group:
1416 if self.repo_group:
1417 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1417 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1418 return 'Global'
1418 return 'Global'
1419
1419
1420 @property
1420 @property
1421 def scope_humanized(self):
1421 def scope_humanized(self):
1422 return self._get_scope()
1422 return self._get_scope()
1423
1423
1424 @property
1424 @property
1425 def token_obfuscated(self):
1425 def token_obfuscated(self):
1426 if self.api_key:
1426 if self.api_key:
1427 return self.api_key[:4] + "****"
1427 return self.api_key[:4] + "****"
1428
1428
1429
1429
1430 class UserEmailMap(Base, BaseModel):
1430 class UserEmailMap(Base, BaseModel):
1431 __tablename__ = 'user_email_map'
1431 __tablename__ = 'user_email_map'
1432 __table_args__ = (
1432 __table_args__ = (
1433 Index('uem_email_idx', 'email'),
1433 Index('uem_email_idx', 'email'),
1434 Index('uem_user_id_idx', 'user_id'),
1434 Index('uem_user_id_idx', 'user_id'),
1435 UniqueConstraint('email'),
1435 UniqueConstraint('email'),
1436 base_table_args
1436 base_table_args
1437 )
1437 )
1438
1438
1439 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1439 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1440 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1440 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1441 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1441 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1442 user = relationship('User', lazy='joined', back_populates='user_emails')
1442 user = relationship('User', lazy='joined', back_populates='user_emails')
1443
1443
1444 @validates('_email')
1444 @validates('_email')
1445 def validate_email(self, key, email):
1445 def validate_email(self, key, email):
1446 # check if this email is not main one
1446 # check if this email is not main one
1447 main_email = Session().query(User).filter(User.email == email).scalar()
1447 main_email = Session().query(User).filter(User.email == email).scalar()
1448 if main_email is not None:
1448 if main_email is not None:
1449 raise AttributeError('email %s is present is user table' % email)
1449 raise AttributeError('email %s is present is user table' % email)
1450 return email
1450 return email
1451
1451
1452 @hybrid_property
1452 @hybrid_property
1453 def email(self):
1453 def email(self):
1454 return self._email
1454 return self._email
1455
1455
1456 @email.setter
1456 @email.setter
1457 def email(self, val):
1457 def email(self, val):
1458 self._email = val.lower() if val else None
1458 self._email = val.lower() if val else None
1459
1459
1460
1460
1461 class UserIpMap(Base, BaseModel):
1461 class UserIpMap(Base, BaseModel):
1462 __tablename__ = 'user_ip_map'
1462 __tablename__ = 'user_ip_map'
1463 __table_args__ = (
1463 __table_args__ = (
1464 UniqueConstraint('user_id', 'ip_addr'),
1464 UniqueConstraint('user_id', 'ip_addr'),
1465 base_table_args
1465 base_table_args
1466 )
1466 )
1467
1467
1468 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1468 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1469 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1469 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1470 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1470 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1471 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1471 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1472 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1472 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1473 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1473 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1474
1474
1475 @hybrid_property
1475 @hybrid_property
1476 def description_safe(self):
1476 def description_safe(self):
1477 return description_escaper(self.description)
1477 return description_escaper(self.description)
1478
1478
1479 @classmethod
1479 @classmethod
1480 def _get_ip_range(cls, ip_addr):
1480 def _get_ip_range(cls, ip_addr):
1481 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1481 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1482 return [str(net.network_address), str(net.broadcast_address)]
1482 return [str(net.network_address), str(net.broadcast_address)]
1483
1483
1484 def __json__(self):
1484 def __json__(self):
1485 return {
1485 return {
1486 'ip_addr': self.ip_addr,
1486 'ip_addr': self.ip_addr,
1487 'ip_range': self._get_ip_range(self.ip_addr),
1487 'ip_range': self._get_ip_range(self.ip_addr),
1488 }
1488 }
1489
1489
1490 def __repr__(self):
1490 def __repr__(self):
1491 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1491 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1492
1492
1493
1493
1494 class UserSshKeys(Base, BaseModel):
1494 class UserSshKeys(Base, BaseModel):
1495 __tablename__ = 'user_ssh_keys'
1495 __tablename__ = 'user_ssh_keys'
1496 __table_args__ = (
1496 __table_args__ = (
1497 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1497 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1498
1498
1499 UniqueConstraint('ssh_key_fingerprint'),
1499 UniqueConstraint('ssh_key_fingerprint'),
1500
1500
1501 base_table_args
1501 base_table_args
1502 )
1502 )
1503
1503
1504 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1504 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1505 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1505 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1506 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1506 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1507
1507
1508 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1508 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1509
1509
1510 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1510 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1511 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1511 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1512 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1512 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1513
1513
1514 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1514 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1515
1515
1516 def __json__(self):
1516 def __json__(self):
1517 data = {
1517 data = {
1518 'ssh_fingerprint': self.ssh_key_fingerprint,
1518 'ssh_fingerprint': self.ssh_key_fingerprint,
1519 'description': self.description,
1519 'description': self.description,
1520 'created_on': self.created_on
1520 'created_on': self.created_on
1521 }
1521 }
1522 return data
1522 return data
1523
1523
1524 def get_api_data(self):
1524 def get_api_data(self):
1525 data = self.__json__()
1525 data = self.__json__()
1526 return data
1526 return data
1527
1527
1528
1528
1529 class UserLog(Base, BaseModel):
1529 class UserLog(Base, BaseModel):
1530 __tablename__ = 'user_logs'
1530 __tablename__ = 'user_logs'
1531 __table_args__ = (
1531 __table_args__ = (
1532 base_table_args,
1532 base_table_args,
1533 )
1533 )
1534
1534
1535 VERSION_1 = 'v1'
1535 VERSION_1 = 'v1'
1536 VERSION_2 = 'v2'
1536 VERSION_2 = 'v2'
1537 VERSIONS = [VERSION_1, VERSION_2]
1537 VERSIONS = [VERSION_1, VERSION_2]
1538
1538
1539 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1539 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1540 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1540 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1541 username = Column("username", String(255), nullable=True, unique=None, default=None)
1541 username = Column("username", String(255), nullable=True, unique=None, default=None)
1542 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1542 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1543 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1543 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1544 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1544 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1545 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1545 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1546 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1546 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1547
1547
1548 version = Column("version", String(255), nullable=True, default=VERSION_1)
1548 version = Column("version", String(255), nullable=True, default=VERSION_1)
1549 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1549 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1550 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1550 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1551 user = relationship('User', cascade='', back_populates='user_log')
1551 user = relationship('User', cascade='', back_populates='user_log')
1552 repository = relationship('Repository', cascade='', back_populates='logs')
1552 repository = relationship('Repository', cascade='', back_populates='logs')
1553
1553
1554 def __repr__(self):
1554 def __repr__(self):
1555 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1555 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1556
1556
1557 def __json__(self):
1557 def __json__(self):
1558 return {
1558 return {
1559 'user_id': self.user_id,
1559 'user_id': self.user_id,
1560 'username': self.username,
1560 'username': self.username,
1561 'repository_id': self.repository_id,
1561 'repository_id': self.repository_id,
1562 'repository_name': self.repository_name,
1562 'repository_name': self.repository_name,
1563 'user_ip': self.user_ip,
1563 'user_ip': self.user_ip,
1564 'action_date': self.action_date,
1564 'action_date': self.action_date,
1565 'action': self.action,
1565 'action': self.action,
1566 }
1566 }
1567
1567
1568 @hybrid_property
1568 @hybrid_property
1569 def entry_id(self):
1569 def entry_id(self):
1570 return self.user_log_id
1570 return self.user_log_id
1571
1571
1572 @property
1572 @property
1573 def action_as_day(self):
1573 def action_as_day(self):
1574 return datetime.date(*self.action_date.timetuple()[:3])
1574 return datetime.date(*self.action_date.timetuple()[:3])
1575
1575
1576
1576
1577 class UserGroup(Base, BaseModel):
1577 class UserGroup(Base, BaseModel):
1578 __tablename__ = 'users_groups'
1578 __tablename__ = 'users_groups'
1579 __table_args__ = (
1579 __table_args__ = (
1580 base_table_args,
1580 base_table_args,
1581 )
1581 )
1582
1582
1583 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1583 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1584 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1584 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1585 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1585 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1586 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1586 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1587 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1587 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1588 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1588 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1589 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1589 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1590 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1590 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1591
1591
1592 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1592 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1593 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1593 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1594 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1594 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1595 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1595 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1596 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1596 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1597
1597
1598 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1598 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1599
1599
1600 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1600 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1601 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1601 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1602
1602
1603 @classmethod
1603 @classmethod
1604 def _load_group_data(cls, column):
1604 def _load_group_data(cls, column):
1605 if not column:
1605 if not column:
1606 return {}
1606 return {}
1607
1607
1608 try:
1608 try:
1609 return json.loads(column) or {}
1609 return json.loads(column) or {}
1610 except TypeError:
1610 except TypeError:
1611 return {}
1611 return {}
1612
1612
1613 @hybrid_property
1613 @hybrid_property
1614 def description_safe(self):
1614 def description_safe(self):
1615 return description_escaper(self.user_group_description)
1615 return description_escaper(self.user_group_description)
1616
1616
1617 @hybrid_property
1617 @hybrid_property
1618 def group_data(self):
1618 def group_data(self):
1619 return self._load_group_data(self._group_data)
1619 return self._load_group_data(self._group_data)
1620
1620
1621 @group_data.expression
1621 @group_data.expression
1622 def group_data(self, **kwargs):
1622 def group_data(self, **kwargs):
1623 return self._group_data
1623 return self._group_data
1624
1624
1625 @group_data.setter
1625 @group_data.setter
1626 def group_data(self, val):
1626 def group_data(self, val):
1627 try:
1627 try:
1628 self._group_data = json.dumps(val)
1628 self._group_data = json.dumps(val)
1629 except Exception:
1629 except Exception:
1630 log.error(traceback.format_exc())
1630 log.error(traceback.format_exc())
1631
1631
1632 @classmethod
1632 @classmethod
1633 def _load_sync(cls, group_data):
1633 def _load_sync(cls, group_data):
1634 if group_data:
1634 if group_data:
1635 return group_data.get('extern_type')
1635 return group_data.get('extern_type')
1636
1636
1637 @property
1637 @property
1638 def sync(self):
1638 def sync(self):
1639 return self._load_sync(self.group_data)
1639 return self._load_sync(self.group_data)
1640
1640
1641 def __repr__(self):
1641 def __repr__(self):
1642 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1642 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1643
1643
1644 @classmethod
1644 @classmethod
1645 def get_by_group_name(cls, group_name, cache=False,
1645 def get_by_group_name(cls, group_name, cache=False,
1646 case_insensitive=False):
1646 case_insensitive=False):
1647 if case_insensitive:
1647 if case_insensitive:
1648 q = cls.query().filter(func.lower(cls.users_group_name) ==
1648 q = cls.query().filter(func.lower(cls.users_group_name) ==
1649 func.lower(group_name))
1649 func.lower(group_name))
1650
1650
1651 else:
1651 else:
1652 q = cls.query().filter(cls.users_group_name == group_name)
1652 q = cls.query().filter(cls.users_group_name == group_name)
1653 if cache:
1653 if cache:
1654 name_key = _hash_key(group_name)
1654 name_key = _hash_key(group_name)
1655 q = q.options(
1655 q = q.options(
1656 FromCache("sql_cache_short", f"get_group_{name_key}"))
1656 FromCache("sql_cache_short", f"get_group_{name_key}"))
1657 return q.scalar()
1657 return q.scalar()
1658
1658
1659 @classmethod
1659 @classmethod
1660 def get(cls, user_group_id, cache=False):
1660 def get(cls, user_group_id, cache=False):
1661 if not user_group_id:
1661 if not user_group_id:
1662 return
1662 return
1663
1663
1664 user_group = cls.query()
1664 user_group = cls.query()
1665 if cache:
1665 if cache:
1666 user_group = user_group.options(
1666 user_group = user_group.options(
1667 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1667 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1668 return user_group.get(user_group_id)
1668 return user_group.get(user_group_id)
1669
1669
1670 def permissions(self, with_admins=True, with_owner=True,
1670 def permissions(self, with_admins=True, with_owner=True,
1671 expand_from_user_groups=False):
1671 expand_from_user_groups=False):
1672 """
1672 """
1673 Permissions for user groups
1673 Permissions for user groups
1674 """
1674 """
1675 _admin_perm = 'usergroup.admin'
1675 _admin_perm = 'usergroup.admin'
1676
1676
1677 owner_row = []
1677 owner_row = []
1678 if with_owner:
1678 if with_owner:
1679 usr = AttributeDict(self.user.get_dict())
1679 usr = AttributeDict(self.user.get_dict())
1680 usr.owner_row = True
1680 usr.owner_row = True
1681 usr.permission = _admin_perm
1681 usr.permission = _admin_perm
1682 owner_row.append(usr)
1682 owner_row.append(usr)
1683
1683
1684 super_admin_ids = []
1684 super_admin_ids = []
1685 super_admin_rows = []
1685 super_admin_rows = []
1686 if with_admins:
1686 if with_admins:
1687 for usr in User.get_all_super_admins():
1687 for usr in User.get_all_super_admins():
1688 super_admin_ids.append(usr.user_id)
1688 super_admin_ids.append(usr.user_id)
1689 # if this admin is also owner, don't double the record
1689 # if this admin is also owner, don't double the record
1690 if usr.user_id == owner_row[0].user_id:
1690 if usr.user_id == owner_row[0].user_id:
1691 owner_row[0].admin_row = True
1691 owner_row[0].admin_row = True
1692 else:
1692 else:
1693 usr = AttributeDict(usr.get_dict())
1693 usr = AttributeDict(usr.get_dict())
1694 usr.admin_row = True
1694 usr.admin_row = True
1695 usr.permission = _admin_perm
1695 usr.permission = _admin_perm
1696 super_admin_rows.append(usr)
1696 super_admin_rows.append(usr)
1697
1697
1698 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1698 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1699 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1699 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1700 joinedload(UserUserGroupToPerm.user),
1700 joinedload(UserUserGroupToPerm.user),
1701 joinedload(UserUserGroupToPerm.permission),)
1701 joinedload(UserUserGroupToPerm.permission),)
1702
1702
1703 # get owners and admins and permissions. We do a trick of re-writing
1703 # get owners and admins and permissions. We do a trick of re-writing
1704 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1704 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1705 # has a global reference and changing one object propagates to all
1705 # has a global reference and changing one object propagates to all
1706 # others. This means if admin is also an owner admin_row that change
1706 # others. This means if admin is also an owner admin_row that change
1707 # would propagate to both objects
1707 # would propagate to both objects
1708 perm_rows = []
1708 perm_rows = []
1709 for _usr in q.all():
1709 for _usr in q.all():
1710 usr = AttributeDict(_usr.user.get_dict())
1710 usr = AttributeDict(_usr.user.get_dict())
1711 # if this user is also owner/admin, mark as duplicate record
1711 # if this user is also owner/admin, mark as duplicate record
1712 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1712 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1713 usr.duplicate_perm = True
1713 usr.duplicate_perm = True
1714 usr.permission = _usr.permission.permission_name
1714 usr.permission = _usr.permission.permission_name
1715 perm_rows.append(usr)
1715 perm_rows.append(usr)
1716
1716
1717 # filter the perm rows by 'default' first and then sort them by
1717 # filter the perm rows by 'default' first and then sort them by
1718 # admin,write,read,none permissions sorted again alphabetically in
1718 # admin,write,read,none permissions sorted again alphabetically in
1719 # each group
1719 # each group
1720 perm_rows = sorted(perm_rows, key=display_user_sort)
1720 perm_rows = sorted(perm_rows, key=display_user_sort)
1721
1721
1722 user_groups_rows = []
1722 user_groups_rows = []
1723 if expand_from_user_groups:
1723 if expand_from_user_groups:
1724 for ug in self.permission_user_groups(with_members=True):
1724 for ug in self.permission_user_groups(with_members=True):
1725 for user_data in ug.members:
1725 for user_data in ug.members:
1726 user_groups_rows.append(user_data)
1726 user_groups_rows.append(user_data)
1727
1727
1728 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1728 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1729
1729
1730 def permission_user_groups(self, with_members=False):
1730 def permission_user_groups(self, with_members=False):
1731 q = UserGroupUserGroupToPerm.query()\
1731 q = UserGroupUserGroupToPerm.query()\
1732 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1732 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1733 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1733 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1734 joinedload(UserGroupUserGroupToPerm.target_user_group),
1734 joinedload(UserGroupUserGroupToPerm.target_user_group),
1735 joinedload(UserGroupUserGroupToPerm.permission),)
1735 joinedload(UserGroupUserGroupToPerm.permission),)
1736
1736
1737 perm_rows = []
1737 perm_rows = []
1738 for _user_group in q.all():
1738 for _user_group in q.all():
1739 entry = AttributeDict(_user_group.user_group.get_dict())
1739 entry = AttributeDict(_user_group.user_group.get_dict())
1740 entry.permission = _user_group.permission.permission_name
1740 entry.permission = _user_group.permission.permission_name
1741 if with_members:
1741 if with_members:
1742 entry.members = [x.user.get_dict()
1742 entry.members = [x.user.get_dict()
1743 for x in _user_group.user_group.members]
1743 for x in _user_group.user_group.members]
1744 perm_rows.append(entry)
1744 perm_rows.append(entry)
1745
1745
1746 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1746 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1747 return perm_rows
1747 return perm_rows
1748
1748
1749 def _get_default_perms(self, user_group, suffix=''):
1749 def _get_default_perms(self, user_group, suffix=''):
1750 from rhodecode.model.permission import PermissionModel
1750 from rhodecode.model.permission import PermissionModel
1751 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1751 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1752
1752
1753 def get_default_perms(self, suffix=''):
1753 def get_default_perms(self, suffix=''):
1754 return self._get_default_perms(self, suffix)
1754 return self._get_default_perms(self, suffix)
1755
1755
1756 def get_api_data(self, with_group_members=True, include_secrets=False):
1756 def get_api_data(self, with_group_members=True, include_secrets=False):
1757 """
1757 """
1758 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1758 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1759 basically forwarded.
1759 basically forwarded.
1760
1760
1761 """
1761 """
1762 user_group = self
1762 user_group = self
1763 data = {
1763 data = {
1764 'users_group_id': user_group.users_group_id,
1764 'users_group_id': user_group.users_group_id,
1765 'group_name': user_group.users_group_name,
1765 'group_name': user_group.users_group_name,
1766 'group_description': user_group.user_group_description,
1766 'group_description': user_group.user_group_description,
1767 'active': user_group.users_group_active,
1767 'active': user_group.users_group_active,
1768 'owner': user_group.user.username,
1768 'owner': user_group.user.username,
1769 'sync': user_group.sync,
1769 'sync': user_group.sync,
1770 'owner_email': user_group.user.email,
1770 'owner_email': user_group.user.email,
1771 }
1771 }
1772
1772
1773 if with_group_members:
1773 if with_group_members:
1774 users = []
1774 users = []
1775 for user in user_group.members:
1775 for user in user_group.members:
1776 user = user.user
1776 user = user.user
1777 users.append(user.get_api_data(include_secrets=include_secrets))
1777 users.append(user.get_api_data(include_secrets=include_secrets))
1778 data['users'] = users
1778 data['users'] = users
1779
1779
1780 return data
1780 return data
1781
1781
1782
1782
1783 class UserGroupMember(Base, BaseModel):
1783 class UserGroupMember(Base, BaseModel):
1784 __tablename__ = 'users_groups_members'
1784 __tablename__ = 'users_groups_members'
1785 __table_args__ = (
1785 __table_args__ = (
1786 base_table_args,
1786 base_table_args,
1787 )
1787 )
1788
1788
1789 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1789 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1790 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1790 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1791 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1791 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1792
1792
1793 user = relationship('User', lazy='joined', back_populates='group_member')
1793 user = relationship('User', lazy='joined', back_populates='group_member')
1794 users_group = relationship('UserGroup', back_populates='members')
1794 users_group = relationship('UserGroup', back_populates='members')
1795
1795
1796 def __init__(self, gr_id='', u_id=''):
1796 def __init__(self, gr_id='', u_id=''):
1797 self.users_group_id = gr_id
1797 self.users_group_id = gr_id
1798 self.user_id = u_id
1798 self.user_id = u_id
1799
1799
1800
1800
1801 class RepositoryField(Base, BaseModel):
1801 class RepositoryField(Base, BaseModel):
1802 __tablename__ = 'repositories_fields'
1802 __tablename__ = 'repositories_fields'
1803 __table_args__ = (
1803 __table_args__ = (
1804 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1804 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1805 base_table_args,
1805 base_table_args,
1806 )
1806 )
1807
1807
1808 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1808 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1809
1809
1810 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1810 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1811 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1811 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1812 field_key = Column("field_key", String(250))
1812 field_key = Column("field_key", String(250))
1813 field_label = Column("field_label", String(1024), nullable=False)
1813 field_label = Column("field_label", String(1024), nullable=False)
1814 field_value = Column("field_value", String(10000), nullable=False)
1814 field_value = Column("field_value", String(10000), nullable=False)
1815 field_desc = Column("field_desc", String(1024), nullable=False)
1815 field_desc = Column("field_desc", String(1024), nullable=False)
1816 field_type = Column("field_type", String(255), nullable=False, unique=None)
1816 field_type = Column("field_type", String(255), nullable=False, unique=None)
1817 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1817 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1818
1818
1819 repository = relationship('Repository', back_populates='extra_fields')
1819 repository = relationship('Repository', back_populates='extra_fields')
1820
1820
1821 @property
1821 @property
1822 def field_key_prefixed(self):
1822 def field_key_prefixed(self):
1823 return 'ex_%s' % self.field_key
1823 return 'ex_%s' % self.field_key
1824
1824
1825 @classmethod
1825 @classmethod
1826 def un_prefix_key(cls, key):
1826 def un_prefix_key(cls, key):
1827 if key.startswith(cls.PREFIX):
1827 if key.startswith(cls.PREFIX):
1828 return key[len(cls.PREFIX):]
1828 return key[len(cls.PREFIX):]
1829 return key
1829 return key
1830
1830
1831 @classmethod
1831 @classmethod
1832 def get_by_key_name(cls, key, repo):
1832 def get_by_key_name(cls, key, repo):
1833 row = cls.query()\
1833 row = cls.query()\
1834 .filter(cls.repository == repo)\
1834 .filter(cls.repository == repo)\
1835 .filter(cls.field_key == key).scalar()
1835 .filter(cls.field_key == key).scalar()
1836 return row
1836 return row
1837
1837
1838
1838
1839 class Repository(Base, BaseModel):
1839 class Repository(Base, BaseModel):
1840 __tablename__ = 'repositories'
1840 __tablename__ = 'repositories'
1841 __table_args__ = (
1841 __table_args__ = (
1842 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1842 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1843 base_table_args,
1843 base_table_args,
1844 )
1844 )
1845 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1845 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1846 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1846 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1847 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1847 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1848
1848
1849 STATE_CREATED = 'repo_state_created'
1849 STATE_CREATED = 'repo_state_created'
1850 STATE_PENDING = 'repo_state_pending'
1850 STATE_PENDING = 'repo_state_pending'
1851 STATE_ERROR = 'repo_state_error'
1851 STATE_ERROR = 'repo_state_error'
1852
1852
1853 LOCK_AUTOMATIC = 'lock_auto'
1853 LOCK_AUTOMATIC = 'lock_auto'
1854 LOCK_API = 'lock_api'
1854 LOCK_API = 'lock_api'
1855 LOCK_WEB = 'lock_web'
1855 LOCK_WEB = 'lock_web'
1856 LOCK_PULL = 'lock_pull'
1856 LOCK_PULL = 'lock_pull'
1857
1857
1858 NAME_SEP = URL_SEP
1858 NAME_SEP = URL_SEP
1859
1859
1860 repo_id = Column(
1860 repo_id = Column(
1861 "repo_id", Integer(), nullable=False, unique=True, default=None,
1861 "repo_id", Integer(), nullable=False, unique=True, default=None,
1862 primary_key=True)
1862 primary_key=True)
1863 _repo_name = Column(
1863 _repo_name = Column(
1864 "repo_name", Text(), nullable=False, default=None)
1864 "repo_name", Text(), nullable=False, default=None)
1865 repo_name_hash = Column(
1865 repo_name_hash = Column(
1866 "repo_name_hash", String(255), nullable=False, unique=True)
1866 "repo_name_hash", String(255), nullable=False, unique=True)
1867 repo_state = Column("repo_state", String(255), nullable=True)
1867 repo_state = Column("repo_state", String(255), nullable=True)
1868
1868
1869 clone_uri = Column(
1869 clone_uri = Column(
1870 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1870 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1871 default=None)
1871 default=None)
1872 push_uri = Column(
1872 push_uri = Column(
1873 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1873 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1874 default=None)
1874 default=None)
1875 repo_type = Column(
1875 repo_type = Column(
1876 "repo_type", String(255), nullable=False, unique=False, default=None)
1876 "repo_type", String(255), nullable=False, unique=False, default=None)
1877 user_id = Column(
1877 user_id = Column(
1878 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1878 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1879 unique=False, default=None)
1879 unique=False, default=None)
1880 private = Column(
1880 private = Column(
1881 "private", Boolean(), nullable=True, unique=None, default=None)
1881 "private", Boolean(), nullable=True, unique=None, default=None)
1882 archived = Column(
1882 archived = Column(
1883 "archived", Boolean(), nullable=True, unique=None, default=None)
1883 "archived", Boolean(), nullable=True, unique=None, default=None)
1884 enable_statistics = Column(
1884 enable_statistics = Column(
1885 "statistics", Boolean(), nullable=True, unique=None, default=True)
1885 "statistics", Boolean(), nullable=True, unique=None, default=True)
1886 enable_downloads = Column(
1886 enable_downloads = Column(
1887 "downloads", Boolean(), nullable=True, unique=None, default=True)
1887 "downloads", Boolean(), nullable=True, unique=None, default=True)
1888 description = Column(
1888 description = Column(
1889 "description", String(10000), nullable=True, unique=None, default=None)
1889 "description", String(10000), nullable=True, unique=None, default=None)
1890 created_on = Column(
1890 created_on = Column(
1891 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1891 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1892 default=datetime.datetime.now)
1892 default=datetime.datetime.now)
1893 updated_on = Column(
1893 updated_on = Column(
1894 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1894 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1895 default=datetime.datetime.now)
1895 default=datetime.datetime.now)
1896 _landing_revision = Column(
1896 _landing_revision = Column(
1897 "landing_revision", String(255), nullable=False, unique=False,
1897 "landing_revision", String(255), nullable=False, unique=False,
1898 default=None)
1898 default=None)
1899 enable_locking = Column(
1899 enable_locking = Column(
1900 "enable_locking", Boolean(), nullable=False, unique=None,
1900 "enable_locking", Boolean(), nullable=False, unique=None,
1901 default=False)
1901 default=False)
1902 _locked = Column(
1902 _locked = Column(
1903 "locked", String(255), nullable=True, unique=False, default=None)
1903 "locked", String(255), nullable=True, unique=False, default=None)
1904 _changeset_cache = Column(
1904 _changeset_cache = Column(
1905 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1905 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1906
1906
1907 fork_id = Column(
1907 fork_id = Column(
1908 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1908 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1909 nullable=True, unique=False, default=None)
1909 nullable=True, unique=False, default=None)
1910 group_id = Column(
1910 group_id = Column(
1911 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1911 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1912 unique=False, default=None)
1912 unique=False, default=None)
1913
1913
1914 user = relationship('User', lazy='joined', back_populates='repositories')
1914 user = relationship('User', lazy='joined', back_populates='repositories')
1915 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1915 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1916 group = relationship('RepoGroup', lazy='joined')
1916 group = relationship('RepoGroup', lazy='joined')
1917 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1917 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1918 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1918 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1919 stats = relationship('Statistics', cascade='all', uselist=False)
1919 stats = relationship('Statistics', cascade='all', uselist=False)
1920
1920
1921 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1921 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1922 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1922 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1923
1923
1924 logs = relationship('UserLog', back_populates='repository')
1924 logs = relationship('UserLog', back_populates='repository')
1925
1925
1926 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1926 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1927
1927
1928 pull_requests_source = relationship(
1928 pull_requests_source = relationship(
1929 'PullRequest',
1929 'PullRequest',
1930 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1930 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1931 cascade="all, delete-orphan",
1931 cascade="all, delete-orphan",
1932 overlaps="source_repo"
1932 overlaps="source_repo"
1933 )
1933 )
1934 pull_requests_target = relationship(
1934 pull_requests_target = relationship(
1935 'PullRequest',
1935 'PullRequest',
1936 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1936 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1937 cascade="all, delete-orphan",
1937 cascade="all, delete-orphan",
1938 overlaps="target_repo"
1938 overlaps="target_repo"
1939 )
1939 )
1940
1940
1941 ui = relationship('RepoRhodeCodeUi', cascade="all")
1941 ui = relationship('RepoRhodeCodeUi', cascade="all")
1942 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1942 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1943 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1943 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1944
1944
1945 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1945 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1946
1946
1947 # no cascade, set NULL
1947 # no cascade, set NULL
1948 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1948 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1949
1949
1950 review_rules = relationship('RepoReviewRule')
1950 review_rules = relationship('RepoReviewRule')
1951 user_branch_perms = relationship('UserToRepoBranchPermission')
1951 user_branch_perms = relationship('UserToRepoBranchPermission')
1952 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1952 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1953
1953
1954 def __repr__(self):
1954 def __repr__(self):
1955 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1955 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1956
1956
1957 @hybrid_property
1957 @hybrid_property
1958 def description_safe(self):
1958 def description_safe(self):
1959 return description_escaper(self.description)
1959 return description_escaper(self.description)
1960
1960
1961 @hybrid_property
1961 @hybrid_property
1962 def landing_rev(self):
1962 def landing_rev(self):
1963 # always should return [rev_type, rev], e.g ['branch', 'master']
1963 # always should return [rev_type, rev], e.g ['branch', 'master']
1964 if self._landing_revision:
1964 if self._landing_revision:
1965 _rev_info = self._landing_revision.split(':')
1965 _rev_info = self._landing_revision.split(':')
1966 if len(_rev_info) < 2:
1966 if len(_rev_info) < 2:
1967 _rev_info.insert(0, 'rev')
1967 _rev_info.insert(0, 'rev')
1968 return [_rev_info[0], _rev_info[1]]
1968 return [_rev_info[0], _rev_info[1]]
1969 return [None, None]
1969 return [None, None]
1970
1970
1971 @property
1971 @property
1972 def landing_ref_type(self):
1972 def landing_ref_type(self):
1973 return self.landing_rev[0]
1973 return self.landing_rev[0]
1974
1974
1975 @property
1975 @property
1976 def landing_ref_name(self):
1976 def landing_ref_name(self):
1977 return self.landing_rev[1]
1977 return self.landing_rev[1]
1978
1978
1979 @landing_rev.setter
1979 @landing_rev.setter
1980 def landing_rev(self, val):
1980 def landing_rev(self, val):
1981 if ':' not in val:
1981 if ':' not in val:
1982 raise ValueError('value must be delimited with `:` and consist '
1982 raise ValueError('value must be delimited with `:` and consist '
1983 'of <rev_type>:<rev>, got %s instead' % val)
1983 'of <rev_type>:<rev>, got %s instead' % val)
1984 self._landing_revision = val
1984 self._landing_revision = val
1985
1985
1986 @hybrid_property
1986 @hybrid_property
1987 def locked(self):
1987 def locked(self):
1988 if self._locked:
1988 if self._locked:
1989 user_id, timelocked, reason = self._locked.split(':')
1989 user_id, timelocked, reason = self._locked.split(':')
1990 lock_values = int(user_id), timelocked, reason
1990 lock_values = int(user_id), timelocked, reason
1991 else:
1991 else:
1992 lock_values = [None, None, None]
1992 lock_values = [None, None, None]
1993 return lock_values
1993 return lock_values
1994
1994
1995 @locked.setter
1995 @locked.setter
1996 def locked(self, val):
1996 def locked(self, val):
1997 if val and isinstance(val, (list, tuple)):
1997 if val and isinstance(val, (list, tuple)):
1998 self._locked = ':'.join(map(str, val))
1998 self._locked = ':'.join(map(str, val))
1999 else:
1999 else:
2000 self._locked = None
2000 self._locked = None
2001
2001
2002 @classmethod
2002 @classmethod
2003 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2003 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2004 from rhodecode.lib.vcs.backends.base import EmptyCommit
2004 from rhodecode.lib.vcs.backends.base import EmptyCommit
2005 dummy = EmptyCommit().__json__()
2005 dummy = EmptyCommit().__json__()
2006 if not changeset_cache_raw:
2006 if not changeset_cache_raw:
2007 dummy['source_repo_id'] = repo_id
2007 dummy['source_repo_id'] = repo_id
2008 return json.loads(json.dumps(dummy))
2008 return json.loads(json.dumps(dummy))
2009
2009
2010 try:
2010 try:
2011 return json.loads(changeset_cache_raw)
2011 return json.loads(changeset_cache_raw)
2012 except TypeError:
2012 except TypeError:
2013 return dummy
2013 return dummy
2014 except Exception:
2014 except Exception:
2015 log.error(traceback.format_exc())
2015 log.error(traceback.format_exc())
2016 return dummy
2016 return dummy
2017
2017
2018 @hybrid_property
2018 @hybrid_property
2019 def changeset_cache(self):
2019 def changeset_cache(self):
2020 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2020 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2021
2021
2022 @changeset_cache.setter
2022 @changeset_cache.setter
2023 def changeset_cache(self, val):
2023 def changeset_cache(self, val):
2024 try:
2024 try:
2025 self._changeset_cache = json.dumps(val)
2025 self._changeset_cache = json.dumps(val)
2026 except Exception:
2026 except Exception:
2027 log.error(traceback.format_exc())
2027 log.error(traceback.format_exc())
2028
2028
2029 @hybrid_property
2029 @hybrid_property
2030 def repo_name(self):
2030 def repo_name(self):
2031 return self._repo_name
2031 return self._repo_name
2032
2032
2033 @repo_name.setter
2033 @repo_name.setter
2034 def repo_name(self, value):
2034 def repo_name(self, value):
2035 self._repo_name = value
2035 self._repo_name = value
2036 self.repo_name_hash = sha1(safe_bytes(value))
2036 self.repo_name_hash = sha1(safe_bytes(value))
2037
2037
2038 @classmethod
2038 @classmethod
2039 def normalize_repo_name(cls, repo_name):
2039 def normalize_repo_name(cls, repo_name):
2040 """
2040 """
2041 Normalizes os specific repo_name to the format internally stored inside
2041 Normalizes os specific repo_name to the format internally stored inside
2042 database using URL_SEP
2042 database using URL_SEP
2043
2043
2044 :param cls:
2044 :param cls:
2045 :param repo_name:
2045 :param repo_name:
2046 """
2046 """
2047 return cls.NAME_SEP.join(repo_name.split(os.sep))
2047 return cls.NAME_SEP.join(repo_name.split(os.sep))
2048
2048
2049 @classmethod
2049 @classmethod
2050 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2050 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2051 session = Session()
2051 session = Session()
2052 q = session.query(cls).filter(cls.repo_name == repo_name)
2052 q = session.query(cls).filter(cls.repo_name == repo_name)
2053
2053
2054 if cache:
2054 if cache:
2055 if identity_cache:
2055 if identity_cache:
2056 val = cls.identity_cache(session, 'repo_name', repo_name)
2056 val = cls.identity_cache(session, 'repo_name', repo_name)
2057 if val:
2057 if val:
2058 return val
2058 return val
2059 else:
2059 else:
2060 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2060 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2061 q = q.options(
2061 q = q.options(
2062 FromCache("sql_cache_short", cache_key))
2062 FromCache("sql_cache_short", cache_key))
2063
2063
2064 return q.scalar()
2064 return q.scalar()
2065
2065
2066 @classmethod
2066 @classmethod
2067 def get_by_id_or_repo_name(cls, repoid):
2067 def get_by_id_or_repo_name(cls, repoid):
2068 if isinstance(repoid, int):
2068 if isinstance(repoid, int):
2069 try:
2069 try:
2070 repo = cls.get(repoid)
2070 repo = cls.get(repoid)
2071 except ValueError:
2071 except ValueError:
2072 repo = None
2072 repo = None
2073 else:
2073 else:
2074 repo = cls.get_by_repo_name(repoid)
2074 repo = cls.get_by_repo_name(repoid)
2075 return repo
2075 return repo
2076
2076
2077 @classmethod
2077 @classmethod
2078 def get_by_full_path(cls, repo_full_path):
2078 def get_by_full_path(cls, repo_full_path):
2079 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2079 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2080 repo_name = cls.normalize_repo_name(repo_name)
2080 repo_name = cls.normalize_repo_name(repo_name)
2081 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2081 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2082
2082
2083 @classmethod
2083 @classmethod
2084 def get_repo_forks(cls, repo_id):
2084 def get_repo_forks(cls, repo_id):
2085 return cls.query().filter(Repository.fork_id == repo_id)
2085 return cls.query().filter(Repository.fork_id == repo_id)
2086
2086
2087 @classmethod
2087 @classmethod
2088 def base_path(cls):
2088 def base_path(cls):
2089 """
2089 """
2090 Returns base path when all repos are stored
2090 Returns base path when all repos are stored
2091
2091
2092 :param cls:
2092 :param cls:
2093 """
2093 """
2094 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2094 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2095 return get_rhodecode_repo_store_path()
2095 return get_rhodecode_repo_store_path()
2096
2096
2097 @classmethod
2097 @classmethod
2098 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2098 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2099 case_insensitive=True, archived=False):
2099 case_insensitive=True, archived=False):
2100 q = Repository.query()
2100 q = Repository.query()
2101
2101
2102 if not archived:
2102 if not archived:
2103 q = q.filter(Repository.archived.isnot(true()))
2103 q = q.filter(Repository.archived.isnot(true()))
2104
2104
2105 if not isinstance(user_id, Optional):
2105 if not isinstance(user_id, Optional):
2106 q = q.filter(Repository.user_id == user_id)
2106 q = q.filter(Repository.user_id == user_id)
2107
2107
2108 if not isinstance(group_id, Optional):
2108 if not isinstance(group_id, Optional):
2109 q = q.filter(Repository.group_id == group_id)
2109 q = q.filter(Repository.group_id == group_id)
2110
2110
2111 if case_insensitive:
2111 if case_insensitive:
2112 q = q.order_by(func.lower(Repository.repo_name))
2112 q = q.order_by(func.lower(Repository.repo_name))
2113 else:
2113 else:
2114 q = q.order_by(Repository.repo_name)
2114 q = q.order_by(Repository.repo_name)
2115
2115
2116 return q.all()
2116 return q.all()
2117
2117
2118 @property
2118 @property
2119 def repo_uid(self):
2119 def repo_uid(self):
2120 return '_{}'.format(self.repo_id)
2120 return '_{}'.format(self.repo_id)
2121
2121
2122 @property
2122 @property
2123 def forks(self):
2123 def forks(self):
2124 """
2124 """
2125 Return forks of this repo
2125 Return forks of this repo
2126 """
2126 """
2127 return Repository.get_repo_forks(self.repo_id)
2127 return Repository.get_repo_forks(self.repo_id)
2128
2128
2129 @property
2129 @property
2130 def parent(self):
2130 def parent(self):
2131 """
2131 """
2132 Returns fork parent
2132 Returns fork parent
2133 """
2133 """
2134 return self.fork
2134 return self.fork
2135
2135
2136 @property
2136 @property
2137 def just_name(self):
2137 def just_name(self):
2138 return self.repo_name.split(self.NAME_SEP)[-1]
2138 return self.repo_name.split(self.NAME_SEP)[-1]
2139
2139
2140 @property
2140 @property
2141 def groups_with_parents(self):
2141 def groups_with_parents(self):
2142 groups = []
2142 groups = []
2143 if self.group is None:
2143 if self.group is None:
2144 return groups
2144 return groups
2145
2145
2146 cur_gr = self.group
2146 cur_gr = self.group
2147 groups.insert(0, cur_gr)
2147 groups.insert(0, cur_gr)
2148 while 1:
2148 while 1:
2149 gr = getattr(cur_gr, 'parent_group', None)
2149 gr = getattr(cur_gr, 'parent_group', None)
2150 cur_gr = cur_gr.parent_group
2150 cur_gr = cur_gr.parent_group
2151 if gr is None:
2151 if gr is None:
2152 break
2152 break
2153 groups.insert(0, gr)
2153 groups.insert(0, gr)
2154
2154
2155 return groups
2155 return groups
2156
2156
2157 @property
2157 @property
2158 def groups_and_repo(self):
2158 def groups_and_repo(self):
2159 return self.groups_with_parents, self
2159 return self.groups_with_parents, self
2160
2160
2161 @property
2161 @property
2162 def repo_path(self):
2162 def repo_path(self):
2163 """
2163 """
2164 Returns base full path for that repository means where it actually
2164 Returns base full path for that repository means where it actually
2165 exists on a filesystem
2165 exists on a filesystem
2166 """
2166 """
2167 return self.base_path()
2167 return self.base_path()
2168
2168
2169 @property
2169 @property
2170 def repo_full_path(self):
2170 def repo_full_path(self):
2171 p = [self.repo_path]
2171 p = [self.repo_path]
2172 # we need to split the name by / since this is how we store the
2172 # we need to split the name by / since this is how we store the
2173 # names in the database, but that eventually needs to be converted
2173 # names in the database, but that eventually needs to be converted
2174 # into a valid system path
2174 # into a valid system path
2175 p += self.repo_name.split(self.NAME_SEP)
2175 p += self.repo_name.split(self.NAME_SEP)
2176 return os.path.join(*map(safe_str, p))
2176 return os.path.join(*map(safe_str, p))
2177
2177
2178 @property
2178 @property
2179 def cache_keys(self):
2179 def cache_keys(self):
2180 """
2180 """
2181 Returns associated cache keys for that repo
2181 Returns associated cache keys for that repo
2182 """
2182 """
2183 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2183 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2184 return CacheKey.query()\
2184 return CacheKey.query()\
2185 .filter(CacheKey.cache_key == repo_namespace_key)\
2185 .filter(CacheKey.cache_key == repo_namespace_key)\
2186 .order_by(CacheKey.cache_key)\
2186 .order_by(CacheKey.cache_key)\
2187 .all()
2187 .all()
2188
2188
2189 @property
2189 @property
2190 def cached_diffs_relative_dir(self):
2190 def cached_diffs_relative_dir(self):
2191 """
2191 """
2192 Return a relative to the repository store path of cached diffs
2192 Return a relative to the repository store path of cached diffs
2193 used for safe display for users, who shouldn't know the absolute store
2193 used for safe display for users, who shouldn't know the absolute store
2194 path
2194 path
2195 """
2195 """
2196 return os.path.join(
2196 return os.path.join(
2197 os.path.dirname(self.repo_name),
2197 os.path.dirname(self.repo_name),
2198 self.cached_diffs_dir.split(os.path.sep)[-1])
2198 self.cached_diffs_dir.split(os.path.sep)[-1])
2199
2199
2200 @property
2200 @property
2201 def cached_diffs_dir(self):
2201 def cached_diffs_dir(self):
2202 path = self.repo_full_path
2202 path = self.repo_full_path
2203 return os.path.join(
2203 return os.path.join(
2204 os.path.dirname(path),
2204 os.path.dirname(path),
2205 f'.__shadow_diff_cache_repo_{self.repo_id}')
2205 f'.__shadow_diff_cache_repo_{self.repo_id}')
2206
2206
2207 def cached_diffs(self):
2207 def cached_diffs(self):
2208 diff_cache_dir = self.cached_diffs_dir
2208 diff_cache_dir = self.cached_diffs_dir
2209 if os.path.isdir(diff_cache_dir):
2209 if os.path.isdir(diff_cache_dir):
2210 return os.listdir(diff_cache_dir)
2210 return os.listdir(diff_cache_dir)
2211 return []
2211 return []
2212
2212
2213 def shadow_repos(self):
2213 def shadow_repos(self):
2214 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2214 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2215 return [
2215 return [
2216 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2216 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2217 if x.startswith(shadow_repos_pattern)
2217 if x.startswith(shadow_repos_pattern)
2218 ]
2218 ]
2219
2219
2220 def get_new_name(self, repo_name):
2220 def get_new_name(self, repo_name):
2221 """
2221 """
2222 returns new full repository name based on assigned group and new new
2222 returns new full repository name based on assigned group and new new
2223
2223
2224 :param repo_name:
2224 :param repo_name:
2225 """
2225 """
2226 path_prefix = self.group.full_path_splitted if self.group else []
2226 path_prefix = self.group.full_path_splitted if self.group else []
2227 return self.NAME_SEP.join(path_prefix + [repo_name])
2227 return self.NAME_SEP.join(path_prefix + [repo_name])
2228
2228
2229 @property
2229 @property
2230 def _config(self):
2230 def _config(self):
2231 """
2231 """
2232 Returns db based config object.
2232 Returns db based config object.
2233 """
2233 """
2234 from rhodecode.lib.utils import make_db_config
2234 from rhodecode.lib.utils import make_db_config
2235 return make_db_config(clear_session=False, repo=self)
2235 return make_db_config(clear_session=False, repo=self)
2236
2236
2237 def permissions(self, with_admins=True, with_owner=True,
2237 def permissions(self, with_admins=True, with_owner=True,
2238 expand_from_user_groups=False):
2238 expand_from_user_groups=False):
2239 """
2239 """
2240 Permissions for repositories
2240 Permissions for repositories
2241 """
2241 """
2242 _admin_perm = 'repository.admin'
2242 _admin_perm = 'repository.admin'
2243
2243
2244 owner_row = []
2244 owner_row = []
2245 if with_owner:
2245 if with_owner:
2246 usr = AttributeDict(self.user.get_dict())
2246 usr = AttributeDict(self.user.get_dict())
2247 usr.owner_row = True
2247 usr.owner_row = True
2248 usr.permission = _admin_perm
2248 usr.permission = _admin_perm
2249 usr.permission_id = None
2249 usr.permission_id = None
2250 owner_row.append(usr)
2250 owner_row.append(usr)
2251
2251
2252 super_admin_ids = []
2252 super_admin_ids = []
2253 super_admin_rows = []
2253 super_admin_rows = []
2254 if with_admins:
2254 if with_admins:
2255 for usr in User.get_all_super_admins():
2255 for usr in User.get_all_super_admins():
2256 super_admin_ids.append(usr.user_id)
2256 super_admin_ids.append(usr.user_id)
2257 # if this admin is also owner, don't double the record
2257 # if this admin is also owner, don't double the record
2258 if usr.user_id == owner_row[0].user_id:
2258 if usr.user_id == owner_row[0].user_id:
2259 owner_row[0].admin_row = True
2259 owner_row[0].admin_row = True
2260 else:
2260 else:
2261 usr = AttributeDict(usr.get_dict())
2261 usr = AttributeDict(usr.get_dict())
2262 usr.admin_row = True
2262 usr.admin_row = True
2263 usr.permission = _admin_perm
2263 usr.permission = _admin_perm
2264 usr.permission_id = None
2264 usr.permission_id = None
2265 super_admin_rows.append(usr)
2265 super_admin_rows.append(usr)
2266
2266
2267 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2267 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2268 q = q.options(joinedload(UserRepoToPerm.repository),
2268 q = q.options(joinedload(UserRepoToPerm.repository),
2269 joinedload(UserRepoToPerm.user),
2269 joinedload(UserRepoToPerm.user),
2270 joinedload(UserRepoToPerm.permission),)
2270 joinedload(UserRepoToPerm.permission),)
2271
2271
2272 # get owners and admins and permissions. We do a trick of re-writing
2272 # get owners and admins and permissions. We do a trick of re-writing
2273 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2273 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2274 # has a global reference and changing one object propagates to all
2274 # has a global reference and changing one object propagates to all
2275 # others. This means if admin is also an owner admin_row that change
2275 # others. This means if admin is also an owner admin_row that change
2276 # would propagate to both objects
2276 # would propagate to both objects
2277 perm_rows = []
2277 perm_rows = []
2278 for _usr in q.all():
2278 for _usr in q.all():
2279 usr = AttributeDict(_usr.user.get_dict())
2279 usr = AttributeDict(_usr.user.get_dict())
2280 # if this user is also owner/admin, mark as duplicate record
2280 # if this user is also owner/admin, mark as duplicate record
2281 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2281 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2282 usr.duplicate_perm = True
2282 usr.duplicate_perm = True
2283 # also check if this permission is maybe used by branch_permissions
2283 # also check if this permission is maybe used by branch_permissions
2284 if _usr.branch_perm_entry:
2284 if _usr.branch_perm_entry:
2285 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2285 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2286
2286
2287 usr.permission = _usr.permission.permission_name
2287 usr.permission = _usr.permission.permission_name
2288 usr.permission_id = _usr.repo_to_perm_id
2288 usr.permission_id = _usr.repo_to_perm_id
2289 perm_rows.append(usr)
2289 perm_rows.append(usr)
2290
2290
2291 # filter the perm rows by 'default' first and then sort them by
2291 # filter the perm rows by 'default' first and then sort them by
2292 # admin,write,read,none permissions sorted again alphabetically in
2292 # admin,write,read,none permissions sorted again alphabetically in
2293 # each group
2293 # each group
2294 perm_rows = sorted(perm_rows, key=display_user_sort)
2294 perm_rows = sorted(perm_rows, key=display_user_sort)
2295
2295
2296 user_groups_rows = []
2296 user_groups_rows = []
2297 if expand_from_user_groups:
2297 if expand_from_user_groups:
2298 for ug in self.permission_user_groups(with_members=True):
2298 for ug in self.permission_user_groups(with_members=True):
2299 for user_data in ug.members:
2299 for user_data in ug.members:
2300 user_groups_rows.append(user_data)
2300 user_groups_rows.append(user_data)
2301
2301
2302 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2302 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2303
2303
2304 def permission_user_groups(self, with_members=True):
2304 def permission_user_groups(self, with_members=True):
2305 q = UserGroupRepoToPerm.query()\
2305 q = UserGroupRepoToPerm.query()\
2306 .filter(UserGroupRepoToPerm.repository == self)
2306 .filter(UserGroupRepoToPerm.repository == self)
2307 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2307 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2308 joinedload(UserGroupRepoToPerm.users_group),
2308 joinedload(UserGroupRepoToPerm.users_group),
2309 joinedload(UserGroupRepoToPerm.permission),)
2309 joinedload(UserGroupRepoToPerm.permission),)
2310
2310
2311 perm_rows = []
2311 perm_rows = []
2312 for _user_group in q.all():
2312 for _user_group in q.all():
2313 entry = AttributeDict(_user_group.users_group.get_dict())
2313 entry = AttributeDict(_user_group.users_group.get_dict())
2314 entry.permission = _user_group.permission.permission_name
2314 entry.permission = _user_group.permission.permission_name
2315 if with_members:
2315 if with_members:
2316 entry.members = [x.user.get_dict()
2316 entry.members = [x.user.get_dict()
2317 for x in _user_group.users_group.members]
2317 for x in _user_group.users_group.members]
2318 perm_rows.append(entry)
2318 perm_rows.append(entry)
2319
2319
2320 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2320 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2321 return perm_rows
2321 return perm_rows
2322
2322
2323 def get_api_data(self, include_secrets=False):
2323 def get_api_data(self, include_secrets=False):
2324 """
2324 """
2325 Common function for generating repo api data
2325 Common function for generating repo api data
2326
2326
2327 :param include_secrets: See :meth:`User.get_api_data`.
2327 :param include_secrets: See :meth:`User.get_api_data`.
2328
2328
2329 """
2329 """
2330 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2330 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2331 # move this methods on models level.
2331 # move this methods on models level.
2332 from rhodecode.model.settings import SettingsModel
2332 from rhodecode.model.settings import SettingsModel
2333 from rhodecode.model.repo import RepoModel
2333 from rhodecode.model.repo import RepoModel
2334
2334
2335 repo = self
2335 repo = self
2336 _user_id, _time, _reason = self.locked
2336 _user_id, _time, _reason = self.locked
2337
2337
2338 data = {
2338 data = {
2339 'repo_id': repo.repo_id,
2339 'repo_id': repo.repo_id,
2340 'repo_name': repo.repo_name,
2340 'repo_name': repo.repo_name,
2341 'repo_type': repo.repo_type,
2341 'repo_type': repo.repo_type,
2342 'clone_uri': repo.clone_uri or '',
2342 'clone_uri': repo.clone_uri or '',
2343 'push_uri': repo.push_uri or '',
2343 'push_uri': repo.push_uri or '',
2344 'url': RepoModel().get_url(self),
2344 'url': RepoModel().get_url(self),
2345 'private': repo.private,
2345 'private': repo.private,
2346 'created_on': repo.created_on,
2346 'created_on': repo.created_on,
2347 'description': repo.description_safe,
2347 'description': repo.description_safe,
2348 'landing_rev': repo.landing_rev,
2348 'landing_rev': repo.landing_rev,
2349 'owner': repo.user.username,
2349 'owner': repo.user.username,
2350 'fork_of': repo.fork.repo_name if repo.fork else None,
2350 'fork_of': repo.fork.repo_name if repo.fork else None,
2351 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2351 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2352 'enable_statistics': repo.enable_statistics,
2352 'enable_statistics': repo.enable_statistics,
2353 'enable_locking': repo.enable_locking,
2353 'enable_locking': repo.enable_locking,
2354 'enable_downloads': repo.enable_downloads,
2354 'enable_downloads': repo.enable_downloads,
2355 'last_changeset': repo.changeset_cache,
2355 'last_changeset': repo.changeset_cache,
2356 'locked_by': User.get(_user_id).get_api_data(
2356 'locked_by': User.get(_user_id).get_api_data(
2357 include_secrets=include_secrets) if _user_id else None,
2357 include_secrets=include_secrets) if _user_id else None,
2358 'locked_date': time_to_datetime(_time) if _time else None,
2358 'locked_date': time_to_datetime(_time) if _time else None,
2359 'lock_reason': _reason if _reason else None,
2359 'lock_reason': _reason if _reason else None,
2360 }
2360 }
2361
2361
2362 # TODO: mikhail: should be per-repo settings here
2362 # TODO: mikhail: should be per-repo settings here
2363 rc_config = SettingsModel().get_all_settings()
2363 rc_config = SettingsModel().get_all_settings()
2364 repository_fields = str2bool(
2364 repository_fields = str2bool(
2365 rc_config.get('rhodecode_repository_fields'))
2365 rc_config.get('rhodecode_repository_fields'))
2366 if repository_fields:
2366 if repository_fields:
2367 for f in self.extra_fields:
2367 for f in self.extra_fields:
2368 data[f.field_key_prefixed] = f.field_value
2368 data[f.field_key_prefixed] = f.field_value
2369
2369
2370 return data
2370 return data
2371
2371
2372 @classmethod
2372 @classmethod
2373 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2373 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2374 if not lock_time:
2374 if not lock_time:
2375 lock_time = time.time()
2375 lock_time = time.time()
2376 if not lock_reason:
2376 if not lock_reason:
2377 lock_reason = cls.LOCK_AUTOMATIC
2377 lock_reason = cls.LOCK_AUTOMATIC
2378 repo.locked = [user_id, lock_time, lock_reason]
2378 repo.locked = [user_id, lock_time, lock_reason]
2379 Session().add(repo)
2379 Session().add(repo)
2380 Session().commit()
2380 Session().commit()
2381
2381
2382 @classmethod
2382 @classmethod
2383 def unlock(cls, repo):
2383 def unlock(cls, repo):
2384 repo.locked = None
2384 repo.locked = None
2385 Session().add(repo)
2385 Session().add(repo)
2386 Session().commit()
2386 Session().commit()
2387
2387
2388 @classmethod
2388 @classmethod
2389 def getlock(cls, repo):
2389 def getlock(cls, repo):
2390 return repo.locked
2390 return repo.locked
2391
2391
2392 def get_locking_state(self, action, user_id, only_when_enabled=True):
2392 def get_locking_state(self, action, user_id, only_when_enabled=True):
2393 """
2393 """
2394 Checks locking on this repository, if locking is enabled and lock is
2394 Checks locking on this repository, if locking is enabled and lock is
2395 present returns a tuple of make_lock, locked, locked_by.
2395 present returns a tuple of make_lock, locked, locked_by.
2396 make_lock can have 3 states None (do nothing) True, make lock
2396 make_lock can have 3 states None (do nothing) True, make lock
2397 False release lock, This value is later propagated to hooks, which
2397 False release lock, This value is later propagated to hooks, which
2398 do the locking. Think about this as signals passed to hooks what to do.
2398 do the locking. Think about this as signals passed to hooks what to do.
2399
2399
2400 """
2400 """
2401 # TODO: johbo: This is part of the business logic and should be moved
2401 # TODO: johbo: This is part of the business logic and should be moved
2402 # into the RepositoryModel.
2402 # into the RepositoryModel.
2403
2403
2404 if action not in ('push', 'pull'):
2404 if action not in ('push', 'pull'):
2405 raise ValueError("Invalid action value: %s" % repr(action))
2405 raise ValueError("Invalid action value: %s" % repr(action))
2406
2406
2407 # defines if locked error should be thrown to user
2407 # defines if locked error should be thrown to user
2408 currently_locked = False
2408 currently_locked = False
2409 # defines if new lock should be made, tri-state
2409 # defines if new lock should be made, tri-state
2410 make_lock = None
2410 make_lock = None
2411 repo = self
2411 repo = self
2412 user = User.get(user_id)
2412 user = User.get(user_id)
2413
2413
2414 lock_info = repo.locked
2414 lock_info = repo.locked
2415
2415
2416 if repo and (repo.enable_locking or not only_when_enabled):
2416 if repo and (repo.enable_locking or not only_when_enabled):
2417 if action == 'push':
2417 if action == 'push':
2418 # check if it's already locked !, if it is compare users
2418 # check if it's already locked !, if it is compare users
2419 locked_by_user_id = lock_info[0]
2419 locked_by_user_id = lock_info[0]
2420 if user.user_id == locked_by_user_id:
2420 if user.user_id == locked_by_user_id:
2421 log.debug(
2421 log.debug(
2422 'Got `push` action from user %s, now unlocking', user)
2422 'Got `push` action from user %s, now unlocking', user)
2423 # unlock if we have push from user who locked
2423 # unlock if we have push from user who locked
2424 make_lock = False
2424 make_lock = False
2425 else:
2425 else:
2426 # we're not the same user who locked, ban with
2426 # we're not the same user who locked, ban with
2427 # code defined in settings (default is 423 HTTP Locked) !
2427 # code defined in settings (default is 423 HTTP Locked) !
2428 log.debug('Repo %s is currently locked by %s', repo, user)
2428 log.debug('Repo %s is currently locked by %s', repo, user)
2429 currently_locked = True
2429 currently_locked = True
2430 elif action == 'pull':
2430 elif action == 'pull':
2431 # [0] user [1] date
2431 # [0] user [1] date
2432 if lock_info[0] and lock_info[1]:
2432 if lock_info[0] and lock_info[1]:
2433 log.debug('Repo %s is currently locked by %s', repo, user)
2433 log.debug('Repo %s is currently locked by %s', repo, user)
2434 currently_locked = True
2434 currently_locked = True
2435 else:
2435 else:
2436 log.debug('Setting lock on repo %s by %s', repo, user)
2436 log.debug('Setting lock on repo %s by %s', repo, user)
2437 make_lock = True
2437 make_lock = True
2438
2438
2439 else:
2439 else:
2440 log.debug('Repository %s do not have locking enabled', repo)
2440 log.debug('Repository %s do not have locking enabled', repo)
2441
2441
2442 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2442 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2443 make_lock, currently_locked, lock_info)
2443 make_lock, currently_locked, lock_info)
2444
2444
2445 from rhodecode.lib.auth import HasRepoPermissionAny
2445 from rhodecode.lib.auth import HasRepoPermissionAny
2446 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2446 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2447 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2447 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2448 # if we don't have at least write permission we cannot make a lock
2448 # if we don't have at least write permission we cannot make a lock
2449 log.debug('lock state reset back to FALSE due to lack '
2449 log.debug('lock state reset back to FALSE due to lack '
2450 'of at least read permission')
2450 'of at least read permission')
2451 make_lock = False
2451 make_lock = False
2452
2452
2453 return make_lock, currently_locked, lock_info
2453 return make_lock, currently_locked, lock_info
2454
2454
2455 @property
2455 @property
2456 def last_commit_cache_update_diff(self):
2456 def last_commit_cache_update_diff(self):
2457 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2457 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2458
2458
2459 @classmethod
2459 @classmethod
2460 def _load_commit_change(cls, last_commit_cache):
2460 def _load_commit_change(cls, last_commit_cache):
2461 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2461 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2462 empty_date = datetime.datetime.fromtimestamp(0)
2462 empty_date = datetime.datetime.fromtimestamp(0)
2463 date_latest = last_commit_cache.get('date', empty_date)
2463 date_latest = last_commit_cache.get('date', empty_date)
2464 try:
2464 try:
2465 return parse_datetime(date_latest)
2465 return parse_datetime(date_latest)
2466 except Exception:
2466 except Exception:
2467 return empty_date
2467 return empty_date
2468
2468
2469 @property
2469 @property
2470 def last_commit_change(self):
2470 def last_commit_change(self):
2471 return self._load_commit_change(self.changeset_cache)
2471 return self._load_commit_change(self.changeset_cache)
2472
2472
2473 @property
2473 @property
2474 def last_db_change(self):
2474 def last_db_change(self):
2475 return self.updated_on
2475 return self.updated_on
2476
2476
2477 @property
2477 @property
2478 def clone_uri_hidden(self):
2478 def clone_uri_hidden(self):
2479 clone_uri = self.clone_uri
2479 clone_uri = self.clone_uri
2480 if clone_uri:
2480 if clone_uri:
2481 import urlobject
2481 import urlobject
2482 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2482 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2483 if url_obj.password:
2483 if url_obj.password:
2484 clone_uri = url_obj.with_password('*****')
2484 clone_uri = url_obj.with_password('*****')
2485 return clone_uri
2485 return clone_uri
2486
2486
2487 @property
2487 @property
2488 def push_uri_hidden(self):
2488 def push_uri_hidden(self):
2489 push_uri = self.push_uri
2489 push_uri = self.push_uri
2490 if push_uri:
2490 if push_uri:
2491 import urlobject
2491 import urlobject
2492 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2492 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2493 if url_obj.password:
2493 if url_obj.password:
2494 push_uri = url_obj.with_password('*****')
2494 push_uri = url_obj.with_password('*****')
2495 return push_uri
2495 return push_uri
2496
2496
2497 def clone_url(self, **override):
2497 def clone_url(self, **override):
2498 from rhodecode.model.settings import SettingsModel
2498 from rhodecode.model.settings import SettingsModel
2499
2499
2500 uri_tmpl = None
2500 uri_tmpl = None
2501 if 'with_id' in override:
2501 if 'with_id' in override:
2502 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2502 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2503 del override['with_id']
2503 del override['with_id']
2504
2504
2505 if 'uri_tmpl' in override:
2505 if 'uri_tmpl' in override:
2506 uri_tmpl = override['uri_tmpl']
2506 uri_tmpl = override['uri_tmpl']
2507 del override['uri_tmpl']
2507 del override['uri_tmpl']
2508
2508
2509 ssh = False
2509 ssh = False
2510 if 'ssh' in override:
2510 if 'ssh' in override:
2511 ssh = True
2511 ssh = True
2512 del override['ssh']
2512 del override['ssh']
2513
2513
2514 # we didn't override our tmpl from **overrides
2514 # we didn't override our tmpl from **overrides
2515 request = get_current_request()
2515 request = get_current_request()
2516 if not uri_tmpl:
2516 if not uri_tmpl:
2517 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2517 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2518 rc_config = request.call_context.rc_config
2518 rc_config = request.call_context.rc_config
2519 else:
2519 else:
2520 rc_config = SettingsModel().get_all_settings(cache=True)
2520 rc_config = SettingsModel().get_all_settings(cache=True)
2521
2521
2522 if ssh:
2522 if ssh:
2523 uri_tmpl = rc_config.get(
2523 uri_tmpl = rc_config.get(
2524 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2524 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2525
2525
2526 else:
2526 else:
2527 uri_tmpl = rc_config.get(
2527 uri_tmpl = rc_config.get(
2528 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2528 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2529
2529
2530 return get_clone_url(request=request,
2530 return get_clone_url(request=request,
2531 uri_tmpl=uri_tmpl,
2531 uri_tmpl=uri_tmpl,
2532 repo_name=self.repo_name,
2532 repo_name=self.repo_name,
2533 repo_id=self.repo_id,
2533 repo_id=self.repo_id,
2534 repo_type=self.repo_type,
2534 repo_type=self.repo_type,
2535 **override)
2535 **override)
2536
2536
2537 def set_state(self, state):
2537 def set_state(self, state):
2538 self.repo_state = state
2538 self.repo_state = state
2539 Session().add(self)
2539 Session().add(self)
2540 #==========================================================================
2540 #==========================================================================
2541 # SCM PROPERTIES
2541 # SCM PROPERTIES
2542 #==========================================================================
2542 #==========================================================================
2543
2543
2544 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2544 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2545 return get_commit_safe(
2545 return get_commit_safe(
2546 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2546 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2547 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2547 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2548
2548
2549 def get_changeset(self, rev=None, pre_load=None):
2549 def get_changeset(self, rev=None, pre_load=None):
2550 warnings.warn("Use get_commit", DeprecationWarning)
2550 warnings.warn("Use get_commit", DeprecationWarning)
2551 commit_id = None
2551 commit_id = None
2552 commit_idx = None
2552 commit_idx = None
2553 if isinstance(rev, str):
2553 if isinstance(rev, str):
2554 commit_id = rev
2554 commit_id = rev
2555 else:
2555 else:
2556 commit_idx = rev
2556 commit_idx = rev
2557 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2557 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2558 pre_load=pre_load)
2558 pre_load=pre_load)
2559
2559
2560 def get_landing_commit(self):
2560 def get_landing_commit(self):
2561 """
2561 """
2562 Returns landing commit, or if that doesn't exist returns the tip
2562 Returns landing commit, or if that doesn't exist returns the tip
2563 """
2563 """
2564 _rev_type, _rev = self.landing_rev
2564 _rev_type, _rev = self.landing_rev
2565 commit = self.get_commit(_rev)
2565 commit = self.get_commit(_rev)
2566 if isinstance(commit, EmptyCommit):
2566 if isinstance(commit, EmptyCommit):
2567 return self.get_commit()
2567 return self.get_commit()
2568 return commit
2568 return commit
2569
2569
2570 def flush_commit_cache(self):
2570 def flush_commit_cache(self):
2571 self.update_commit_cache(cs_cache={'raw_id': '0'})
2571 self.update_commit_cache(cs_cache={'raw_id': '0'})
2572 self.update_commit_cache()
2572 self.update_commit_cache()
2573
2573
2574 def update_commit_cache(self, cs_cache=None, config=None, recursive=True):
2574 def update_commit_cache(self, cs_cache=None, config=None, recursive=True):
2575 """
2575 """
2576 Update cache of last commit for repository
2576 Update cache of last commit for repository
2577 cache_keys should be::
2577 cache_keys should be::
2578
2578
2579 source_repo_id
2579 source_repo_id
2580 short_id
2580 short_id
2581 raw_id
2581 raw_id
2582 revision
2582 revision
2583 parents
2583 parents
2584 message
2584 message
2585 date
2585 date
2586 author
2586 author
2587 updated_on
2587 updated_on
2588
2588
2589 """
2589 """
2590 from rhodecode.lib.vcs.backends.base import BaseCommit
2590 from rhodecode.lib.vcs.backends.base import BaseCommit
2591 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2591 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2592 empty_date = datetime.datetime.fromtimestamp(0)
2592 empty_date = datetime.datetime.fromtimestamp(0)
2593 repo_commit_count = 0
2593 repo_commit_count = 0
2594
2594
2595 if cs_cache is None:
2595 if cs_cache is None:
2596 # use no-cache version here
2596 # use no-cache version here
2597 try:
2597 try:
2598 scm_repo = self.scm_instance(cache=False, config=config)
2598 scm_repo = self.scm_instance(cache=False, config=config)
2599 except VCSError:
2599 except VCSError:
2600 scm_repo = None
2600 scm_repo = None
2601 empty = scm_repo is None or scm_repo.is_empty()
2601 empty = scm_repo is None or scm_repo.is_empty()
2602
2602
2603 if not empty:
2603 if not empty:
2604 cs_cache = scm_repo.get_commit(
2604 cs_cache = scm_repo.get_commit(
2605 pre_load=["author", "date", "message", "parents", "branch"])
2605 pre_load=["author", "date", "message", "parents", "branch"])
2606 repo_commit_count = scm_repo.count()
2606 repo_commit_count = scm_repo.count()
2607 else:
2607 else:
2608 cs_cache = EmptyCommit()
2608 cs_cache = EmptyCommit()
2609
2609
2610 if isinstance(cs_cache, BaseCommit):
2610 if isinstance(cs_cache, BaseCommit):
2611 cs_cache = cs_cache.__json__()
2611 cs_cache = cs_cache.__json__()
2612
2612
2613 def maybe_update_recursive(instance, _config, _recursive, _cs_cache, _last_change):
2613 def maybe_update_recursive(instance, _config, _recursive, _cs_cache, _last_change):
2614 if _recursive:
2614 if _recursive:
2615 repo_id = instance.repo_id
2615 repo_id = instance.repo_id
2616 _cs_cache['source_repo_id'] = repo_id
2616 _cs_cache['source_repo_id'] = repo_id
2617 for gr in instance.groups_with_parents:
2617 for gr in instance.groups_with_parents:
2618 gr.changeset_cache = _cs_cache
2618 gr.changeset_cache = _cs_cache
2619 gr.updated_on = _last_change
2619 gr.updated_on = _last_change
2620
2620
2621 def is_outdated(new_cs_cache):
2621 def is_outdated(new_cs_cache):
2622 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2622 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2623 new_cs_cache['revision'] != self.changeset_cache['revision']):
2623 new_cs_cache['revision'] != self.changeset_cache['revision']):
2624 return True
2624 return True
2625 return False
2625 return False
2626
2626
2627 # check if we have maybe already latest cached revision
2627 # check if we have maybe already latest cached revision
2628 if is_outdated(cs_cache) or not self.changeset_cache:
2628 if is_outdated(cs_cache) or not self.changeset_cache:
2629 _current_datetime = datetime.datetime.utcnow()
2629 _current_datetime = datetime.datetime.utcnow()
2630 last_change = cs_cache.get('date') or _current_datetime
2630 last_change = cs_cache.get('date') or _current_datetime
2631 # we check if last update is newer than the new value
2631 # we check if last update is newer than the new value
2632 # if yes, we use the current timestamp instead. Imagine you get
2632 # if yes, we use the current timestamp instead. Imagine you get
2633 # old commit pushed 1y ago, we'd set last update 1y to ago.
2633 # old commit pushed 1y ago, we'd set last update 1y to ago.
2634 last_change_timestamp = datetime_to_time(last_change)
2634 last_change_timestamp = datetime_to_time(last_change)
2635 current_timestamp = datetime_to_time(last_change)
2635 current_timestamp = datetime_to_time(last_change)
2636 if last_change_timestamp > current_timestamp and not empty:
2636 if last_change_timestamp > current_timestamp and not empty:
2637 cs_cache['date'] = _current_datetime
2637 cs_cache['date'] = _current_datetime
2638
2638
2639 # also store size of repo
2639 # also store size of repo
2640 cs_cache['repo_commit_count'] = repo_commit_count
2640 cs_cache['repo_commit_count'] = repo_commit_count
2641
2641
2642 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2642 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2643 cs_cache['updated_on'] = time.time()
2643 cs_cache['updated_on'] = time.time()
2644 self.changeset_cache = cs_cache
2644 self.changeset_cache = cs_cache
2645 self.updated_on = last_change
2645 self.updated_on = last_change
2646 Session().add(self)
2646 Session().add(self)
2647 maybe_update_recursive(self, config, recursive, cs_cache, last_change)
2647 maybe_update_recursive(self, config, recursive, cs_cache, last_change)
2648 Session().commit()
2648 Session().commit()
2649
2649
2650 else:
2650 else:
2651 if empty:
2651 if empty:
2652 cs_cache = EmptyCommit().__json__()
2652 cs_cache = EmptyCommit().__json__()
2653 else:
2653 else:
2654 cs_cache = self.changeset_cache
2654 cs_cache = self.changeset_cache
2655
2655
2656 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2656 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2657
2657
2658 cs_cache['updated_on'] = time.time()
2658 cs_cache['updated_on'] = time.time()
2659 self.changeset_cache = cs_cache
2659 self.changeset_cache = cs_cache
2660 self.updated_on = _date_latest
2660 self.updated_on = _date_latest
2661 Session().add(self)
2661 Session().add(self)
2662 maybe_update_recursive(self, config, recursive, cs_cache, _date_latest)
2662 maybe_update_recursive(self, config, recursive, cs_cache, _date_latest)
2663 Session().commit()
2663 Session().commit()
2664
2664
2665 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2665 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2666 self.repo_name, cs_cache, _date_latest)
2666 self.repo_name, cs_cache, _date_latest)
2667
2667
2668 @property
2668 @property
2669 def tip(self):
2669 def tip(self):
2670 return self.get_commit('tip')
2670 return self.get_commit('tip')
2671
2671
2672 @property
2672 @property
2673 def author(self):
2673 def author(self):
2674 return self.tip.author
2674 return self.tip.author
2675
2675
2676 @property
2676 @property
2677 def last_change(self):
2677 def last_change(self):
2678 return self.scm_instance().last_change
2678 return self.scm_instance().last_change
2679
2679
2680 def get_comments(self, revisions=None):
2680 def get_comments(self, revisions=None):
2681 """
2681 """
2682 Returns comments for this repository grouped by revisions
2682 Returns comments for this repository grouped by revisions
2683
2683
2684 :param revisions: filter query by revisions only
2684 :param revisions: filter query by revisions only
2685 """
2685 """
2686 cmts = ChangesetComment.query()\
2686 cmts = ChangesetComment.query()\
2687 .filter(ChangesetComment.repo == self)
2687 .filter(ChangesetComment.repo == self)
2688 if revisions:
2688 if revisions:
2689 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2689 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2690 grouped = collections.defaultdict(list)
2690 grouped = collections.defaultdict(list)
2691 for cmt in cmts.all():
2691 for cmt in cmts.all():
2692 grouped[cmt.revision].append(cmt)
2692 grouped[cmt.revision].append(cmt)
2693 return grouped
2693 return grouped
2694
2694
2695 def statuses(self, revisions=None):
2695 def statuses(self, revisions=None):
2696 """
2696 """
2697 Returns statuses for this repository
2697 Returns statuses for this repository
2698
2698
2699 :param revisions: list of revisions to get statuses for
2699 :param revisions: list of revisions to get statuses for
2700 """
2700 """
2701 statuses = ChangesetStatus.query()\
2701 statuses = ChangesetStatus.query()\
2702 .filter(ChangesetStatus.repo == self)\
2702 .filter(ChangesetStatus.repo == self)\
2703 .filter(ChangesetStatus.version == 0)
2703 .filter(ChangesetStatus.version == 0)
2704
2704
2705 if revisions:
2705 if revisions:
2706 # Try doing the filtering in chunks to avoid hitting limits
2706 # Try doing the filtering in chunks to avoid hitting limits
2707 size = 500
2707 size = 500
2708 status_results = []
2708 status_results = []
2709 for chunk in range(0, len(revisions), size):
2709 for chunk in range(0, len(revisions), size):
2710 status_results += statuses.filter(
2710 status_results += statuses.filter(
2711 ChangesetStatus.revision.in_(
2711 ChangesetStatus.revision.in_(
2712 revisions[chunk: chunk+size])
2712 revisions[chunk: chunk+size])
2713 ).all()
2713 ).all()
2714 else:
2714 else:
2715 status_results = statuses.all()
2715 status_results = statuses.all()
2716
2716
2717 grouped = {}
2717 grouped = {}
2718
2718
2719 # maybe we have open new pullrequest without a status?
2719 # maybe we have open new pullrequest without a status?
2720 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2720 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2721 status_lbl = ChangesetStatus.get_status_lbl(stat)
2721 status_lbl = ChangesetStatus.get_status_lbl(stat)
2722 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2722 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2723 for rev in pr.revisions:
2723 for rev in pr.revisions:
2724 pr_id = pr.pull_request_id
2724 pr_id = pr.pull_request_id
2725 pr_repo = pr.target_repo.repo_name
2725 pr_repo = pr.target_repo.repo_name
2726 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2726 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2727
2727
2728 for stat in status_results:
2728 for stat in status_results:
2729 pr_id = pr_repo = None
2729 pr_id = pr_repo = None
2730 if stat.pull_request:
2730 if stat.pull_request:
2731 pr_id = stat.pull_request.pull_request_id
2731 pr_id = stat.pull_request.pull_request_id
2732 pr_repo = stat.pull_request.target_repo.repo_name
2732 pr_repo = stat.pull_request.target_repo.repo_name
2733 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2733 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2734 pr_id, pr_repo]
2734 pr_id, pr_repo]
2735 return grouped
2735 return grouped
2736
2736
2737 # ==========================================================================
2737 # ==========================================================================
2738 # SCM CACHE INSTANCE
2738 # SCM CACHE INSTANCE
2739 # ==========================================================================
2739 # ==========================================================================
2740
2740
2741 def scm_instance(self, **kwargs):
2741 def scm_instance(self, **kwargs):
2742 import rhodecode
2742 import rhodecode
2743
2743
2744 # Passing a config will not hit the cache currently only used
2744 # Passing a config will not hit the cache currently only used
2745 # for repo2dbmapper
2745 # for repo2dbmapper
2746 config = kwargs.pop('config', None)
2746 config = kwargs.pop('config', None)
2747 cache = kwargs.pop('cache', None)
2747 cache = kwargs.pop('cache', None)
2748 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2748 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2749 if vcs_full_cache is not None:
2749 if vcs_full_cache is not None:
2750 # allows override global config
2750 # allows override global config
2751 full_cache = vcs_full_cache
2751 full_cache = vcs_full_cache
2752 else:
2752 else:
2753 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2753 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2754 # if cache is NOT defined use default global, else we have a full
2754 # if cache is NOT defined use default global, else we have a full
2755 # control over cache behaviour
2755 # control over cache behaviour
2756 if cache is None and full_cache and not config:
2756 if cache is None and full_cache and not config:
2757 log.debug('Initializing pure cached instance for %s', self.repo_path)
2757 log.debug('Initializing pure cached instance for %s', self.repo_path)
2758 return self._get_instance_cached()
2758 return self._get_instance_cached()
2759
2759
2760 # cache here is sent to the "vcs server"
2760 # cache here is sent to the "vcs server"
2761 return self._get_instance(cache=bool(cache), config=config)
2761 return self._get_instance(cache=bool(cache), config=config)
2762
2762
2763 def _get_instance_cached(self):
2763 def _get_instance_cached(self):
2764 from rhodecode.lib import rc_cache
2764 from rhodecode.lib import rc_cache
2765
2765
2766 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2766 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2767 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2767 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2768
2768
2769 # we must use thread scoped cache here,
2769 # we must use thread scoped cache here,
2770 # because each thread of gevent needs it's own not shared connection and cache
2770 # because each thread of gevent needs it's own not shared connection and cache
2771 # we also alter `args` so the cache key is individual for every green thread.
2771 # we also alter `args` so the cache key is individual for every green thread.
2772 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2772 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2773 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2773 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2774
2774
2775 # our wrapped caching function that takes state_uid to save the previous state in
2775 # our wrapped caching function that takes state_uid to save the previous state in
2776 def cache_generator(_state_uid):
2776 def cache_generator(_state_uid):
2777
2777
2778 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2778 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2779 def get_instance_cached(_repo_id, _process_context_id):
2779 def get_instance_cached(_repo_id, _process_context_id):
2780 # we save in cached func the generation state so we can detect a change and invalidate caches
2780 # we save in cached func the generation state so we can detect a change and invalidate caches
2781 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2781 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2782
2782
2783 return get_instance_cached
2783 return get_instance_cached
2784
2784
2785 with inv_context_manager as invalidation_context:
2785 with inv_context_manager as invalidation_context:
2786 cache_state_uid = invalidation_context.state_uid
2786 cache_state_uid = invalidation_context.state_uid
2787 cache_func = cache_generator(cache_state_uid)
2787 cache_func = cache_generator(cache_state_uid)
2788
2788
2789 args = self.repo_id, inv_context_manager.proc_key
2789 args = self.repo_id, inv_context_manager.proc_key
2790
2790
2791 previous_state_uid, instance = cache_func(*args)
2791 previous_state_uid, instance = cache_func(*args)
2792
2792
2793 # now compare keys, the "cache" state vs expected state.
2793 # now compare keys, the "cache" state vs expected state.
2794 if previous_state_uid != cache_state_uid:
2794 if previous_state_uid != cache_state_uid:
2795 log.warning('Cached state uid %s is different than current state uid %s',
2795 log.warning('Cached state uid %s is different than current state uid %s',
2796 previous_state_uid, cache_state_uid)
2796 previous_state_uid, cache_state_uid)
2797 _, instance = cache_func.refresh(*args)
2797 _, instance = cache_func.refresh(*args)
2798
2798
2799 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2799 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2800 return instance
2800 return instance
2801
2801
2802 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2802 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2803 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2803 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2804 self.repo_type, self.repo_path, cache)
2804 self.repo_type, self.repo_path, cache)
2805 config = config or self._config
2805 config = config or self._config
2806 custom_wire = {
2806 custom_wire = {
2807 'cache': cache, # controls the vcs.remote cache
2807 'cache': cache, # controls the vcs.remote cache
2808 'repo_state_uid': repo_state_uid
2808 'repo_state_uid': repo_state_uid
2809 }
2809 }
2810
2810
2811 repo = get_vcs_instance(
2811 repo = get_vcs_instance(
2812 repo_path=safe_str(self.repo_full_path),
2812 repo_path=safe_str(self.repo_full_path),
2813 config=config,
2813 config=config,
2814 with_wire=custom_wire,
2814 with_wire=custom_wire,
2815 create=False,
2815 create=False,
2816 _vcs_alias=self.repo_type)
2816 _vcs_alias=self.repo_type)
2817 if repo is not None:
2817 if repo is not None:
2818 repo.count() # cache rebuild
2818 repo.count() # cache rebuild
2819
2819
2820 return repo
2820 return repo
2821
2821
2822 def get_shadow_repository_path(self, workspace_id):
2822 def get_shadow_repository_path(self, workspace_id):
2823 from rhodecode.lib.vcs.backends.base import BaseRepository
2823 from rhodecode.lib.vcs.backends.base import BaseRepository
2824 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2824 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2825 self.repo_full_path, self.repo_id, workspace_id)
2825 self.repo_full_path, self.repo_id, workspace_id)
2826 return shadow_repo_path
2826 return shadow_repo_path
2827
2827
2828 def __json__(self):
2828 def __json__(self):
2829 return {'landing_rev': self.landing_rev}
2829 return {'landing_rev': self.landing_rev}
2830
2830
2831 def get_dict(self):
2831 def get_dict(self):
2832
2832
2833 # Since we transformed `repo_name` to a hybrid property, we need to
2833 # Since we transformed `repo_name` to a hybrid property, we need to
2834 # keep compatibility with the code which uses `repo_name` field.
2834 # keep compatibility with the code which uses `repo_name` field.
2835
2835
2836 result = super(Repository, self).get_dict()
2836 result = super(Repository, self).get_dict()
2837 result['repo_name'] = result.pop('_repo_name', None)
2837 result['repo_name'] = result.pop('_repo_name', None)
2838 result.pop('_changeset_cache', '')
2838 result.pop('_changeset_cache', '')
2839 return result
2839 return result
2840
2840
2841
2841
2842 class RepoGroup(Base, BaseModel):
2842 class RepoGroup(Base, BaseModel):
2843 __tablename__ = 'groups'
2843 __tablename__ = 'groups'
2844 __table_args__ = (
2844 __table_args__ = (
2845 UniqueConstraint('group_name', 'group_parent_id'),
2845 UniqueConstraint('group_name', 'group_parent_id'),
2846 base_table_args,
2846 base_table_args,
2847 )
2847 )
2848
2848
2849 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2849 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2850
2850
2851 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2851 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2852 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2852 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2853 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2853 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2854 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2854 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2855 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2855 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2856 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2856 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2858 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2858 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2859 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2859 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2860 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2860 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2861 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2861 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2862
2862
2863 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2863 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2864 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2864 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2865 parent_group = relationship('RepoGroup', remote_side=group_id)
2865 parent_group = relationship('RepoGroup', remote_side=group_id)
2866 user = relationship('User', back_populates='repository_groups')
2866 user = relationship('User', back_populates='repository_groups')
2867 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2867 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2868
2868
2869 # no cascade, set NULL
2869 # no cascade, set NULL
2870 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2870 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2871
2871
2872 def __init__(self, group_name='', parent_group=None):
2872 def __init__(self, group_name='', parent_group=None):
2873 self.group_name = group_name
2873 self.group_name = group_name
2874 self.parent_group = parent_group
2874 self.parent_group = parent_group
2875
2875
2876 def __repr__(self):
2876 def __repr__(self):
2877 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2877 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2878
2878
2879 @hybrid_property
2879 @hybrid_property
2880 def group_name(self):
2880 def group_name(self):
2881 return self._group_name
2881 return self._group_name
2882
2882
2883 @group_name.setter
2883 @group_name.setter
2884 def group_name(self, value):
2884 def group_name(self, value):
2885 self._group_name = value
2885 self._group_name = value
2886 self.group_name_hash = self.hash_repo_group_name(value)
2886 self.group_name_hash = self.hash_repo_group_name(value)
2887
2887
2888 @classmethod
2888 @classmethod
2889 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2889 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2890 from rhodecode.lib.vcs.backends.base import EmptyCommit
2890 from rhodecode.lib.vcs.backends.base import EmptyCommit
2891 dummy = EmptyCommit().__json__()
2891 dummy = EmptyCommit().__json__()
2892 if not changeset_cache_raw:
2892 if not changeset_cache_raw:
2893 dummy['source_repo_id'] = repo_id
2893 dummy['source_repo_id'] = repo_id
2894 return json.loads(json.dumps(dummy))
2894 return json.loads(json.dumps(dummy))
2895
2895
2896 try:
2896 try:
2897 return json.loads(changeset_cache_raw)
2897 return json.loads(changeset_cache_raw)
2898 except TypeError:
2898 except TypeError:
2899 return dummy
2899 return dummy
2900 except Exception:
2900 except Exception:
2901 log.error(traceback.format_exc())
2901 log.error(traceback.format_exc())
2902 return dummy
2902 return dummy
2903
2903
2904 @hybrid_property
2904 @hybrid_property
2905 def changeset_cache(self):
2905 def changeset_cache(self):
2906 return self._load_changeset_cache('', self._changeset_cache)
2906 return self._load_changeset_cache('', self._changeset_cache)
2907
2907
2908 @changeset_cache.setter
2908 @changeset_cache.setter
2909 def changeset_cache(self, val):
2909 def changeset_cache(self, val):
2910 try:
2910 try:
2911 self._changeset_cache = json.dumps(val)
2911 self._changeset_cache = json.dumps(val)
2912 except Exception:
2912 except Exception:
2913 log.error(traceback.format_exc())
2913 log.error(traceback.format_exc())
2914
2914
2915 @validates('group_parent_id')
2915 @validates('group_parent_id')
2916 def validate_group_parent_id(self, key, val):
2916 def validate_group_parent_id(self, key, val):
2917 """
2917 """
2918 Check cycle references for a parent group to self
2918 Check cycle references for a parent group to self
2919 """
2919 """
2920 if self.group_id and val:
2920 if self.group_id and val:
2921 assert val != self.group_id
2921 assert val != self.group_id
2922
2922
2923 return val
2923 return val
2924
2924
2925 @hybrid_property
2925 @hybrid_property
2926 def description_safe(self):
2926 def description_safe(self):
2927 return description_escaper(self.group_description)
2927 return description_escaper(self.group_description)
2928
2928
2929 @classmethod
2929 @classmethod
2930 def hash_repo_group_name(cls, repo_group_name):
2930 def hash_repo_group_name(cls, repo_group_name):
2931 val = remove_formatting(repo_group_name)
2931 val = remove_formatting(repo_group_name)
2932 val = safe_str(val).lower()
2932 val = safe_str(val).lower()
2933 chars = []
2933 chars = []
2934 for c in val:
2934 for c in val:
2935 if c not in string.ascii_letters:
2935 if c not in string.ascii_letters:
2936 c = str(ord(c))
2936 c = str(ord(c))
2937 chars.append(c)
2937 chars.append(c)
2938
2938
2939 return ''.join(chars)
2939 return ''.join(chars)
2940
2940
2941 @classmethod
2941 @classmethod
2942 def _generate_choice(cls, repo_group):
2942 def _generate_choice(cls, repo_group):
2943 from webhelpers2.html import literal as _literal
2943 from webhelpers2.html import literal as _literal
2944
2944
2945 def _name(k):
2945 def _name(k):
2946 return _literal(cls.CHOICES_SEPARATOR.join(k))
2946 return _literal(cls.CHOICES_SEPARATOR.join(k))
2947
2947
2948 return repo_group.group_id, _name(repo_group.full_path_splitted)
2948 return repo_group.group_id, _name(repo_group.full_path_splitted)
2949
2949
2950 @classmethod
2950 @classmethod
2951 def groups_choices(cls, groups=None, show_empty_group=True):
2951 def groups_choices(cls, groups=None, show_empty_group=True):
2952 if not groups:
2952 if not groups:
2953 groups = cls.query().all()
2953 groups = cls.query().all()
2954
2954
2955 repo_groups = []
2955 repo_groups = []
2956 if show_empty_group:
2956 if show_empty_group:
2957 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2957 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2958
2958
2959 repo_groups.extend([cls._generate_choice(x) for x in groups])
2959 repo_groups.extend([cls._generate_choice(x) for x in groups])
2960
2960
2961 repo_groups = sorted(
2961 repo_groups = sorted(
2962 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2962 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2963 return repo_groups
2963 return repo_groups
2964
2964
2965 @classmethod
2965 @classmethod
2966 def url_sep(cls):
2966 def url_sep(cls):
2967 return URL_SEP
2967 return URL_SEP
2968
2968
2969 @classmethod
2969 @classmethod
2970 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2970 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2971 if case_insensitive:
2971 if case_insensitive:
2972 gr = cls.query().filter(func.lower(cls.group_name)
2972 gr = cls.query().filter(func.lower(cls.group_name)
2973 == func.lower(group_name))
2973 == func.lower(group_name))
2974 else:
2974 else:
2975 gr = cls.query().filter(cls.group_name == group_name)
2975 gr = cls.query().filter(cls.group_name == group_name)
2976 if cache:
2976 if cache:
2977 name_key = _hash_key(group_name)
2977 name_key = _hash_key(group_name)
2978 gr = gr.options(
2978 gr = gr.options(
2979 FromCache("sql_cache_short", f"get_group_{name_key}"))
2979 FromCache("sql_cache_short", f"get_group_{name_key}"))
2980 return gr.scalar()
2980 return gr.scalar()
2981
2981
2982 @classmethod
2982 @classmethod
2983 def get_user_personal_repo_group(cls, user_id):
2983 def get_user_personal_repo_group(cls, user_id):
2984 user = User.get(user_id)
2984 user = User.get(user_id)
2985 if user.username == User.DEFAULT_USER:
2985 if user.username == User.DEFAULT_USER:
2986 return None
2986 return None
2987
2987
2988 return cls.query()\
2988 return cls.query()\
2989 .filter(cls.personal == true()) \
2989 .filter(cls.personal == true()) \
2990 .filter(cls.user == user) \
2990 .filter(cls.user == user) \
2991 .order_by(cls.group_id.asc()) \
2991 .order_by(cls.group_id.asc()) \
2992 .first()
2992 .first()
2993
2993
2994 @classmethod
2994 @classmethod
2995 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2995 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2996 case_insensitive=True):
2996 case_insensitive=True):
2997 q = RepoGroup.query()
2997 q = RepoGroup.query()
2998
2998
2999 if not isinstance(user_id, Optional):
2999 if not isinstance(user_id, Optional):
3000 q = q.filter(RepoGroup.user_id == user_id)
3000 q = q.filter(RepoGroup.user_id == user_id)
3001
3001
3002 if not isinstance(group_id, Optional):
3002 if not isinstance(group_id, Optional):
3003 q = q.filter(RepoGroup.group_parent_id == group_id)
3003 q = q.filter(RepoGroup.group_parent_id == group_id)
3004
3004
3005 if case_insensitive:
3005 if case_insensitive:
3006 q = q.order_by(func.lower(RepoGroup.group_name))
3006 q = q.order_by(func.lower(RepoGroup.group_name))
3007 else:
3007 else:
3008 q = q.order_by(RepoGroup.group_name)
3008 q = q.order_by(RepoGroup.group_name)
3009 return q.all()
3009 return q.all()
3010
3010
3011 @property
3011 @property
3012 def parents(self, parents_recursion_limit=10):
3012 def parents(self, parents_recursion_limit=10):
3013 groups = []
3013 groups = []
3014 if self.parent_group is None:
3014 if self.parent_group is None:
3015 return groups
3015 return groups
3016 cur_gr = self.parent_group
3016 cur_gr = self.parent_group
3017 groups.insert(0, cur_gr)
3017 groups.insert(0, cur_gr)
3018 cnt = 0
3018 cnt = 0
3019 while 1:
3019 while 1:
3020 cnt += 1
3020 cnt += 1
3021 gr = getattr(cur_gr, 'parent_group', None)
3021 gr = getattr(cur_gr, 'parent_group', None)
3022 cur_gr = cur_gr.parent_group
3022 cur_gr = cur_gr.parent_group
3023 if gr is None:
3023 if gr is None:
3024 break
3024 break
3025 if cnt == parents_recursion_limit:
3025 if cnt == parents_recursion_limit:
3026 # this will prevent accidental infinit loops
3026 # this will prevent accidental infinit loops
3027 log.error('more than %s parents found for group %s, stopping '
3027 log.error('more than %s parents found for group %s, stopping '
3028 'recursive parent fetching', parents_recursion_limit, self)
3028 'recursive parent fetching', parents_recursion_limit, self)
3029 break
3029 break
3030
3030
3031 groups.insert(0, gr)
3031 groups.insert(0, gr)
3032 return groups
3032 return groups
3033
3033
3034 @property
3034 @property
3035 def last_commit_cache_update_diff(self):
3035 def last_commit_cache_update_diff(self):
3036 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3036 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3037
3037
3038 @classmethod
3038 @classmethod
3039 def _load_commit_change(cls, last_commit_cache):
3039 def _load_commit_change(cls, last_commit_cache):
3040 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3040 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3041 empty_date = datetime.datetime.fromtimestamp(0)
3041 empty_date = datetime.datetime.fromtimestamp(0)
3042 date_latest = last_commit_cache.get('date', empty_date)
3042 date_latest = last_commit_cache.get('date', empty_date)
3043 try:
3043 try:
3044 return parse_datetime(date_latest)
3044 return parse_datetime(date_latest)
3045 except Exception:
3045 except Exception:
3046 return empty_date
3046 return empty_date
3047
3047
3048 @property
3048 @property
3049 def last_commit_change(self):
3049 def last_commit_change(self):
3050 return self._load_commit_change(self.changeset_cache)
3050 return self._load_commit_change(self.changeset_cache)
3051
3051
3052 @property
3052 @property
3053 def last_db_change(self):
3053 def last_db_change(self):
3054 return self.updated_on
3054 return self.updated_on
3055
3055
3056 @property
3056 @property
3057 def children(self):
3057 def children(self):
3058 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3058 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3059
3059
3060 @property
3060 @property
3061 def name(self):
3061 def name(self):
3062 return self.group_name.split(RepoGroup.url_sep())[-1]
3062 return self.group_name.split(RepoGroup.url_sep())[-1]
3063
3063
3064 @property
3064 @property
3065 def full_path(self):
3065 def full_path(self):
3066 return self.group_name
3066 return self.group_name
3067
3067
3068 @property
3068 @property
3069 def full_path_splitted(self):
3069 def full_path_splitted(self):
3070 return self.group_name.split(RepoGroup.url_sep())
3070 return self.group_name.split(RepoGroup.url_sep())
3071
3071
3072 @property
3072 @property
3073 def repositories(self):
3073 def repositories(self):
3074 return Repository.query()\
3074 return Repository.query()\
3075 .filter(Repository.group == self)\
3075 .filter(Repository.group == self)\
3076 .order_by(Repository.repo_name)
3076 .order_by(Repository.repo_name)
3077
3077
3078 @property
3078 @property
3079 def repositories_recursive_count(self):
3079 def repositories_recursive_count(self):
3080 cnt = self.repositories.count()
3080 cnt = self.repositories.count()
3081
3081
3082 def children_count(group):
3082 def children_count(group):
3083 cnt = 0
3083 cnt = 0
3084 for child in group.children:
3084 for child in group.children:
3085 cnt += child.repositories.count()
3085 cnt += child.repositories.count()
3086 cnt += children_count(child)
3086 cnt += children_count(child)
3087 return cnt
3087 return cnt
3088
3088
3089 return cnt + children_count(self)
3089 return cnt + children_count(self)
3090
3090
3091 def _recursive_objects(self, include_repos=True, include_groups=True):
3091 def _recursive_objects(self, include_repos=True, include_groups=True):
3092 all_ = []
3092 all_ = []
3093
3093
3094 def _get_members(root_gr):
3094 def _get_members(root_gr):
3095 if include_repos:
3095 if include_repos:
3096 for r in root_gr.repositories:
3096 for r in root_gr.repositories:
3097 all_.append(r)
3097 all_.append(r)
3098 childs = root_gr.children.all()
3098 childs = root_gr.children.all()
3099 if childs:
3099 if childs:
3100 for gr in childs:
3100 for gr in childs:
3101 if include_groups:
3101 if include_groups:
3102 all_.append(gr)
3102 all_.append(gr)
3103 _get_members(gr)
3103 _get_members(gr)
3104
3104
3105 root_group = []
3105 root_group = []
3106 if include_groups:
3106 if include_groups:
3107 root_group = [self]
3107 root_group = [self]
3108
3108
3109 _get_members(self)
3109 _get_members(self)
3110 return root_group + all_
3110 return root_group + all_
3111
3111
3112 def recursive_groups_and_repos(self):
3112 def recursive_groups_and_repos(self):
3113 """
3113 """
3114 Recursive return all groups, with repositories in those groups
3114 Recursive return all groups, with repositories in those groups
3115 """
3115 """
3116 return self._recursive_objects()
3116 return self._recursive_objects()
3117
3117
3118 def recursive_groups(self):
3118 def recursive_groups(self):
3119 """
3119 """
3120 Returns all children groups for this group including children of children
3120 Returns all children groups for this group including children of children
3121 """
3121 """
3122 return self._recursive_objects(include_repos=False)
3122 return self._recursive_objects(include_repos=False)
3123
3123
3124 def recursive_repos(self):
3124 def recursive_repos(self):
3125 """
3125 """
3126 Returns all children repositories for this group
3126 Returns all children repositories for this group
3127 """
3127 """
3128 return self._recursive_objects(include_groups=False)
3128 return self._recursive_objects(include_groups=False)
3129
3129
3130 def get_new_name(self, group_name):
3130 def get_new_name(self, group_name):
3131 """
3131 """
3132 returns new full group name based on parent and new name
3132 returns new full group name based on parent and new name
3133
3133
3134 :param group_name:
3134 :param group_name:
3135 """
3135 """
3136 path_prefix = (self.parent_group.full_path_splitted if
3136 path_prefix = (self.parent_group.full_path_splitted if
3137 self.parent_group else [])
3137 self.parent_group else [])
3138 return RepoGroup.url_sep().join(path_prefix + [group_name])
3138 return RepoGroup.url_sep().join(path_prefix + [group_name])
3139
3139
3140 def update_commit_cache(self, config=None):
3140 def update_commit_cache(self, config=None):
3141 """
3141 """
3142 Update cache of last commit for newest repository inside this repository group.
3142 Update cache of last commit for newest repository inside this repository group.
3143 cache_keys should be::
3143 cache_keys should be::
3144
3144
3145 source_repo_id
3145 source_repo_id
3146 short_id
3146 short_id
3147 raw_id
3147 raw_id
3148 revision
3148 revision
3149 parents
3149 parents
3150 message
3150 message
3151 date
3151 date
3152 author
3152 author
3153
3153
3154 """
3154 """
3155 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3155 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3156 empty_date = datetime.datetime.fromtimestamp(0)
3156 empty_date = datetime.datetime.fromtimestamp(0)
3157
3157
3158 def repo_groups_and_repos(root_gr):
3158 def repo_groups_and_repos(root_gr):
3159 for _repo in root_gr.repositories:
3159 for _repo in root_gr.repositories:
3160 yield _repo
3160 yield _repo
3161 for child_group in root_gr.children.all():
3161 for child_group in root_gr.children.all():
3162 yield child_group
3162 yield child_group
3163
3163
3164 latest_repo_cs_cache = {}
3164 latest_repo_cs_cache = {}
3165 for obj in repo_groups_and_repos(self):
3165 for obj in repo_groups_and_repos(self):
3166 repo_cs_cache = obj.changeset_cache
3166 repo_cs_cache = obj.changeset_cache
3167 date_latest = latest_repo_cs_cache.get('date', empty_date)
3167 date_latest = latest_repo_cs_cache.get('date', empty_date)
3168 date_current = repo_cs_cache.get('date', empty_date)
3168 date_current = repo_cs_cache.get('date', empty_date)
3169 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3169 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3170 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3170 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3171 latest_repo_cs_cache = repo_cs_cache
3171 latest_repo_cs_cache = repo_cs_cache
3172 if hasattr(obj, 'repo_id'):
3172 if hasattr(obj, 'repo_id'):
3173 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3173 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3174 else:
3174 else:
3175 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3175 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3176
3176
3177 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3177 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3178
3178
3179 latest_repo_cs_cache['updated_on'] = time.time()
3179 latest_repo_cs_cache['updated_on'] = time.time()
3180 self.changeset_cache = latest_repo_cs_cache
3180 self.changeset_cache = latest_repo_cs_cache
3181 self.updated_on = _date_latest
3181 self.updated_on = _date_latest
3182 Session().add(self)
3182 Session().add(self)
3183 Session().commit()
3183 Session().commit()
3184
3184
3185 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3185 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3186 self.group_name, latest_repo_cs_cache, _date_latest)
3186 self.group_name, latest_repo_cs_cache, _date_latest)
3187
3187
3188 def permissions(self, with_admins=True, with_owner=True,
3188 def permissions(self, with_admins=True, with_owner=True,
3189 expand_from_user_groups=False):
3189 expand_from_user_groups=False):
3190 """
3190 """
3191 Permissions for repository groups
3191 Permissions for repository groups
3192 """
3192 """
3193 _admin_perm = 'group.admin'
3193 _admin_perm = 'group.admin'
3194
3194
3195 owner_row = []
3195 owner_row = []
3196 if with_owner:
3196 if with_owner:
3197 usr = AttributeDict(self.user.get_dict())
3197 usr = AttributeDict(self.user.get_dict())
3198 usr.owner_row = True
3198 usr.owner_row = True
3199 usr.permission = _admin_perm
3199 usr.permission = _admin_perm
3200 owner_row.append(usr)
3200 owner_row.append(usr)
3201
3201
3202 super_admin_ids = []
3202 super_admin_ids = []
3203 super_admin_rows = []
3203 super_admin_rows = []
3204 if with_admins:
3204 if with_admins:
3205 for usr in User.get_all_super_admins():
3205 for usr in User.get_all_super_admins():
3206 super_admin_ids.append(usr.user_id)
3206 super_admin_ids.append(usr.user_id)
3207 # if this admin is also owner, don't double the record
3207 # if this admin is also owner, don't double the record
3208 if usr.user_id == owner_row[0].user_id:
3208 if usr.user_id == owner_row[0].user_id:
3209 owner_row[0].admin_row = True
3209 owner_row[0].admin_row = True
3210 else:
3210 else:
3211 usr = AttributeDict(usr.get_dict())
3211 usr = AttributeDict(usr.get_dict())
3212 usr.admin_row = True
3212 usr.admin_row = True
3213 usr.permission = _admin_perm
3213 usr.permission = _admin_perm
3214 super_admin_rows.append(usr)
3214 super_admin_rows.append(usr)
3215
3215
3216 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3216 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3217 q = q.options(joinedload(UserRepoGroupToPerm.group),
3217 q = q.options(joinedload(UserRepoGroupToPerm.group),
3218 joinedload(UserRepoGroupToPerm.user),
3218 joinedload(UserRepoGroupToPerm.user),
3219 joinedload(UserRepoGroupToPerm.permission),)
3219 joinedload(UserRepoGroupToPerm.permission),)
3220
3220
3221 # get owners and admins and permissions. We do a trick of re-writing
3221 # get owners and admins and permissions. We do a trick of re-writing
3222 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3222 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3223 # has a global reference and changing one object propagates to all
3223 # has a global reference and changing one object propagates to all
3224 # others. This means if admin is also an owner admin_row that change
3224 # others. This means if admin is also an owner admin_row that change
3225 # would propagate to both objects
3225 # would propagate to both objects
3226 perm_rows = []
3226 perm_rows = []
3227 for _usr in q.all():
3227 for _usr in q.all():
3228 usr = AttributeDict(_usr.user.get_dict())
3228 usr = AttributeDict(_usr.user.get_dict())
3229 # if this user is also owner/admin, mark as duplicate record
3229 # if this user is also owner/admin, mark as duplicate record
3230 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3230 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3231 usr.duplicate_perm = True
3231 usr.duplicate_perm = True
3232 usr.permission = _usr.permission.permission_name
3232 usr.permission = _usr.permission.permission_name
3233 perm_rows.append(usr)
3233 perm_rows.append(usr)
3234
3234
3235 # filter the perm rows by 'default' first and then sort them by
3235 # filter the perm rows by 'default' first and then sort them by
3236 # admin,write,read,none permissions sorted again alphabetically in
3236 # admin,write,read,none permissions sorted again alphabetically in
3237 # each group
3237 # each group
3238 perm_rows = sorted(perm_rows, key=display_user_sort)
3238 perm_rows = sorted(perm_rows, key=display_user_sort)
3239
3239
3240 user_groups_rows = []
3240 user_groups_rows = []
3241 if expand_from_user_groups:
3241 if expand_from_user_groups:
3242 for ug in self.permission_user_groups(with_members=True):
3242 for ug in self.permission_user_groups(with_members=True):
3243 for user_data in ug.members:
3243 for user_data in ug.members:
3244 user_groups_rows.append(user_data)
3244 user_groups_rows.append(user_data)
3245
3245
3246 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3246 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3247
3247
3248 def permission_user_groups(self, with_members=False):
3248 def permission_user_groups(self, with_members=False):
3249 q = UserGroupRepoGroupToPerm.query()\
3249 q = UserGroupRepoGroupToPerm.query()\
3250 .filter(UserGroupRepoGroupToPerm.group == self)
3250 .filter(UserGroupRepoGroupToPerm.group == self)
3251 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3251 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3252 joinedload(UserGroupRepoGroupToPerm.users_group),
3252 joinedload(UserGroupRepoGroupToPerm.users_group),
3253 joinedload(UserGroupRepoGroupToPerm.permission),)
3253 joinedload(UserGroupRepoGroupToPerm.permission),)
3254
3254
3255 perm_rows = []
3255 perm_rows = []
3256 for _user_group in q.all():
3256 for _user_group in q.all():
3257 entry = AttributeDict(_user_group.users_group.get_dict())
3257 entry = AttributeDict(_user_group.users_group.get_dict())
3258 entry.permission = _user_group.permission.permission_name
3258 entry.permission = _user_group.permission.permission_name
3259 if with_members:
3259 if with_members:
3260 entry.members = [x.user.get_dict()
3260 entry.members = [x.user.get_dict()
3261 for x in _user_group.users_group.members]
3261 for x in _user_group.users_group.members]
3262 perm_rows.append(entry)
3262 perm_rows.append(entry)
3263
3263
3264 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3264 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3265 return perm_rows
3265 return perm_rows
3266
3266
3267 def get_api_data(self):
3267 def get_api_data(self):
3268 """
3268 """
3269 Common function for generating api data
3269 Common function for generating api data
3270
3270
3271 """
3271 """
3272 group = self
3272 group = self
3273 data = {
3273 data = {
3274 'group_id': group.group_id,
3274 'group_id': group.group_id,
3275 'group_name': group.group_name,
3275 'group_name': group.group_name,
3276 'group_description': group.description_safe,
3276 'group_description': group.description_safe,
3277 'parent_group': group.parent_group.group_name if group.parent_group else None,
3277 'parent_group': group.parent_group.group_name if group.parent_group else None,
3278 'repositories': [x.repo_name for x in group.repositories],
3278 'repositories': [x.repo_name for x in group.repositories],
3279 'owner': group.user.username,
3279 'owner': group.user.username,
3280 }
3280 }
3281 return data
3281 return data
3282
3282
3283 def get_dict(self):
3283 def get_dict(self):
3284 # Since we transformed `group_name` to a hybrid property, we need to
3284 # Since we transformed `group_name` to a hybrid property, we need to
3285 # keep compatibility with the code which uses `group_name` field.
3285 # keep compatibility with the code which uses `group_name` field.
3286 result = super(RepoGroup, self).get_dict()
3286 result = super(RepoGroup, self).get_dict()
3287 result['group_name'] = result.pop('_group_name', None)
3287 result['group_name'] = result.pop('_group_name', None)
3288 result.pop('_changeset_cache', '')
3288 result.pop('_changeset_cache', '')
3289 return result
3289 return result
3290
3290
3291
3291
3292 class Permission(Base, BaseModel):
3292 class Permission(Base, BaseModel):
3293 __tablename__ = 'permissions'
3293 __tablename__ = 'permissions'
3294 __table_args__ = (
3294 __table_args__ = (
3295 Index('p_perm_name_idx', 'permission_name'),
3295 Index('p_perm_name_idx', 'permission_name'),
3296 base_table_args,
3296 base_table_args,
3297 )
3297 )
3298
3298
3299 PERMS = [
3299 PERMS = [
3300 ('hg.admin', _('RhodeCode Super Administrator')),
3300 ('hg.admin', _('RhodeCode Super Administrator')),
3301
3301
3302 ('repository.none', _('Repository no access')),
3302 ('repository.none', _('Repository no access')),
3303 ('repository.read', _('Repository read access')),
3303 ('repository.read', _('Repository read access')),
3304 ('repository.write', _('Repository write access')),
3304 ('repository.write', _('Repository write access')),
3305 ('repository.admin', _('Repository admin access')),
3305 ('repository.admin', _('Repository admin access')),
3306
3306
3307 ('group.none', _('Repository group no access')),
3307 ('group.none', _('Repository group no access')),
3308 ('group.read', _('Repository group read access')),
3308 ('group.read', _('Repository group read access')),
3309 ('group.write', _('Repository group write access')),
3309 ('group.write', _('Repository group write access')),
3310 ('group.admin', _('Repository group admin access')),
3310 ('group.admin', _('Repository group admin access')),
3311
3311
3312 ('usergroup.none', _('User group no access')),
3312 ('usergroup.none', _('User group no access')),
3313 ('usergroup.read', _('User group read access')),
3313 ('usergroup.read', _('User group read access')),
3314 ('usergroup.write', _('User group write access')),
3314 ('usergroup.write', _('User group write access')),
3315 ('usergroup.admin', _('User group admin access')),
3315 ('usergroup.admin', _('User group admin access')),
3316
3316
3317 ('branch.none', _('Branch no permissions')),
3317 ('branch.none', _('Branch no permissions')),
3318 ('branch.merge', _('Branch access by web merge')),
3318 ('branch.merge', _('Branch access by web merge')),
3319 ('branch.push', _('Branch access by push')),
3319 ('branch.push', _('Branch access by push')),
3320 ('branch.push_force', _('Branch access by push with force')),
3320 ('branch.push_force', _('Branch access by push with force')),
3321
3321
3322 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3322 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3323 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3323 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3324
3324
3325 ('hg.usergroup.create.false', _('User Group creation disabled')),
3325 ('hg.usergroup.create.false', _('User Group creation disabled')),
3326 ('hg.usergroup.create.true', _('User Group creation enabled')),
3326 ('hg.usergroup.create.true', _('User Group creation enabled')),
3327
3327
3328 ('hg.create.none', _('Repository creation disabled')),
3328 ('hg.create.none', _('Repository creation disabled')),
3329 ('hg.create.repository', _('Repository creation enabled')),
3329 ('hg.create.repository', _('Repository creation enabled')),
3330 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3330 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3331 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3331 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3332
3332
3333 ('hg.fork.none', _('Repository forking disabled')),
3333 ('hg.fork.none', _('Repository forking disabled')),
3334 ('hg.fork.repository', _('Repository forking enabled')),
3334 ('hg.fork.repository', _('Repository forking enabled')),
3335
3335
3336 ('hg.register.none', _('Registration disabled')),
3336 ('hg.register.none', _('Registration disabled')),
3337 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3337 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3338 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3338 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3339
3339
3340 ('hg.password_reset.enabled', _('Password reset enabled')),
3340 ('hg.password_reset.enabled', _('Password reset enabled')),
3341 ('hg.password_reset.hidden', _('Password reset hidden')),
3341 ('hg.password_reset.hidden', _('Password reset hidden')),
3342 ('hg.password_reset.disabled', _('Password reset disabled')),
3342 ('hg.password_reset.disabled', _('Password reset disabled')),
3343
3343
3344 ('hg.extern_activate.manual', _('Manual activation of external account')),
3344 ('hg.extern_activate.manual', _('Manual activation of external account')),
3345 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3345 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3346
3346
3347 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3347 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3348 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3348 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3349 ]
3349 ]
3350
3350
3351 # definition of system default permissions for DEFAULT user, created on
3351 # definition of system default permissions for DEFAULT user, created on
3352 # system setup
3352 # system setup
3353 DEFAULT_USER_PERMISSIONS = [
3353 DEFAULT_USER_PERMISSIONS = [
3354 # object perms
3354 # object perms
3355 'repository.read',
3355 'repository.read',
3356 'group.read',
3356 'group.read',
3357 'usergroup.read',
3357 'usergroup.read',
3358 # branch, for backward compat we need same value as before so forced pushed
3358 # branch, for backward compat we need same value as before so forced pushed
3359 'branch.push_force',
3359 'branch.push_force',
3360 # global
3360 # global
3361 'hg.create.repository',
3361 'hg.create.repository',
3362 'hg.repogroup.create.false',
3362 'hg.repogroup.create.false',
3363 'hg.usergroup.create.false',
3363 'hg.usergroup.create.false',
3364 'hg.create.write_on_repogroup.true',
3364 'hg.create.write_on_repogroup.true',
3365 'hg.fork.repository',
3365 'hg.fork.repository',
3366 'hg.register.manual_activate',
3366 'hg.register.manual_activate',
3367 'hg.password_reset.enabled',
3367 'hg.password_reset.enabled',
3368 'hg.extern_activate.auto',
3368 'hg.extern_activate.auto',
3369 'hg.inherit_default_perms.true',
3369 'hg.inherit_default_perms.true',
3370 ]
3370 ]
3371
3371
3372 # defines which permissions are more important higher the more important
3372 # defines which permissions are more important higher the more important
3373 # Weight defines which permissions are more important.
3373 # Weight defines which permissions are more important.
3374 # The higher number the more important.
3374 # The higher number the more important.
3375 PERM_WEIGHTS = {
3375 PERM_WEIGHTS = {
3376 'repository.none': 0,
3376 'repository.none': 0,
3377 'repository.read': 1,
3377 'repository.read': 1,
3378 'repository.write': 3,
3378 'repository.write': 3,
3379 'repository.admin': 4,
3379 'repository.admin': 4,
3380
3380
3381 'group.none': 0,
3381 'group.none': 0,
3382 'group.read': 1,
3382 'group.read': 1,
3383 'group.write': 3,
3383 'group.write': 3,
3384 'group.admin': 4,
3384 'group.admin': 4,
3385
3385
3386 'usergroup.none': 0,
3386 'usergroup.none': 0,
3387 'usergroup.read': 1,
3387 'usergroup.read': 1,
3388 'usergroup.write': 3,
3388 'usergroup.write': 3,
3389 'usergroup.admin': 4,
3389 'usergroup.admin': 4,
3390
3390
3391 'branch.none': 0,
3391 'branch.none': 0,
3392 'branch.merge': 1,
3392 'branch.merge': 1,
3393 'branch.push': 3,
3393 'branch.push': 3,
3394 'branch.push_force': 4,
3394 'branch.push_force': 4,
3395
3395
3396 'hg.repogroup.create.false': 0,
3396 'hg.repogroup.create.false': 0,
3397 'hg.repogroup.create.true': 1,
3397 'hg.repogroup.create.true': 1,
3398
3398
3399 'hg.usergroup.create.false': 0,
3399 'hg.usergroup.create.false': 0,
3400 'hg.usergroup.create.true': 1,
3400 'hg.usergroup.create.true': 1,
3401
3401
3402 'hg.fork.none': 0,
3402 'hg.fork.none': 0,
3403 'hg.fork.repository': 1,
3403 'hg.fork.repository': 1,
3404 'hg.create.none': 0,
3404 'hg.create.none': 0,
3405 'hg.create.repository': 1
3405 'hg.create.repository': 1
3406 }
3406 }
3407
3407
3408 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3408 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3409 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3409 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3410 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3410 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3411
3411
3412 def __repr__(self):
3412 def __repr__(self):
3413 return "<%s('%s:%s')>" % (
3413 return "<%s('%s:%s')>" % (
3414 self.cls_name, self.permission_id, self.permission_name
3414 self.cls_name, self.permission_id, self.permission_name
3415 )
3415 )
3416
3416
3417 @classmethod
3417 @classmethod
3418 def get_by_key(cls, key):
3418 def get_by_key(cls, key):
3419 return cls.query().filter(cls.permission_name == key).scalar()
3419 return cls.query().filter(cls.permission_name == key).scalar()
3420
3420
3421 @classmethod
3421 @classmethod
3422 def get_default_repo_perms(cls, user_id, repo_id=None):
3422 def get_default_repo_perms(cls, user_id, repo_id=None):
3423 q = Session().query(UserRepoToPerm, Repository, Permission)\
3423 q = Session().query(UserRepoToPerm, Repository, Permission)\
3424 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3424 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3425 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3425 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3426 .filter(UserRepoToPerm.user_id == user_id)
3426 .filter(UserRepoToPerm.user_id == user_id)
3427 if repo_id:
3427 if repo_id:
3428 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3428 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3429 return q.all()
3429 return q.all()
3430
3430
3431 @classmethod
3431 @classmethod
3432 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3432 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3433 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3433 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3434 .join(
3434 .join(
3435 Permission,
3435 Permission,
3436 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3436 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3437 .join(
3437 .join(
3438 UserRepoToPerm,
3438 UserRepoToPerm,
3439 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3439 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3440 .filter(UserRepoToPerm.user_id == user_id)
3440 .filter(UserRepoToPerm.user_id == user_id)
3441
3441
3442 if repo_id:
3442 if repo_id:
3443 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3443 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3444 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3444 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3445
3445
3446 @classmethod
3446 @classmethod
3447 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3447 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3448 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3448 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3449 .join(
3449 .join(
3450 Permission,
3450 Permission,
3451 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3451 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3452 .join(
3452 .join(
3453 Repository,
3453 Repository,
3454 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3454 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3455 .join(
3455 .join(
3456 UserGroup,
3456 UserGroup,
3457 UserGroupRepoToPerm.users_group_id ==
3457 UserGroupRepoToPerm.users_group_id ==
3458 UserGroup.users_group_id)\
3458 UserGroup.users_group_id)\
3459 .join(
3459 .join(
3460 UserGroupMember,
3460 UserGroupMember,
3461 UserGroupRepoToPerm.users_group_id ==
3461 UserGroupRepoToPerm.users_group_id ==
3462 UserGroupMember.users_group_id)\
3462 UserGroupMember.users_group_id)\
3463 .filter(
3463 .filter(
3464 UserGroupMember.user_id == user_id,
3464 UserGroupMember.user_id == user_id,
3465 UserGroup.users_group_active == true())
3465 UserGroup.users_group_active == true())
3466 if repo_id:
3466 if repo_id:
3467 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3467 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3468 return q.all()
3468 return q.all()
3469
3469
3470 @classmethod
3470 @classmethod
3471 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3471 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3472 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3472 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3473 .join(
3473 .join(
3474 Permission,
3474 Permission,
3475 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3475 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3476 .join(
3476 .join(
3477 UserGroupRepoToPerm,
3477 UserGroupRepoToPerm,
3478 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3478 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3479 .join(
3479 .join(
3480 UserGroup,
3480 UserGroup,
3481 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3481 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3482 .join(
3482 .join(
3483 UserGroupMember,
3483 UserGroupMember,
3484 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3484 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3485 .filter(
3485 .filter(
3486 UserGroupMember.user_id == user_id,
3486 UserGroupMember.user_id == user_id,
3487 UserGroup.users_group_active == true())
3487 UserGroup.users_group_active == true())
3488
3488
3489 if repo_id:
3489 if repo_id:
3490 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3490 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3491 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3491 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3492
3492
3493 @classmethod
3493 @classmethod
3494 def get_default_group_perms(cls, user_id, repo_group_id=None):
3494 def get_default_group_perms(cls, user_id, repo_group_id=None):
3495 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3495 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3496 .join(
3496 .join(
3497 Permission,
3497 Permission,
3498 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3498 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3499 .join(
3499 .join(
3500 RepoGroup,
3500 RepoGroup,
3501 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3501 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3502 .filter(UserRepoGroupToPerm.user_id == user_id)
3502 .filter(UserRepoGroupToPerm.user_id == user_id)
3503 if repo_group_id:
3503 if repo_group_id:
3504 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3504 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3505 return q.all()
3505 return q.all()
3506
3506
3507 @classmethod
3507 @classmethod
3508 def get_default_group_perms_from_user_group(
3508 def get_default_group_perms_from_user_group(
3509 cls, user_id, repo_group_id=None):
3509 cls, user_id, repo_group_id=None):
3510 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3510 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3511 .join(
3511 .join(
3512 Permission,
3512 Permission,
3513 UserGroupRepoGroupToPerm.permission_id ==
3513 UserGroupRepoGroupToPerm.permission_id ==
3514 Permission.permission_id)\
3514 Permission.permission_id)\
3515 .join(
3515 .join(
3516 RepoGroup,
3516 RepoGroup,
3517 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3517 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3518 .join(
3518 .join(
3519 UserGroup,
3519 UserGroup,
3520 UserGroupRepoGroupToPerm.users_group_id ==
3520 UserGroupRepoGroupToPerm.users_group_id ==
3521 UserGroup.users_group_id)\
3521 UserGroup.users_group_id)\
3522 .join(
3522 .join(
3523 UserGroupMember,
3523 UserGroupMember,
3524 UserGroupRepoGroupToPerm.users_group_id ==
3524 UserGroupRepoGroupToPerm.users_group_id ==
3525 UserGroupMember.users_group_id)\
3525 UserGroupMember.users_group_id)\
3526 .filter(
3526 .filter(
3527 UserGroupMember.user_id == user_id,
3527 UserGroupMember.user_id == user_id,
3528 UserGroup.users_group_active == true())
3528 UserGroup.users_group_active == true())
3529 if repo_group_id:
3529 if repo_group_id:
3530 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3530 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3531 return q.all()
3531 return q.all()
3532
3532
3533 @classmethod
3533 @classmethod
3534 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3534 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3535 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3535 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3536 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3536 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3537 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3537 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3538 .filter(UserUserGroupToPerm.user_id == user_id)
3538 .filter(UserUserGroupToPerm.user_id == user_id)
3539 if user_group_id:
3539 if user_group_id:
3540 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3540 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3541 return q.all()
3541 return q.all()
3542
3542
3543 @classmethod
3543 @classmethod
3544 def get_default_user_group_perms_from_user_group(
3544 def get_default_user_group_perms_from_user_group(
3545 cls, user_id, user_group_id=None):
3545 cls, user_id, user_group_id=None):
3546 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3546 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3547 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3547 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3548 .join(
3548 .join(
3549 Permission,
3549 Permission,
3550 UserGroupUserGroupToPerm.permission_id ==
3550 UserGroupUserGroupToPerm.permission_id ==
3551 Permission.permission_id)\
3551 Permission.permission_id)\
3552 .join(
3552 .join(
3553 TargetUserGroup,
3553 TargetUserGroup,
3554 UserGroupUserGroupToPerm.target_user_group_id ==
3554 UserGroupUserGroupToPerm.target_user_group_id ==
3555 TargetUserGroup.users_group_id)\
3555 TargetUserGroup.users_group_id)\
3556 .join(
3556 .join(
3557 UserGroup,
3557 UserGroup,
3558 UserGroupUserGroupToPerm.user_group_id ==
3558 UserGroupUserGroupToPerm.user_group_id ==
3559 UserGroup.users_group_id)\
3559 UserGroup.users_group_id)\
3560 .join(
3560 .join(
3561 UserGroupMember,
3561 UserGroupMember,
3562 UserGroupUserGroupToPerm.user_group_id ==
3562 UserGroupUserGroupToPerm.user_group_id ==
3563 UserGroupMember.users_group_id)\
3563 UserGroupMember.users_group_id)\
3564 .filter(
3564 .filter(
3565 UserGroupMember.user_id == user_id,
3565 UserGroupMember.user_id == user_id,
3566 UserGroup.users_group_active == true())
3566 UserGroup.users_group_active == true())
3567 if user_group_id:
3567 if user_group_id:
3568 q = q.filter(
3568 q = q.filter(
3569 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3569 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3570
3570
3571 return q.all()
3571 return q.all()
3572
3572
3573
3573
3574 class UserRepoToPerm(Base, BaseModel):
3574 class UserRepoToPerm(Base, BaseModel):
3575 __tablename__ = 'repo_to_perm'
3575 __tablename__ = 'repo_to_perm'
3576 __table_args__ = (
3576 __table_args__ = (
3577 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3577 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3578 base_table_args
3578 base_table_args
3579 )
3579 )
3580
3580
3581 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3581 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3582 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3582 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3583 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3583 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3584 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3584 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3585
3585
3586 user = relationship('User', back_populates="repo_to_perm")
3586 user = relationship('User', back_populates="repo_to_perm")
3587 repository = relationship('Repository', back_populates="repo_to_perm")
3587 repository = relationship('Repository', back_populates="repo_to_perm")
3588 permission = relationship('Permission')
3588 permission = relationship('Permission')
3589
3589
3590 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3590 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3591
3591
3592 @classmethod
3592 @classmethod
3593 def create(cls, user, repository, permission):
3593 def create(cls, user, repository, permission):
3594 n = cls()
3594 n = cls()
3595 n.user = user
3595 n.user = user
3596 n.repository = repository
3596 n.repository = repository
3597 n.permission = permission
3597 n.permission = permission
3598 Session().add(n)
3598 Session().add(n)
3599 return n
3599 return n
3600
3600
3601 def __repr__(self):
3601 def __repr__(self):
3602 return f'<{self.user} => {self.repository} >'
3602 return f'<{self.user} => {self.repository} >'
3603
3603
3604
3604
3605 class UserUserGroupToPerm(Base, BaseModel):
3605 class UserUserGroupToPerm(Base, BaseModel):
3606 __tablename__ = 'user_user_group_to_perm'
3606 __tablename__ = 'user_user_group_to_perm'
3607 __table_args__ = (
3607 __table_args__ = (
3608 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3608 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3609 base_table_args
3609 base_table_args
3610 )
3610 )
3611
3611
3612 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3612 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3613 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3613 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3616
3616
3617 user = relationship('User', back_populates='user_group_to_perm')
3617 user = relationship('User', back_populates='user_group_to_perm')
3618 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3618 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3619 permission = relationship('Permission')
3619 permission = relationship('Permission')
3620
3620
3621 @classmethod
3621 @classmethod
3622 def create(cls, user, user_group, permission):
3622 def create(cls, user, user_group, permission):
3623 n = cls()
3623 n = cls()
3624 n.user = user
3624 n.user = user
3625 n.user_group = user_group
3625 n.user_group = user_group
3626 n.permission = permission
3626 n.permission = permission
3627 Session().add(n)
3627 Session().add(n)
3628 return n
3628 return n
3629
3629
3630 def __repr__(self):
3630 def __repr__(self):
3631 return f'<{self.user} => {self.user_group} >'
3631 return f'<{self.user} => {self.user_group} >'
3632
3632
3633
3633
3634 class UserToPerm(Base, BaseModel):
3634 class UserToPerm(Base, BaseModel):
3635 __tablename__ = 'user_to_perm'
3635 __tablename__ = 'user_to_perm'
3636 __table_args__ = (
3636 __table_args__ = (
3637 UniqueConstraint('user_id', 'permission_id'),
3637 UniqueConstraint('user_id', 'permission_id'),
3638 base_table_args
3638 base_table_args
3639 )
3639 )
3640
3640
3641 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3641 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3642 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3642 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3644
3644
3645 user = relationship('User', back_populates='user_perms')
3645 user = relationship('User', back_populates='user_perms')
3646 permission = relationship('Permission', lazy='joined')
3646 permission = relationship('Permission', lazy='joined')
3647
3647
3648 def __repr__(self):
3648 def __repr__(self):
3649 return f'<{self.user} => {self.permission} >'
3649 return f'<{self.user} => {self.permission} >'
3650
3650
3651
3651
3652 class UserGroupRepoToPerm(Base, BaseModel):
3652 class UserGroupRepoToPerm(Base, BaseModel):
3653 __tablename__ = 'users_group_repo_to_perm'
3653 __tablename__ = 'users_group_repo_to_perm'
3654 __table_args__ = (
3654 __table_args__ = (
3655 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3655 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3656 base_table_args
3656 base_table_args
3657 )
3657 )
3658
3658
3659 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3659 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3660 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3660 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3661 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3661 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3662 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3662 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3663
3663
3664 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3664 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3665 permission = relationship('Permission')
3665 permission = relationship('Permission')
3666 repository = relationship('Repository', back_populates='users_group_to_perm')
3666 repository = relationship('Repository', back_populates='users_group_to_perm')
3667 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3667 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3668
3668
3669 @classmethod
3669 @classmethod
3670 def create(cls, users_group, repository, permission):
3670 def create(cls, users_group, repository, permission):
3671 n = cls()
3671 n = cls()
3672 n.users_group = users_group
3672 n.users_group = users_group
3673 n.repository = repository
3673 n.repository = repository
3674 n.permission = permission
3674 n.permission = permission
3675 Session().add(n)
3675 Session().add(n)
3676 return n
3676 return n
3677
3677
3678 def __repr__(self):
3678 def __repr__(self):
3679 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3679 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3680
3680
3681
3681
3682 class UserGroupUserGroupToPerm(Base, BaseModel):
3682 class UserGroupUserGroupToPerm(Base, BaseModel):
3683 __tablename__ = 'user_group_user_group_to_perm'
3683 __tablename__ = 'user_group_user_group_to_perm'
3684 __table_args__ = (
3684 __table_args__ = (
3685 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3685 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3686 CheckConstraint('target_user_group_id != user_group_id'),
3686 CheckConstraint('target_user_group_id != user_group_id'),
3687 base_table_args
3687 base_table_args
3688 )
3688 )
3689
3689
3690 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)
3690 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)
3691 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3691 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3692 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3692 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3693 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3693 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3694
3694
3695 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3695 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3696 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3696 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3697 permission = relationship('Permission')
3697 permission = relationship('Permission')
3698
3698
3699 @classmethod
3699 @classmethod
3700 def create(cls, target_user_group, user_group, permission):
3700 def create(cls, target_user_group, user_group, permission):
3701 n = cls()
3701 n = cls()
3702 n.target_user_group = target_user_group
3702 n.target_user_group = target_user_group
3703 n.user_group = user_group
3703 n.user_group = user_group
3704 n.permission = permission
3704 n.permission = permission
3705 Session().add(n)
3705 Session().add(n)
3706 return n
3706 return n
3707
3707
3708 def __repr__(self):
3708 def __repr__(self):
3709 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3709 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3710
3710
3711
3711
3712 class UserGroupToPerm(Base, BaseModel):
3712 class UserGroupToPerm(Base, BaseModel):
3713 __tablename__ = 'users_group_to_perm'
3713 __tablename__ = 'users_group_to_perm'
3714 __table_args__ = (
3714 __table_args__ = (
3715 UniqueConstraint('users_group_id', 'permission_id',),
3715 UniqueConstraint('users_group_id', 'permission_id',),
3716 base_table_args
3716 base_table_args
3717 )
3717 )
3718
3718
3719 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3719 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3720 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3720 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3721 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3721 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3722
3722
3723 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3723 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3724 permission = relationship('Permission')
3724 permission = relationship('Permission')
3725
3725
3726
3726
3727 class UserRepoGroupToPerm(Base, BaseModel):
3727 class UserRepoGroupToPerm(Base, BaseModel):
3728 __tablename__ = 'user_repo_group_to_perm'
3728 __tablename__ = 'user_repo_group_to_perm'
3729 __table_args__ = (
3729 __table_args__ = (
3730 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3730 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3731 base_table_args
3731 base_table_args
3732 )
3732 )
3733
3733
3734 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3734 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3735 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3735 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3736 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3736 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3737 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3737 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3738
3738
3739 user = relationship('User', back_populates='repo_group_to_perm')
3739 user = relationship('User', back_populates='repo_group_to_perm')
3740 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3740 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3741 permission = relationship('Permission')
3741 permission = relationship('Permission')
3742
3742
3743 @classmethod
3743 @classmethod
3744 def create(cls, user, repository_group, permission):
3744 def create(cls, user, repository_group, permission):
3745 n = cls()
3745 n = cls()
3746 n.user = user
3746 n.user = user
3747 n.group = repository_group
3747 n.group = repository_group
3748 n.permission = permission
3748 n.permission = permission
3749 Session().add(n)
3749 Session().add(n)
3750 return n
3750 return n
3751
3751
3752
3752
3753 class UserGroupRepoGroupToPerm(Base, BaseModel):
3753 class UserGroupRepoGroupToPerm(Base, BaseModel):
3754 __tablename__ = 'users_group_repo_group_to_perm'
3754 __tablename__ = 'users_group_repo_group_to_perm'
3755 __table_args__ = (
3755 __table_args__ = (
3756 UniqueConstraint('users_group_id', 'group_id'),
3756 UniqueConstraint('users_group_id', 'group_id'),
3757 base_table_args
3757 base_table_args
3758 )
3758 )
3759
3759
3760 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)
3760 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)
3761 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3761 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3762 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3762 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3763 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3763 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3764
3764
3765 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3765 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3766 permission = relationship('Permission')
3766 permission = relationship('Permission')
3767 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3767 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3768
3768
3769 @classmethod
3769 @classmethod
3770 def create(cls, user_group, repository_group, permission):
3770 def create(cls, user_group, repository_group, permission):
3771 n = cls()
3771 n = cls()
3772 n.users_group = user_group
3772 n.users_group = user_group
3773 n.group = repository_group
3773 n.group = repository_group
3774 n.permission = permission
3774 n.permission = permission
3775 Session().add(n)
3775 Session().add(n)
3776 return n
3776 return n
3777
3777
3778 def __repr__(self):
3778 def __repr__(self):
3779 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3779 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3780
3780
3781
3781
3782 class Statistics(Base, BaseModel):
3782 class Statistics(Base, BaseModel):
3783 __tablename__ = 'statistics'
3783 __tablename__ = 'statistics'
3784 __table_args__ = (
3784 __table_args__ = (
3785 base_table_args
3785 base_table_args
3786 )
3786 )
3787
3787
3788 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3788 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3789 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3789 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3790 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3790 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3791 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3791 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3792 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3792 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3793 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3793 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3794
3794
3795 repository = relationship('Repository', single_parent=True, viewonly=True)
3795 repository = relationship('Repository', single_parent=True, viewonly=True)
3796
3796
3797
3797
3798 class UserFollowing(Base, BaseModel):
3798 class UserFollowing(Base, BaseModel):
3799 __tablename__ = 'user_followings'
3799 __tablename__ = 'user_followings'
3800 __table_args__ = (
3800 __table_args__ = (
3801 UniqueConstraint('user_id', 'follows_repository_id'),
3801 UniqueConstraint('user_id', 'follows_repository_id'),
3802 UniqueConstraint('user_id', 'follows_user_id'),
3802 UniqueConstraint('user_id', 'follows_user_id'),
3803 base_table_args
3803 base_table_args
3804 )
3804 )
3805
3805
3806 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3806 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3807 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3807 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3808 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3808 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3809 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3809 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3810 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3810 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3811
3811
3812 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3812 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3813
3813
3814 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3814 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3815 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3815 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3816
3816
3817 @classmethod
3817 @classmethod
3818 def get_repo_followers(cls, repo_id):
3818 def get_repo_followers(cls, repo_id):
3819 return cls.query().filter(cls.follows_repo_id == repo_id)
3819 return cls.query().filter(cls.follows_repo_id == repo_id)
3820
3820
3821
3821
3822 class CacheKey(Base, BaseModel):
3822 class CacheKey(Base, BaseModel):
3823 __tablename__ = 'cache_invalidation'
3823 __tablename__ = 'cache_invalidation'
3824 __table_args__ = (
3824 __table_args__ = (
3825 UniqueConstraint('cache_key'),
3825 UniqueConstraint('cache_key'),
3826 Index('key_idx', 'cache_key'),
3826 Index('key_idx', 'cache_key'),
3827 Index('cache_args_idx', 'cache_args'),
3827 Index('cache_args_idx', 'cache_args'),
3828 base_table_args,
3828 base_table_args,
3829 )
3829 )
3830
3830
3831 CACHE_TYPE_FEED = 'FEED'
3831 CACHE_TYPE_FEED = 'FEED'
3832
3832
3833 # namespaces used to register process/thread aware caches
3833 # namespaces used to register process/thread aware caches
3834 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3834 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3835
3835
3836 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3836 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3837 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3837 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3838 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3838 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3839 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3839 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3840 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3840 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3841
3841
3842 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3842 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3843 self.cache_key = cache_key
3843 self.cache_key = cache_key
3844 self.cache_args = cache_args
3844 self.cache_args = cache_args
3845 self.cache_active = cache_active
3845 self.cache_active = cache_active
3846 # first key should be same for all entries, since all workers should share it
3846 # first key should be same for all entries, since all workers should share it
3847 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3847 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3848
3848
3849 def __repr__(self):
3849 def __repr__(self):
3850 return "<%s('%s:%s[%s]')>" % (
3850 return "<%s('%s:%s[%s]')>" % (
3851 self.cls_name,
3851 self.cls_name,
3852 self.cache_id, self.cache_key, self.cache_active)
3852 self.cache_id, self.cache_key, self.cache_active)
3853
3853
3854 def _cache_key_partition(self):
3854 def _cache_key_partition(self):
3855 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3855 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3856 return prefix, repo_name, suffix
3856 return prefix, repo_name, suffix
3857
3857
3858 def get_prefix(self):
3858 def get_prefix(self):
3859 """
3859 """
3860 Try to extract prefix from existing cache key. The key could consist
3860 Try to extract prefix from existing cache key. The key could consist
3861 of prefix, repo_name, suffix
3861 of prefix, repo_name, suffix
3862 """
3862 """
3863 # this returns prefix, repo_name, suffix
3863 # this returns prefix, repo_name, suffix
3864 return self._cache_key_partition()[0]
3864 return self._cache_key_partition()[0]
3865
3865
3866 def get_suffix(self):
3866 def get_suffix(self):
3867 """
3867 """
3868 get suffix that might have been used in _get_cache_key to
3868 get suffix that might have been used in _get_cache_key to
3869 generate self.cache_key. Only used for informational purposes
3869 generate self.cache_key. Only used for informational purposes
3870 in repo_edit.mako.
3870 in repo_edit.mako.
3871 """
3871 """
3872 # prefix, repo_name, suffix
3872 # prefix, repo_name, suffix
3873 return self._cache_key_partition()[2]
3873 return self._cache_key_partition()[2]
3874
3874
3875 @classmethod
3875 @classmethod
3876 def generate_new_state_uid(cls, based_on=None):
3876 def generate_new_state_uid(cls, based_on=None):
3877 if based_on:
3877 if based_on:
3878 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3878 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3879 else:
3879 else:
3880 return str(uuid.uuid4())
3880 return str(uuid.uuid4())
3881
3881
3882 @classmethod
3882 @classmethod
3883 def delete_all_cache(cls):
3883 def delete_all_cache(cls):
3884 """
3884 """
3885 Delete all cache keys from database.
3885 Delete all cache keys from database.
3886 Should only be run when all instances are down and all entries
3886 Should only be run when all instances are down and all entries
3887 thus stale.
3887 thus stale.
3888 """
3888 """
3889 cls.query().delete()
3889 cls.query().delete()
3890 Session().commit()
3890 Session().commit()
3891
3891
3892 @classmethod
3892 @classmethod
3893 def set_invalidate(cls, cache_uid, delete=False):
3893 def set_invalidate(cls, cache_uid, delete=False):
3894 """
3894 """
3895 Mark all caches of a repo as invalid in the database.
3895 Mark all caches of a repo as invalid in the database.
3896 """
3896 """
3897 try:
3897 try:
3898 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3898 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3899 if delete:
3899 if delete:
3900 qry.delete()
3900 qry.delete()
3901 log.debug('cache objects deleted for cache args %s',
3901 log.debug('cache objects deleted for cache args %s',
3902 safe_str(cache_uid))
3902 safe_str(cache_uid))
3903 else:
3903 else:
3904 new_uid = cls.generate_new_state_uid()
3904 new_uid = cls.generate_new_state_uid()
3905 qry.update({"cache_state_uid": new_uid,
3905 qry.update({"cache_state_uid": new_uid,
3906 "cache_args": f"repo_state:{time.time()}"})
3906 "cache_args": f"repo_state:{time.time()}"})
3907 log.debug('cache object %s set new UID %s',
3907 log.debug('cache object %s set new UID %s',
3908 safe_str(cache_uid), new_uid)
3908 safe_str(cache_uid), new_uid)
3909
3909
3910 Session().commit()
3910 Session().commit()
3911 except Exception:
3911 except Exception:
3912 log.exception(
3912 log.exception(
3913 'Cache key invalidation failed for cache args %s',
3913 'Cache key invalidation failed for cache args %s',
3914 safe_str(cache_uid))
3914 safe_str(cache_uid))
3915 Session().rollback()
3915 Session().rollback()
3916
3916
3917 @classmethod
3917 @classmethod
3918 def get_active_cache(cls, cache_key):
3918 def get_active_cache(cls, cache_key):
3919 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3919 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3920 if inv_obj:
3920 if inv_obj:
3921 return inv_obj
3921 return inv_obj
3922 return None
3922 return None
3923
3923
3924 @classmethod
3924 @classmethod
3925 def get_namespace_map(cls, namespace):
3925 def get_namespace_map(cls, namespace):
3926 return {
3926 return {
3927 x.cache_key: x
3927 x.cache_key: x
3928 for x in cls.query().filter(cls.cache_args == namespace)}
3928 for x in cls.query().filter(cls.cache_args == namespace)}
3929
3929
3930
3930
3931 class ChangesetComment(Base, BaseModel):
3931 class ChangesetComment(Base, BaseModel):
3932 __tablename__ = 'changeset_comments'
3932 __tablename__ = 'changeset_comments'
3933 __table_args__ = (
3933 __table_args__ = (
3934 Index('cc_revision_idx', 'revision'),
3934 Index('cc_revision_idx', 'revision'),
3935 base_table_args,
3935 base_table_args,
3936 )
3936 )
3937
3937
3938 COMMENT_OUTDATED = 'comment_outdated'
3938 COMMENT_OUTDATED = 'comment_outdated'
3939 COMMENT_TYPE_NOTE = 'note'
3939 COMMENT_TYPE_NOTE = 'note'
3940 COMMENT_TYPE_TODO = 'todo'
3940 COMMENT_TYPE_TODO = 'todo'
3941 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3941 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3942
3942
3943 OP_IMMUTABLE = 'immutable'
3943 OP_IMMUTABLE = 'immutable'
3944 OP_CHANGEABLE = 'changeable'
3944 OP_CHANGEABLE = 'changeable'
3945
3945
3946 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3946 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3947 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3947 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3948 revision = Column('revision', String(40), nullable=True)
3948 revision = Column('revision', String(40), nullable=True)
3949 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3949 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3950 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3950 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3951 line_no = Column('line_no', Unicode(10), nullable=True)
3951 line_no = Column('line_no', Unicode(10), nullable=True)
3952 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3952 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3953 f_path = Column('f_path', Unicode(1000), nullable=True)
3953 f_path = Column('f_path', Unicode(1000), nullable=True)
3954 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3954 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3955 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3955 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3956 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3956 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3957 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3957 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3958 renderer = Column('renderer', Unicode(64), nullable=True)
3958 renderer = Column('renderer', Unicode(64), nullable=True)
3959 display_state = Column('display_state', Unicode(128), nullable=True)
3959 display_state = Column('display_state', Unicode(128), nullable=True)
3960 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3960 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3961 draft = Column('draft', Boolean(), nullable=True, default=False)
3961 draft = Column('draft', Boolean(), nullable=True, default=False)
3962
3962
3963 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3963 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3964 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3964 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3965
3965
3966 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3966 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3967 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3967 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3968
3968
3969 author = relationship('User', lazy='select', back_populates='user_comments')
3969 author = relationship('User', lazy='select', back_populates='user_comments')
3970 repo = relationship('Repository', back_populates='comments')
3970 repo = relationship('Repository', back_populates='comments')
3971 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3971 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3972 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3972 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3973 pull_request_version = relationship('PullRequestVersion', lazy='select')
3973 pull_request_version = relationship('PullRequestVersion', lazy='select')
3974 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3974 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3975
3975
3976 @classmethod
3976 @classmethod
3977 def get_users(cls, revision=None, pull_request_id=None):
3977 def get_users(cls, revision=None, pull_request_id=None):
3978 """
3978 """
3979 Returns user associated with this ChangesetComment. ie those
3979 Returns user associated with this ChangesetComment. ie those
3980 who actually commented
3980 who actually commented
3981
3981
3982 :param cls:
3982 :param cls:
3983 :param revision:
3983 :param revision:
3984 """
3984 """
3985 q = Session().query(User).join(ChangesetComment.author)
3985 q = Session().query(User).join(ChangesetComment.author)
3986 if revision:
3986 if revision:
3987 q = q.filter(cls.revision == revision)
3987 q = q.filter(cls.revision == revision)
3988 elif pull_request_id:
3988 elif pull_request_id:
3989 q = q.filter(cls.pull_request_id == pull_request_id)
3989 q = q.filter(cls.pull_request_id == pull_request_id)
3990 return q.all()
3990 return q.all()
3991
3991
3992 @classmethod
3992 @classmethod
3993 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3993 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3994 if pr_version is None:
3994 if pr_version is None:
3995 return 0
3995 return 0
3996
3996
3997 if versions is not None:
3997 if versions is not None:
3998 num_versions = [x.pull_request_version_id for x in versions]
3998 num_versions = [x.pull_request_version_id for x in versions]
3999
3999
4000 num_versions = num_versions or []
4000 num_versions = num_versions or []
4001 try:
4001 try:
4002 return num_versions.index(pr_version) + 1
4002 return num_versions.index(pr_version) + 1
4003 except (IndexError, ValueError):
4003 except (IndexError, ValueError):
4004 return 0
4004 return 0
4005
4005
4006 @property
4006 @property
4007 def outdated(self):
4007 def outdated(self):
4008 return self.display_state == self.COMMENT_OUTDATED
4008 return self.display_state == self.COMMENT_OUTDATED
4009
4009
4010 @property
4010 @property
4011 def outdated_js(self):
4011 def outdated_js(self):
4012 return str_json(self.display_state == self.COMMENT_OUTDATED)
4012 return str_json(self.display_state == self.COMMENT_OUTDATED)
4013
4013
4014 @property
4014 @property
4015 def immutable(self):
4015 def immutable(self):
4016 return self.immutable_state == self.OP_IMMUTABLE
4016 return self.immutable_state == self.OP_IMMUTABLE
4017
4017
4018 def outdated_at_version(self, version: int) -> bool:
4018 def outdated_at_version(self, version: int) -> bool:
4019 """
4019 """
4020 Checks if comment is outdated for given pull request version
4020 Checks if comment is outdated for given pull request version
4021 """
4021 """
4022
4022
4023 def version_check():
4023 def version_check():
4024 return self.pull_request_version_id and self.pull_request_version_id != version
4024 return self.pull_request_version_id and self.pull_request_version_id != version
4025
4025
4026 if self.is_inline:
4026 if self.is_inline:
4027 return self.outdated and version_check()
4027 return self.outdated and version_check()
4028 else:
4028 else:
4029 # general comments don't have .outdated set, also latest don't have a version
4029 # general comments don't have .outdated set, also latest don't have a version
4030 return version_check()
4030 return version_check()
4031
4031
4032 def outdated_at_version_js(self, version):
4032 def outdated_at_version_js(self, version):
4033 """
4033 """
4034 Checks if comment is outdated for given pull request version
4034 Checks if comment is outdated for given pull request version
4035 """
4035 """
4036 return str_json(self.outdated_at_version(version))
4036 return str_json(self.outdated_at_version(version))
4037
4037
4038 def older_than_version(self, version: int) -> bool:
4038 def older_than_version(self, version: int) -> bool:
4039 """
4039 """
4040 Checks if comment is made from a previous version than given.
4040 Checks if comment is made from a previous version than given.
4041 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4041 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4042 """
4042 """
4043
4043
4044 # If version is None, return False as the current version cannot be less than None
4044 # If version is None, return False as the current version cannot be less than None
4045 if version is None:
4045 if version is None:
4046 return False
4046 return False
4047
4047
4048 # Ensure that the version is an integer to prevent TypeError on comparison
4048 # Ensure that the version is an integer to prevent TypeError on comparison
4049 if not isinstance(version, int):
4049 if not isinstance(version, int):
4050 raise ValueError("The provided version must be an integer.")
4050 raise ValueError("The provided version must be an integer.")
4051
4051
4052 # Initialize current version to 0 or pull_request_version_id if it's available
4052 # Initialize current version to 0 or pull_request_version_id if it's available
4053 cur_ver = 0
4053 cur_ver = 0
4054 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4054 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4055 cur_ver = self.pull_request_version.pull_request_version_id
4055 cur_ver = self.pull_request_version.pull_request_version_id
4056
4056
4057 # Return True if the current version is less than the given version
4057 # Return True if the current version is less than the given version
4058 return cur_ver < version
4058 return cur_ver < version
4059
4059
4060 def older_than_version_js(self, version):
4060 def older_than_version_js(self, version):
4061 """
4061 """
4062 Checks if comment is made from previous version than given
4062 Checks if comment is made from previous version than given
4063 """
4063 """
4064 return str_json(self.older_than_version(version))
4064 return str_json(self.older_than_version(version))
4065
4065
4066 @property
4066 @property
4067 def commit_id(self):
4067 def commit_id(self):
4068 """New style naming to stop using .revision"""
4068 """New style naming to stop using .revision"""
4069 return self.revision
4069 return self.revision
4070
4070
4071 @property
4071 @property
4072 def resolved(self):
4072 def resolved(self):
4073 return self.resolved_by[0] if self.resolved_by else None
4073 return self.resolved_by[0] if self.resolved_by else None
4074
4074
4075 @property
4075 @property
4076 def is_todo(self):
4076 def is_todo(self):
4077 return self.comment_type == self.COMMENT_TYPE_TODO
4077 return self.comment_type == self.COMMENT_TYPE_TODO
4078
4078
4079 @property
4079 @property
4080 def is_inline(self):
4080 def is_inline(self):
4081 if self.line_no and self.f_path:
4081 if self.line_no and self.f_path:
4082 return True
4082 return True
4083 return False
4083 return False
4084
4084
4085 @property
4085 @property
4086 def last_version(self):
4086 def last_version(self):
4087 version = 0
4087 version = 0
4088 if self.history:
4088 if self.history:
4089 version = self.history[-1].version
4089 version = self.history[-1].version
4090 return version
4090 return version
4091
4091
4092 def get_index_version(self, versions):
4092 def get_index_version(self, versions):
4093 return self.get_index_from_version(
4093 return self.get_index_from_version(
4094 self.pull_request_version_id, versions)
4094 self.pull_request_version_id, versions)
4095
4095
4096 @property
4096 @property
4097 def review_status(self):
4097 def review_status(self):
4098 if self.status_change:
4098 if self.status_change:
4099 return self.status_change[0].status
4099 return self.status_change[0].status
4100
4100
4101 @property
4101 @property
4102 def review_status_lbl(self):
4102 def review_status_lbl(self):
4103 if self.status_change:
4103 if self.status_change:
4104 return self.status_change[0].status_lbl
4104 return self.status_change[0].status_lbl
4105
4105
4106 def __repr__(self):
4106 def __repr__(self):
4107 if self.comment_id:
4107 if self.comment_id:
4108 return f'<DB:Comment #{self.comment_id}>'
4108 return f'<DB:Comment #{self.comment_id}>'
4109 else:
4109 else:
4110 return f'<DB:Comment at {id(self)!r}>'
4110 return f'<DB:Comment at {id(self)!r}>'
4111
4111
4112 def get_api_data(self):
4112 def get_api_data(self):
4113 comment = self
4113 comment = self
4114
4114
4115 data = {
4115 data = {
4116 'comment_id': comment.comment_id,
4116 'comment_id': comment.comment_id,
4117 'comment_type': comment.comment_type,
4117 'comment_type': comment.comment_type,
4118 'comment_text': comment.text,
4118 'comment_text': comment.text,
4119 'comment_status': comment.status_change,
4119 'comment_status': comment.status_change,
4120 'comment_f_path': comment.f_path,
4120 'comment_f_path': comment.f_path,
4121 'comment_lineno': comment.line_no,
4121 'comment_lineno': comment.line_no,
4122 'comment_author': comment.author,
4122 'comment_author': comment.author,
4123 'comment_created_on': comment.created_on,
4123 'comment_created_on': comment.created_on,
4124 'comment_resolved_by': self.resolved,
4124 'comment_resolved_by': self.resolved,
4125 'comment_commit_id': comment.revision,
4125 'comment_commit_id': comment.revision,
4126 'comment_pull_request_id': comment.pull_request_id,
4126 'comment_pull_request_id': comment.pull_request_id,
4127 'comment_last_version': self.last_version
4127 'comment_last_version': self.last_version
4128 }
4128 }
4129 return data
4129 return data
4130
4130
4131 def __json__(self):
4131 def __json__(self):
4132 data = dict()
4132 data = dict()
4133 data.update(self.get_api_data())
4133 data.update(self.get_api_data())
4134 return data
4134 return data
4135
4135
4136
4136
4137 class ChangesetCommentHistory(Base, BaseModel):
4137 class ChangesetCommentHistory(Base, BaseModel):
4138 __tablename__ = 'changeset_comments_history'
4138 __tablename__ = 'changeset_comments_history'
4139 __table_args__ = (
4139 __table_args__ = (
4140 Index('cch_comment_id_idx', 'comment_id'),
4140 Index('cch_comment_id_idx', 'comment_id'),
4141 base_table_args,
4141 base_table_args,
4142 )
4142 )
4143
4143
4144 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4144 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4145 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4145 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4146 version = Column("version", Integer(), nullable=False, default=0)
4146 version = Column("version", Integer(), nullable=False, default=0)
4147 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4147 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4148 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4148 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4149 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4149 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4150 deleted = Column('deleted', Boolean(), default=False)
4150 deleted = Column('deleted', Boolean(), default=False)
4151
4151
4152 author = relationship('User', lazy='joined')
4152 author = relationship('User', lazy='joined')
4153 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4153 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4154
4154
4155 @classmethod
4155 @classmethod
4156 def get_version(cls, comment_id):
4156 def get_version(cls, comment_id):
4157 q = Session().query(ChangesetCommentHistory).filter(
4157 q = Session().query(ChangesetCommentHistory).filter(
4158 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4158 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4159 if q.count() == 0:
4159 if q.count() == 0:
4160 return 1
4160 return 1
4161 elif q.count() >= q[0].version:
4161 elif q.count() >= q[0].version:
4162 return q.count() + 1
4162 return q.count() + 1
4163 else:
4163 else:
4164 return q[0].version + 1
4164 return q[0].version + 1
4165
4165
4166
4166
4167 class ChangesetStatus(Base, BaseModel):
4167 class ChangesetStatus(Base, BaseModel):
4168 __tablename__ = 'changeset_statuses'
4168 __tablename__ = 'changeset_statuses'
4169 __table_args__ = (
4169 __table_args__ = (
4170 Index('cs_revision_idx', 'revision'),
4170 Index('cs_revision_idx', 'revision'),
4171 Index('cs_version_idx', 'version'),
4171 Index('cs_version_idx', 'version'),
4172 UniqueConstraint('repo_id', 'revision', 'version'),
4172 UniqueConstraint('repo_id', 'revision', 'version'),
4173 base_table_args
4173 base_table_args
4174 )
4174 )
4175
4175
4176 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4176 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4177 STATUS_APPROVED = 'approved'
4177 STATUS_APPROVED = 'approved'
4178 STATUS_REJECTED = 'rejected'
4178 STATUS_REJECTED = 'rejected'
4179 STATUS_UNDER_REVIEW = 'under_review'
4179 STATUS_UNDER_REVIEW = 'under_review'
4180
4180
4181 STATUSES = [
4181 STATUSES = [
4182 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4182 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4183 (STATUS_APPROVED, _("Approved")),
4183 (STATUS_APPROVED, _("Approved")),
4184 (STATUS_REJECTED, _("Rejected")),
4184 (STATUS_REJECTED, _("Rejected")),
4185 (STATUS_UNDER_REVIEW, _("Under Review")),
4185 (STATUS_UNDER_REVIEW, _("Under Review")),
4186 ]
4186 ]
4187
4187
4188 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4188 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4191 revision = Column('revision', String(40), nullable=False)
4191 revision = Column('revision', String(40), nullable=False)
4192 status = Column('status', String(128), nullable=False, default=DEFAULT)
4192 status = Column('status', String(128), nullable=False, default=DEFAULT)
4193 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4193 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4194 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4194 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4195 version = Column('version', Integer(), nullable=False, default=0)
4195 version = Column('version', Integer(), nullable=False, default=0)
4196 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4196 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4197
4197
4198 author = relationship('User', lazy='select')
4198 author = relationship('User', lazy='select')
4199 repo = relationship('Repository', lazy='select')
4199 repo = relationship('Repository', lazy='select')
4200 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4200 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4201 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4201 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4202
4202
4203 def __repr__(self):
4203 def __repr__(self):
4204 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4204 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4205
4205
4206 @classmethod
4206 @classmethod
4207 def get_status_lbl(cls, value):
4207 def get_status_lbl(cls, value):
4208 return dict(cls.STATUSES).get(value)
4208 return dict(cls.STATUSES).get(value)
4209
4209
4210 @property
4210 @property
4211 def status_lbl(self):
4211 def status_lbl(self):
4212 return ChangesetStatus.get_status_lbl(self.status)
4212 return ChangesetStatus.get_status_lbl(self.status)
4213
4213
4214 def get_api_data(self):
4214 def get_api_data(self):
4215 status = self
4215 status = self
4216 data = {
4216 data = {
4217 'status_id': status.changeset_status_id,
4217 'status_id': status.changeset_status_id,
4218 'status': status.status,
4218 'status': status.status,
4219 }
4219 }
4220 return data
4220 return data
4221
4221
4222 def __json__(self):
4222 def __json__(self):
4223 data = dict()
4223 data = dict()
4224 data.update(self.get_api_data())
4224 data.update(self.get_api_data())
4225 return data
4225 return data
4226
4226
4227
4227
4228 class _SetState(object):
4228 class _SetState(object):
4229 """
4229 """
4230 Context processor allowing changing state for sensitive operation such as
4230 Context processor allowing changing state for sensitive operation such as
4231 pull request update or merge
4231 pull request update or merge
4232 """
4232 """
4233
4233
4234 def __init__(self, pull_request, pr_state, back_state=None):
4234 def __init__(self, pull_request, pr_state, back_state=None):
4235 self._pr = pull_request
4235 self._pr = pull_request
4236 self._org_state = back_state or pull_request.pull_request_state
4236 self._org_state = back_state or pull_request.pull_request_state
4237 self._pr_state = pr_state
4237 self._pr_state = pr_state
4238 self._current_state = None
4238 self._current_state = None
4239
4239
4240 def __enter__(self):
4240 def __enter__(self):
4241 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4241 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4242 self._pr, self._pr_state)
4242 self._pr, self._pr_state)
4243 self.set_pr_state(self._pr_state)
4243 self.set_pr_state(self._pr_state)
4244 return self
4244 return self
4245
4245
4246 def __exit__(self, exc_type, exc_val, exc_tb):
4246 def __exit__(self, exc_type, exc_val, exc_tb):
4247 if exc_val is not None or exc_type is not None:
4247 if exc_val is not None or exc_type is not None:
4248 log.error(traceback.format_tb(exc_tb))
4248 log.error(traceback.format_tb(exc_tb))
4249 return None
4249 return None
4250
4250
4251 self.set_pr_state(self._org_state)
4251 self.set_pr_state(self._org_state)
4252 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4252 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4253 self._pr, self._org_state)
4253 self._pr, self._org_state)
4254
4254
4255 @property
4255 @property
4256 def state(self):
4256 def state(self):
4257 return self._current_state
4257 return self._current_state
4258
4258
4259 def set_pr_state(self, pr_state):
4259 def set_pr_state(self, pr_state):
4260 try:
4260 try:
4261 self._pr.pull_request_state = pr_state
4261 self._pr.pull_request_state = pr_state
4262 Session().add(self._pr)
4262 Session().add(self._pr)
4263 Session().commit()
4263 Session().commit()
4264 self._current_state = pr_state
4264 self._current_state = pr_state
4265 except Exception:
4265 except Exception:
4266 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4266 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4267 raise
4267 raise
4268
4268
4269
4269
4270 class _PullRequestBase(BaseModel):
4270 class _PullRequestBase(BaseModel):
4271 """
4271 """
4272 Common attributes of pull request and version entries.
4272 Common attributes of pull request and version entries.
4273 """
4273 """
4274
4274
4275 # .status values
4275 # .status values
4276 STATUS_NEW = 'new'
4276 STATUS_NEW = 'new'
4277 STATUS_OPEN = 'open'
4277 STATUS_OPEN = 'open'
4278 STATUS_CLOSED = 'closed'
4278 STATUS_CLOSED = 'closed'
4279
4279
4280 # available states
4280 # available states
4281 STATE_CREATING = 'creating'
4281 STATE_CREATING = 'creating'
4282 STATE_UPDATING = 'updating'
4282 STATE_UPDATING = 'updating'
4283 STATE_MERGING = 'merging'
4283 STATE_MERGING = 'merging'
4284 STATE_CREATED = 'created'
4284 STATE_CREATED = 'created'
4285
4285
4286 title = Column('title', Unicode(255), nullable=True)
4286 title = Column('title', Unicode(255), nullable=True)
4287 description = Column(
4287 description = Column(
4288 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4288 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4289 nullable=True)
4289 nullable=True)
4290 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4290 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4291
4291
4292 # new/open/closed status of pull request (not approve/reject/etc)
4292 # new/open/closed status of pull request (not approve/reject/etc)
4293 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4293 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4294 created_on = Column(
4294 created_on = Column(
4295 'created_on', DateTime(timezone=False), nullable=False,
4295 'created_on', DateTime(timezone=False), nullable=False,
4296 default=datetime.datetime.now)
4296 default=datetime.datetime.now)
4297 updated_on = Column(
4297 updated_on = Column(
4298 'updated_on', DateTime(timezone=False), nullable=False,
4298 'updated_on', DateTime(timezone=False), nullable=False,
4299 default=datetime.datetime.now)
4299 default=datetime.datetime.now)
4300
4300
4301 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4301 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4302
4302
4303 @declared_attr
4303 @declared_attr
4304 def user_id(cls):
4304 def user_id(cls):
4305 return Column(
4305 return Column(
4306 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4306 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4307 unique=None)
4307 unique=None)
4308
4308
4309 # 500 revisions max
4309 # 500 revisions max
4310 _revisions = Column(
4310 _revisions = Column(
4311 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4311 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4312
4312
4313 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4313 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4314
4314
4315 @declared_attr
4315 @declared_attr
4316 def source_repo_id(cls):
4316 def source_repo_id(cls):
4317 # TODO: dan: rename column to source_repo_id
4317 # TODO: dan: rename column to source_repo_id
4318 return Column(
4318 return Column(
4319 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4319 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4320 nullable=False)
4320 nullable=False)
4321
4321
4322 @declared_attr
4322 @declared_attr
4323 def pr_source(cls):
4323 def pr_source(cls):
4324 return relationship(
4324 return relationship(
4325 'Repository',
4325 'Repository',
4326 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4326 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4327 overlaps="pull_requests_source"
4327 overlaps="pull_requests_source"
4328 )
4328 )
4329
4329
4330 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4330 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4331
4331
4332 @hybrid_property
4332 @hybrid_property
4333 def source_ref(self):
4333 def source_ref(self):
4334 return self._source_ref
4334 return self._source_ref
4335
4335
4336 @source_ref.setter
4336 @source_ref.setter
4337 def source_ref(self, val):
4337 def source_ref(self, val):
4338 parts = (val or '').split(':')
4338 parts = (val or '').split(':')
4339 if len(parts) != 3:
4339 if len(parts) != 3:
4340 raise ValueError(
4340 raise ValueError(
4341 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4341 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4342 self._source_ref = safe_str(val)
4342 self._source_ref = safe_str(val)
4343
4343
4344 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4344 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4345
4345
4346 @hybrid_property
4346 @hybrid_property
4347 def target_ref(self):
4347 def target_ref(self):
4348 return self._target_ref
4348 return self._target_ref
4349
4349
4350 @target_ref.setter
4350 @target_ref.setter
4351 def target_ref(self, val):
4351 def target_ref(self, val):
4352 parts = (val or '').split(':')
4352 parts = (val or '').split(':')
4353 if len(parts) != 3:
4353 if len(parts) != 3:
4354 raise ValueError(
4354 raise ValueError(
4355 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4355 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4356 self._target_ref = safe_str(val)
4356 self._target_ref = safe_str(val)
4357
4357
4358 @declared_attr
4358 @declared_attr
4359 def target_repo_id(cls):
4359 def target_repo_id(cls):
4360 # TODO: dan: rename column to target_repo_id
4360 # TODO: dan: rename column to target_repo_id
4361 return Column(
4361 return Column(
4362 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4362 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4363 nullable=False)
4363 nullable=False)
4364
4364
4365 @declared_attr
4365 @declared_attr
4366 def pr_target(cls):
4366 def pr_target(cls):
4367 return relationship(
4367 return relationship(
4368 'Repository',
4368 'Repository',
4369 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4369 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4370 overlaps="pull_requests_target"
4370 overlaps="pull_requests_target"
4371 )
4371 )
4372
4372
4373 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4373 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4374
4374
4375 # TODO: dan: rename column to last_merge_source_rev
4375 # TODO: dan: rename column to last_merge_source_rev
4376 _last_merge_source_rev = Column(
4376 _last_merge_source_rev = Column(
4377 'last_merge_org_rev', String(40), nullable=True)
4377 'last_merge_org_rev', String(40), nullable=True)
4378 # TODO: dan: rename column to last_merge_target_rev
4378 # TODO: dan: rename column to last_merge_target_rev
4379 _last_merge_target_rev = Column(
4379 _last_merge_target_rev = Column(
4380 'last_merge_other_rev', String(40), nullable=True)
4380 'last_merge_other_rev', String(40), nullable=True)
4381 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4381 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4382 last_merge_metadata = Column(
4382 last_merge_metadata = Column(
4383 'last_merge_metadata', MutationObj.as_mutable(
4383 'last_merge_metadata', MutationObj.as_mutable(
4384 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4384 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4385
4385
4386 merge_rev = Column('merge_rev', String(40), nullable=True)
4386 merge_rev = Column('merge_rev', String(40), nullable=True)
4387
4387
4388 reviewer_data = Column(
4388 reviewer_data = Column(
4389 'reviewer_data_json', MutationObj.as_mutable(
4389 'reviewer_data_json', MutationObj.as_mutable(
4390 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4390 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4391
4391
4392 @property
4392 @property
4393 def reviewer_data_json(self):
4393 def reviewer_data_json(self):
4394 return str_json(self.reviewer_data)
4394 return str_json(self.reviewer_data)
4395
4395
4396 @property
4396 @property
4397 def last_merge_metadata_parsed(self):
4397 def last_merge_metadata_parsed(self):
4398 metadata = {}
4398 metadata = {}
4399 if not self.last_merge_metadata:
4399 if not self.last_merge_metadata:
4400 return metadata
4400 return metadata
4401
4401
4402 if hasattr(self.last_merge_metadata, 'de_coerce'):
4402 if hasattr(self.last_merge_metadata, 'de_coerce'):
4403 for k, v in self.last_merge_metadata.de_coerce().items():
4403 for k, v in self.last_merge_metadata.de_coerce().items():
4404 if k in ['target_ref', 'source_ref']:
4404 if k in ['target_ref', 'source_ref']:
4405 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4405 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4406 else:
4406 else:
4407 if hasattr(v, 'de_coerce'):
4407 if hasattr(v, 'de_coerce'):
4408 metadata[k] = v.de_coerce()
4408 metadata[k] = v.de_coerce()
4409 else:
4409 else:
4410 metadata[k] = v
4410 metadata[k] = v
4411 return metadata
4411 return metadata
4412
4412
4413 @property
4413 @property
4414 def work_in_progress(self):
4414 def work_in_progress(self):
4415 """checks if pull request is work in progress by checking the title"""
4415 """checks if pull request is work in progress by checking the title"""
4416 title = self.title.upper()
4416 title = self.title.upper()
4417 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4417 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4418 return True
4418 return True
4419 return False
4419 return False
4420
4420
4421 @property
4421 @property
4422 def title_safe(self):
4422 def title_safe(self):
4423 return self.title\
4423 return self.title\
4424 .replace('{', '{{')\
4424 .replace('{', '{{')\
4425 .replace('}', '}}')
4425 .replace('}', '}}')
4426
4426
4427 @hybrid_property
4427 @hybrid_property
4428 def description_safe(self):
4428 def description_safe(self):
4429 return description_escaper(self.description)
4429 return description_escaper(self.description)
4430
4430
4431 @hybrid_property
4431 @hybrid_property
4432 def revisions(self):
4432 def revisions(self):
4433 return self._revisions.split(':') if self._revisions else []
4433 return self._revisions.split(':') if self._revisions else []
4434
4434
4435 @revisions.setter
4435 @revisions.setter
4436 def revisions(self, val):
4436 def revisions(self, val):
4437 self._revisions = ':'.join(val)
4437 self._revisions = ':'.join(val)
4438
4438
4439 @hybrid_property
4439 @hybrid_property
4440 def last_merge_status(self):
4440 def last_merge_status(self):
4441 return safe_int(self._last_merge_status)
4441 return safe_int(self._last_merge_status)
4442
4442
4443 @last_merge_status.setter
4443 @last_merge_status.setter
4444 def last_merge_status(self, val):
4444 def last_merge_status(self, val):
4445 self._last_merge_status = val
4445 self._last_merge_status = val
4446
4446
4447 @declared_attr
4447 @declared_attr
4448 def author(cls):
4448 def author(cls):
4449 return relationship(
4449 return relationship(
4450 'User', lazy='joined',
4450 'User', lazy='joined',
4451 #TODO, problem that is somehow :?
4451 #TODO, problem that is somehow :?
4452 #back_populates='user_pull_requests'
4452 #back_populates='user_pull_requests'
4453 )
4453 )
4454
4454
4455 @declared_attr
4455 @declared_attr
4456 def source_repo(cls):
4456 def source_repo(cls):
4457 return relationship(
4457 return relationship(
4458 'Repository',
4458 'Repository',
4459 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4459 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4460 overlaps="pr_source"
4460 overlaps="pr_source"
4461 )
4461 )
4462
4462
4463 @property
4463 @property
4464 def source_ref_parts(self):
4464 def source_ref_parts(self):
4465 return self.unicode_to_reference(self.source_ref)
4465 return self.unicode_to_reference(self.source_ref)
4466
4466
4467 @declared_attr
4467 @declared_attr
4468 def target_repo(cls):
4468 def target_repo(cls):
4469 return relationship(
4469 return relationship(
4470 'Repository',
4470 'Repository',
4471 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4471 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4472 overlaps="pr_target"
4472 overlaps="pr_target"
4473 )
4473 )
4474
4474
4475 @property
4475 @property
4476 def target_ref_parts(self):
4476 def target_ref_parts(self):
4477 return self.unicode_to_reference(self.target_ref)
4477 return self.unicode_to_reference(self.target_ref)
4478
4478
4479 @property
4479 @property
4480 def shadow_merge_ref(self):
4480 def shadow_merge_ref(self):
4481 return self.unicode_to_reference(self._shadow_merge_ref)
4481 return self.unicode_to_reference(self._shadow_merge_ref)
4482
4482
4483 @shadow_merge_ref.setter
4483 @shadow_merge_ref.setter
4484 def shadow_merge_ref(self, ref):
4484 def shadow_merge_ref(self, ref):
4485 self._shadow_merge_ref = self.reference_to_unicode(ref)
4485 self._shadow_merge_ref = self.reference_to_unicode(ref)
4486
4486
4487 @staticmethod
4487 @staticmethod
4488 def unicode_to_reference(raw):
4488 def unicode_to_reference(raw):
4489 return unicode_to_reference(raw)
4489 return unicode_to_reference(raw)
4490
4490
4491 @staticmethod
4491 @staticmethod
4492 def reference_to_unicode(ref):
4492 def reference_to_unicode(ref):
4493 return reference_to_unicode(ref)
4493 return reference_to_unicode(ref)
4494
4494
4495 def get_api_data(self, with_merge_state=True):
4495 def get_api_data(self, with_merge_state=True):
4496 from rhodecode.model.pull_request import PullRequestModel
4496 from rhodecode.model.pull_request import PullRequestModel
4497
4497
4498 pull_request = self
4498 pull_request = self
4499 if with_merge_state:
4499 if with_merge_state:
4500 merge_response, merge_status, msg = \
4500 merge_response, merge_status, msg = \
4501 PullRequestModel().merge_status(pull_request)
4501 PullRequestModel().merge_status(pull_request)
4502 merge_state = {
4502 merge_state = {
4503 'status': merge_status,
4503 'status': merge_status,
4504 'message': safe_str(msg),
4504 'message': safe_str(msg),
4505 }
4505 }
4506 else:
4506 else:
4507 merge_state = {'status': 'not_available',
4507 merge_state = {'status': 'not_available',
4508 'message': 'not_available'}
4508 'message': 'not_available'}
4509
4509
4510 merge_data = {
4510 merge_data = {
4511 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4511 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4512 'reference': (
4512 'reference': (
4513 pull_request.shadow_merge_ref.asdict()
4513 pull_request.shadow_merge_ref.asdict()
4514 if pull_request.shadow_merge_ref else None),
4514 if pull_request.shadow_merge_ref else None),
4515 }
4515 }
4516
4516
4517 data = {
4517 data = {
4518 'pull_request_id': pull_request.pull_request_id,
4518 'pull_request_id': pull_request.pull_request_id,
4519 'url': PullRequestModel().get_url(pull_request),
4519 'url': PullRequestModel().get_url(pull_request),
4520 'title': pull_request.title,
4520 'title': pull_request.title,
4521 'description': pull_request.description,
4521 'description': pull_request.description,
4522 'status': pull_request.status,
4522 'status': pull_request.status,
4523 'state': pull_request.pull_request_state,
4523 'state': pull_request.pull_request_state,
4524 'created_on': pull_request.created_on,
4524 'created_on': pull_request.created_on,
4525 'updated_on': pull_request.updated_on,
4525 'updated_on': pull_request.updated_on,
4526 'commit_ids': pull_request.revisions,
4526 'commit_ids': pull_request.revisions,
4527 'review_status': pull_request.calculated_review_status(),
4527 'review_status': pull_request.calculated_review_status(),
4528 'mergeable': merge_state,
4528 'mergeable': merge_state,
4529 'source': {
4529 'source': {
4530 'clone_url': pull_request.source_repo.clone_url(),
4530 'clone_url': pull_request.source_repo.clone_url(),
4531 'repository': pull_request.source_repo.repo_name,
4531 'repository': pull_request.source_repo.repo_name,
4532 'reference': {
4532 'reference': {
4533 'name': pull_request.source_ref_parts.name,
4533 'name': pull_request.source_ref_parts.name,
4534 'type': pull_request.source_ref_parts.type,
4534 'type': pull_request.source_ref_parts.type,
4535 'commit_id': pull_request.source_ref_parts.commit_id,
4535 'commit_id': pull_request.source_ref_parts.commit_id,
4536 },
4536 },
4537 },
4537 },
4538 'target': {
4538 'target': {
4539 'clone_url': pull_request.target_repo.clone_url(),
4539 'clone_url': pull_request.target_repo.clone_url(),
4540 'repository': pull_request.target_repo.repo_name,
4540 'repository': pull_request.target_repo.repo_name,
4541 'reference': {
4541 'reference': {
4542 'name': pull_request.target_ref_parts.name,
4542 'name': pull_request.target_ref_parts.name,
4543 'type': pull_request.target_ref_parts.type,
4543 'type': pull_request.target_ref_parts.type,
4544 'commit_id': pull_request.target_ref_parts.commit_id,
4544 'commit_id': pull_request.target_ref_parts.commit_id,
4545 },
4545 },
4546 },
4546 },
4547 'merge': merge_data,
4547 'merge': merge_data,
4548 'author': pull_request.author.get_api_data(include_secrets=False,
4548 'author': pull_request.author.get_api_data(include_secrets=False,
4549 details='basic'),
4549 details='basic'),
4550 'reviewers': [
4550 'reviewers': [
4551 {
4551 {
4552 'user': reviewer.get_api_data(include_secrets=False,
4552 'user': reviewer.get_api_data(include_secrets=False,
4553 details='basic'),
4553 details='basic'),
4554 'reasons': reasons,
4554 'reasons': reasons,
4555 'review_status': st[0][1].status if st else 'not_reviewed',
4555 'review_status': st[0][1].status if st else 'not_reviewed',
4556 }
4556 }
4557 for obj, reviewer, reasons, mandatory, st in
4557 for obj, reviewer, reasons, mandatory, st in
4558 pull_request.reviewers_statuses()
4558 pull_request.reviewers_statuses()
4559 ]
4559 ]
4560 }
4560 }
4561
4561
4562 return data
4562 return data
4563
4563
4564 def set_state(self, pull_request_state, final_state=None):
4564 def set_state(self, pull_request_state, final_state=None):
4565 """
4565 """
4566 # goes from initial state to updating to initial state.
4566 # goes from initial state to updating to initial state.
4567 # initial state can be changed by specifying back_state=
4567 # initial state can be changed by specifying back_state=
4568 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4568 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4569 pull_request.merge()
4569 pull_request.merge()
4570
4570
4571 :param pull_request_state:
4571 :param pull_request_state:
4572 :param final_state:
4572 :param final_state:
4573
4573
4574 """
4574 """
4575
4575
4576 return _SetState(self, pull_request_state, back_state=final_state)
4576 return _SetState(self, pull_request_state, back_state=final_state)
4577
4577
4578
4578
4579 class PullRequest(Base, _PullRequestBase):
4579 class PullRequest(Base, _PullRequestBase):
4580 __tablename__ = 'pull_requests'
4580 __tablename__ = 'pull_requests'
4581 __table_args__ = (
4581 __table_args__ = (
4582 base_table_args,
4582 base_table_args,
4583 )
4583 )
4584 LATEST_VER = 'latest'
4584 LATEST_VER = 'latest'
4585
4585
4586 pull_request_id = Column(
4586 pull_request_id = Column(
4587 'pull_request_id', Integer(), nullable=False, primary_key=True)
4587 'pull_request_id', Integer(), nullable=False, primary_key=True)
4588
4588
4589 def __repr__(self):
4589 def __repr__(self):
4590 if self.pull_request_id:
4590 if self.pull_request_id:
4591 return f'<DB:PullRequest #{self.pull_request_id}>'
4591 return f'<DB:PullRequest #{self.pull_request_id}>'
4592 else:
4592 else:
4593 return f'<DB:PullRequest at {id(self)!r}>'
4593 return f'<DB:PullRequest at {id(self)!r}>'
4594
4594
4595 def __str__(self):
4595 def __str__(self):
4596 if self.pull_request_id:
4596 if self.pull_request_id:
4597 return f'#{self.pull_request_id}'
4597 return f'#{self.pull_request_id}'
4598 else:
4598 else:
4599 return f'#{id(self)!r}'
4599 return f'#{id(self)!r}'
4600
4600
4601 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4601 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4602 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4602 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4603 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4603 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4604 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4604 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4605
4605
4606 @classmethod
4606 @classmethod
4607 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4607 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4608 internal_methods=None):
4608 internal_methods=None):
4609
4609
4610 class PullRequestDisplay(object):
4610 class PullRequestDisplay(object):
4611 """
4611 """
4612 Special object wrapper for showing PullRequest data via Versions
4612 Special object wrapper for showing PullRequest data via Versions
4613 It mimics PR object as close as possible. This is read only object
4613 It mimics PR object as close as possible. This is read only object
4614 just for display
4614 just for display
4615 """
4615 """
4616
4616
4617 def __init__(self, attrs, internal=None):
4617 def __init__(self, attrs, internal=None):
4618 self.attrs = attrs
4618 self.attrs = attrs
4619 # internal have priority over the given ones via attrs
4619 # internal have priority over the given ones via attrs
4620 self.internal = internal or ['versions']
4620 self.internal = internal or ['versions']
4621
4621
4622 def __getattr__(self, item):
4622 def __getattr__(self, item):
4623 if item in self.internal:
4623 if item in self.internal:
4624 return getattr(self, item)
4624 return getattr(self, item)
4625 try:
4625 try:
4626 return self.attrs[item]
4626 return self.attrs[item]
4627 except KeyError:
4627 except KeyError:
4628 raise AttributeError(
4628 raise AttributeError(
4629 '%s object has no attribute %s' % (self, item))
4629 '%s object has no attribute %s' % (self, item))
4630
4630
4631 def __repr__(self):
4631 def __repr__(self):
4632 pr_id = self.attrs.get('pull_request_id')
4632 pr_id = self.attrs.get('pull_request_id')
4633 return f'<DB:PullRequestDisplay #{pr_id}>'
4633 return f'<DB:PullRequestDisplay #{pr_id}>'
4634
4634
4635 def versions(self):
4635 def versions(self):
4636 return pull_request_obj.versions.order_by(
4636 return pull_request_obj.versions.order_by(
4637 PullRequestVersion.pull_request_version_id).all()
4637 PullRequestVersion.pull_request_version_id).all()
4638
4638
4639 def is_closed(self):
4639 def is_closed(self):
4640 return pull_request_obj.is_closed()
4640 return pull_request_obj.is_closed()
4641
4641
4642 def is_state_changing(self):
4642 def is_state_changing(self):
4643 return pull_request_obj.is_state_changing()
4643 return pull_request_obj.is_state_changing()
4644
4644
4645 @property
4645 @property
4646 def pull_request_version_id(self):
4646 def pull_request_version_id(self):
4647 return getattr(pull_request_obj, 'pull_request_version_id', None)
4647 return getattr(pull_request_obj, 'pull_request_version_id', None)
4648
4648
4649 @property
4649 @property
4650 def pull_request_last_version(self):
4650 def pull_request_last_version(self):
4651 return pull_request_obj.pull_request_last_version
4651 return pull_request_obj.pull_request_last_version
4652
4652
4653 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4653 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4654
4654
4655 attrs.author = StrictAttributeDict(
4655 attrs.author = StrictAttributeDict(
4656 pull_request_obj.author.get_api_data())
4656 pull_request_obj.author.get_api_data())
4657 if pull_request_obj.target_repo:
4657 if pull_request_obj.target_repo:
4658 attrs.target_repo = StrictAttributeDict(
4658 attrs.target_repo = StrictAttributeDict(
4659 pull_request_obj.target_repo.get_api_data())
4659 pull_request_obj.target_repo.get_api_data())
4660 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4660 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4661
4661
4662 if pull_request_obj.source_repo:
4662 if pull_request_obj.source_repo:
4663 attrs.source_repo = StrictAttributeDict(
4663 attrs.source_repo = StrictAttributeDict(
4664 pull_request_obj.source_repo.get_api_data())
4664 pull_request_obj.source_repo.get_api_data())
4665 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4665 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4666
4666
4667 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4667 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4668 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4668 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4669 attrs.revisions = pull_request_obj.revisions
4669 attrs.revisions = pull_request_obj.revisions
4670 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4670 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4671 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4671 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4672 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4672 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4673 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4673 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4674
4674
4675 return PullRequestDisplay(attrs, internal=internal_methods)
4675 return PullRequestDisplay(attrs, internal=internal_methods)
4676
4676
4677 def is_closed(self):
4677 def is_closed(self):
4678 return self.status == self.STATUS_CLOSED
4678 return self.status == self.STATUS_CLOSED
4679
4679
4680 def is_state_changing(self):
4680 def is_state_changing(self):
4681 return self.pull_request_state != PullRequest.STATE_CREATED
4681 return self.pull_request_state != PullRequest.STATE_CREATED
4682
4682
4683 def __json__(self):
4683 def __json__(self):
4684 return {
4684 return {
4685 'revisions': self.revisions,
4685 'revisions': self.revisions,
4686 'versions': self.versions_count
4686 'versions': self.versions_count
4687 }
4687 }
4688
4688
4689 def calculated_review_status(self):
4689 def calculated_review_status(self):
4690 from rhodecode.model.changeset_status import ChangesetStatusModel
4690 from rhodecode.model.changeset_status import ChangesetStatusModel
4691 return ChangesetStatusModel().calculated_review_status(self)
4691 return ChangesetStatusModel().calculated_review_status(self)
4692
4692
4693 def reviewers_statuses(self, user=None):
4693 def reviewers_statuses(self, user=None):
4694 from rhodecode.model.changeset_status import ChangesetStatusModel
4694 from rhodecode.model.changeset_status import ChangesetStatusModel
4695 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4695 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4696
4696
4697 def get_pull_request_reviewers(self, role=None):
4697 def get_pull_request_reviewers(self, role=None):
4698 qry = PullRequestReviewers.query()\
4698 qry = PullRequestReviewers.query()\
4699 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4699 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4700 if role:
4700 if role:
4701 qry = qry.filter(PullRequestReviewers.role == role)
4701 qry = qry.filter(PullRequestReviewers.role == role)
4702
4702
4703 return qry.all()
4703 return qry.all()
4704
4704
4705 @property
4705 @property
4706 def reviewers_count(self):
4706 def reviewers_count(self):
4707 qry = PullRequestReviewers.query()\
4707 qry = PullRequestReviewers.query()\
4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4710 return qry.count()
4710 return qry.count()
4711
4711
4712 @property
4712 @property
4713 def observers_count(self):
4713 def observers_count(self):
4714 qry = PullRequestReviewers.query()\
4714 qry = PullRequestReviewers.query()\
4715 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4715 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4716 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4716 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4717 return qry.count()
4717 return qry.count()
4718
4718
4719 def observers(self):
4719 def observers(self):
4720 qry = PullRequestReviewers.query()\
4720 qry = PullRequestReviewers.query()\
4721 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4721 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4722 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4722 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4723 .all()
4723 .all()
4724
4724
4725 for entry in qry:
4725 for entry in qry:
4726 yield entry, entry.user
4726 yield entry, entry.user
4727
4727
4728 @property
4728 @property
4729 def workspace_id(self):
4729 def workspace_id(self):
4730 from rhodecode.model.pull_request import PullRequestModel
4730 from rhodecode.model.pull_request import PullRequestModel
4731 return PullRequestModel()._workspace_id(self)
4731 return PullRequestModel()._workspace_id(self)
4732
4732
4733 def get_shadow_repo(self):
4733 def get_shadow_repo(self):
4734 workspace_id = self.workspace_id
4734 workspace_id = self.workspace_id
4735 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4735 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4736 if os.path.isdir(shadow_repository_path):
4736 if os.path.isdir(shadow_repository_path):
4737 vcs_obj = self.target_repo.scm_instance()
4737 vcs_obj = self.target_repo.scm_instance()
4738 return vcs_obj.get_shadow_instance(shadow_repository_path)
4738 return vcs_obj.get_shadow_instance(shadow_repository_path)
4739
4739
4740 @property
4740 @property
4741 def versions_count(self):
4741 def versions_count(self):
4742 """
4742 """
4743 return number of versions this PR have, e.g a PR that once been
4743 return number of versions this PR have, e.g a PR that once been
4744 updated will have 2 versions
4744 updated will have 2 versions
4745 """
4745 """
4746 return self.versions.count() + 1
4746 return self.versions.count() + 1
4747
4747
4748 @property
4748 @property
4749 def pull_request_last_version(self):
4749 def pull_request_last_version(self):
4750 return self.versions_count
4750 return self.versions_count
4751
4751
4752
4752
4753 class PullRequestVersion(Base, _PullRequestBase):
4753 class PullRequestVersion(Base, _PullRequestBase):
4754 __tablename__ = 'pull_request_versions'
4754 __tablename__ = 'pull_request_versions'
4755 __table_args__ = (
4755 __table_args__ = (
4756 base_table_args,
4756 base_table_args,
4757 )
4757 )
4758
4758
4759 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4759 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4760 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4760 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4761 pull_request = relationship('PullRequest', back_populates='versions')
4761 pull_request = relationship('PullRequest', back_populates='versions')
4762
4762
4763 def __repr__(self):
4763 def __repr__(self):
4764 if self.pull_request_version_id:
4764 if self.pull_request_version_id:
4765 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4765 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4766 else:
4766 else:
4767 return f'<DB:PullRequestVersion at {id(self)!r}>'
4767 return f'<DB:PullRequestVersion at {id(self)!r}>'
4768
4768
4769 @property
4769 @property
4770 def reviewers(self):
4770 def reviewers(self):
4771 return self.pull_request.reviewers
4771 return self.pull_request.reviewers
4772
4772
4773 @property
4773 @property
4774 def versions(self):
4774 def versions(self):
4775 return self.pull_request.versions
4775 return self.pull_request.versions
4776
4776
4777 def is_closed(self):
4777 def is_closed(self):
4778 # calculate from original
4778 # calculate from original
4779 return self.pull_request.status == self.STATUS_CLOSED
4779 return self.pull_request.status == self.STATUS_CLOSED
4780
4780
4781 def is_state_changing(self):
4781 def is_state_changing(self):
4782 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4782 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4783
4783
4784 def calculated_review_status(self):
4784 def calculated_review_status(self):
4785 return self.pull_request.calculated_review_status()
4785 return self.pull_request.calculated_review_status()
4786
4786
4787 def reviewers_statuses(self):
4787 def reviewers_statuses(self):
4788 return self.pull_request.reviewers_statuses()
4788 return self.pull_request.reviewers_statuses()
4789
4789
4790 def observers(self):
4790 def observers(self):
4791 return self.pull_request.observers()
4791 return self.pull_request.observers()
4792
4792
4793
4793
4794 class PullRequestReviewers(Base, BaseModel):
4794 class PullRequestReviewers(Base, BaseModel):
4795 __tablename__ = 'pull_request_reviewers'
4795 __tablename__ = 'pull_request_reviewers'
4796 __table_args__ = (
4796 __table_args__ = (
4797 base_table_args,
4797 base_table_args,
4798 )
4798 )
4799 ROLE_REVIEWER = 'reviewer'
4799 ROLE_REVIEWER = 'reviewer'
4800 ROLE_OBSERVER = 'observer'
4800 ROLE_OBSERVER = 'observer'
4801 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4801 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4802
4802
4803 @hybrid_property
4803 @hybrid_property
4804 def reasons(self):
4804 def reasons(self):
4805 if not self._reasons:
4805 if not self._reasons:
4806 return []
4806 return []
4807 return self._reasons
4807 return self._reasons
4808
4808
4809 @reasons.setter
4809 @reasons.setter
4810 def reasons(self, val):
4810 def reasons(self, val):
4811 val = val or []
4811 val = val or []
4812 if any(not isinstance(x, str) for x in val):
4812 if any(not isinstance(x, str) for x in val):
4813 raise Exception('invalid reasons type, must be list of strings')
4813 raise Exception('invalid reasons type, must be list of strings')
4814 self._reasons = val
4814 self._reasons = val
4815
4815
4816 pull_requests_reviewers_id = Column(
4816 pull_requests_reviewers_id = Column(
4817 'pull_requests_reviewers_id', Integer(), nullable=False,
4817 'pull_requests_reviewers_id', Integer(), nullable=False,
4818 primary_key=True)
4818 primary_key=True)
4819 pull_request_id = Column(
4819 pull_request_id = Column(
4820 "pull_request_id", Integer(),
4820 "pull_request_id", Integer(),
4821 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4821 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4822 user_id = Column(
4822 user_id = Column(
4823 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4823 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4824 _reasons = Column(
4824 _reasons = Column(
4825 'reason', MutationList.as_mutable(
4825 'reason', MutationList.as_mutable(
4826 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4826 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4827
4827
4828 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4828 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4829 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4829 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4830
4830
4831 user = relationship('User')
4831 user = relationship('User')
4832 pull_request = relationship('PullRequest', back_populates='reviewers')
4832 pull_request = relationship('PullRequest', back_populates='reviewers')
4833
4833
4834 rule_data = Column(
4834 rule_data = Column(
4835 'rule_data_json',
4835 'rule_data_json',
4836 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4836 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4837
4837
4838 def rule_user_group_data(self):
4838 def rule_user_group_data(self):
4839 """
4839 """
4840 Returns the voting user group rule data for this reviewer
4840 Returns the voting user group rule data for this reviewer
4841 """
4841 """
4842
4842
4843 if self.rule_data and 'vote_rule' in self.rule_data:
4843 if self.rule_data and 'vote_rule' in self.rule_data:
4844 user_group_data = {}
4844 user_group_data = {}
4845 if 'rule_user_group_entry_id' in self.rule_data:
4845 if 'rule_user_group_entry_id' in self.rule_data:
4846 # means a group with voting rules !
4846 # means a group with voting rules !
4847 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4847 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4848 user_group_data['name'] = self.rule_data['rule_name']
4848 user_group_data['name'] = self.rule_data['rule_name']
4849 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4849 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4850
4850
4851 return user_group_data
4851 return user_group_data
4852
4852
4853 @classmethod
4853 @classmethod
4854 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4854 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4855 qry = PullRequestReviewers.query()\
4855 qry = PullRequestReviewers.query()\
4856 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4856 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4857 if role:
4857 if role:
4858 qry = qry.filter(PullRequestReviewers.role == role)
4858 qry = qry.filter(PullRequestReviewers.role == role)
4859
4859
4860 return qry.all()
4860 return qry.all()
4861
4861
4862 def __repr__(self):
4862 def __repr__(self):
4863 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4863 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4864
4864
4865
4865
4866 class Notification(Base, BaseModel):
4866 class Notification(Base, BaseModel):
4867 __tablename__ = 'notifications'
4867 __tablename__ = 'notifications'
4868 __table_args__ = (
4868 __table_args__ = (
4869 Index('notification_type_idx', 'type'),
4869 Index('notification_type_idx', 'type'),
4870 base_table_args,
4870 base_table_args,
4871 )
4871 )
4872
4872
4873 TYPE_CHANGESET_COMMENT = 'cs_comment'
4873 TYPE_CHANGESET_COMMENT = 'cs_comment'
4874 TYPE_MESSAGE = 'message'
4874 TYPE_MESSAGE = 'message'
4875 TYPE_MENTION = 'mention'
4875 TYPE_MENTION = 'mention'
4876 TYPE_REGISTRATION = 'registration'
4876 TYPE_REGISTRATION = 'registration'
4877 TYPE_PULL_REQUEST = 'pull_request'
4877 TYPE_PULL_REQUEST = 'pull_request'
4878 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4878 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4879 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4879 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4880
4880
4881 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4881 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4882 subject = Column('subject', Unicode(512), nullable=True)
4882 subject = Column('subject', Unicode(512), nullable=True)
4883 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4883 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4884 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4884 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4885 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4885 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4886 type_ = Column('type', Unicode(255))
4886 type_ = Column('type', Unicode(255))
4887
4887
4888 created_by_user = relationship('User', back_populates='user_created_notifications')
4888 created_by_user = relationship('User', back_populates='user_created_notifications')
4889 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4889 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4890
4890
4891 @property
4891 @property
4892 def recipients(self):
4892 def recipients(self):
4893 return [x.user for x in UserNotification.query()\
4893 return [x.user for x in UserNotification.query()\
4894 .filter(UserNotification.notification == self)\
4894 .filter(UserNotification.notification == self)\
4895 .order_by(UserNotification.user_id.asc()).all()]
4895 .order_by(UserNotification.user_id.asc()).all()]
4896
4896
4897 @classmethod
4897 @classmethod
4898 def create(cls, created_by, subject, body, recipients, type_=None):
4898 def create(cls, created_by, subject, body, recipients, type_=None):
4899 if type_ is None:
4899 if type_ is None:
4900 type_ = Notification.TYPE_MESSAGE
4900 type_ = Notification.TYPE_MESSAGE
4901
4901
4902 notification = cls()
4902 notification = cls()
4903 notification.created_by_user = created_by
4903 notification.created_by_user = created_by
4904 notification.subject = subject
4904 notification.subject = subject
4905 notification.body = body
4905 notification.body = body
4906 notification.type_ = type_
4906 notification.type_ = type_
4907 notification.created_on = datetime.datetime.now()
4907 notification.created_on = datetime.datetime.now()
4908
4908
4909 # For each recipient link the created notification to his account
4909 # For each recipient link the created notification to his account
4910 for u in recipients:
4910 for u in recipients:
4911 assoc = UserNotification()
4911 assoc = UserNotification()
4912 assoc.user_id = u.user_id
4912 assoc.user_id = u.user_id
4913 assoc.notification = notification
4913 assoc.notification = notification
4914
4914
4915 # if created_by is inside recipients mark his notification
4915 # if created_by is inside recipients mark his notification
4916 # as read
4916 # as read
4917 if u.user_id == created_by.user_id:
4917 if u.user_id == created_by.user_id:
4918 assoc.read = True
4918 assoc.read = True
4919 Session().add(assoc)
4919 Session().add(assoc)
4920
4920
4921 Session().add(notification)
4921 Session().add(notification)
4922
4922
4923 return notification
4923 return notification
4924
4924
4925
4925
4926 class UserNotification(Base, BaseModel):
4926 class UserNotification(Base, BaseModel):
4927 __tablename__ = 'user_to_notification'
4927 __tablename__ = 'user_to_notification'
4928 __table_args__ = (
4928 __table_args__ = (
4929 UniqueConstraint('user_id', 'notification_id'),
4929 UniqueConstraint('user_id', 'notification_id'),
4930 base_table_args
4930 base_table_args
4931 )
4931 )
4932
4932
4933 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4933 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4934 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4934 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4935 read = Column('read', Boolean, default=False)
4935 read = Column('read', Boolean, default=False)
4936 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4936 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4937
4937
4938 user = relationship('User', lazy="joined", back_populates='notifications')
4938 user = relationship('User', lazy="joined", back_populates='notifications')
4939 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4939 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4940
4940
4941 def mark_as_read(self):
4941 def mark_as_read(self):
4942 self.read = True
4942 self.read = True
4943 Session().add(self)
4943 Session().add(self)
4944
4944
4945
4945
4946 class UserNotice(Base, BaseModel):
4946 class UserNotice(Base, BaseModel):
4947 __tablename__ = 'user_notices'
4947 __tablename__ = 'user_notices'
4948 __table_args__ = (
4948 __table_args__ = (
4949 base_table_args
4949 base_table_args
4950 )
4950 )
4951
4951
4952 NOTIFICATION_TYPE_MESSAGE = 'message'
4952 NOTIFICATION_TYPE_MESSAGE = 'message'
4953 NOTIFICATION_TYPE_NOTICE = 'notice'
4953 NOTIFICATION_TYPE_NOTICE = 'notice'
4954
4954
4955 NOTIFICATION_LEVEL_INFO = 'info'
4955 NOTIFICATION_LEVEL_INFO = 'info'
4956 NOTIFICATION_LEVEL_WARNING = 'warning'
4956 NOTIFICATION_LEVEL_WARNING = 'warning'
4957 NOTIFICATION_LEVEL_ERROR = 'error'
4957 NOTIFICATION_LEVEL_ERROR = 'error'
4958
4958
4959 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4959 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4960
4960
4961 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4961 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4962 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4962 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4963
4963
4964 notice_read = Column('notice_read', Boolean, default=False)
4964 notice_read = Column('notice_read', Boolean, default=False)
4965
4965
4966 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4966 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4967 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4967 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4968
4968
4969 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4969 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4970 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4970 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4971
4971
4972 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4972 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4973 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4973 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4974
4974
4975 @classmethod
4975 @classmethod
4976 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4976 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4977
4977
4978 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4978 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4979 cls.NOTIFICATION_LEVEL_WARNING,
4979 cls.NOTIFICATION_LEVEL_WARNING,
4980 cls.NOTIFICATION_LEVEL_INFO]:
4980 cls.NOTIFICATION_LEVEL_INFO]:
4981 return
4981 return
4982
4982
4983 from rhodecode.model.user import UserModel
4983 from rhodecode.model.user import UserModel
4984 user = UserModel().get_user(user)
4984 user = UserModel().get_user(user)
4985
4985
4986 new_notice = UserNotice()
4986 new_notice = UserNotice()
4987 if not allow_duplicate:
4987 if not allow_duplicate:
4988 existing_msg = UserNotice().query() \
4988 existing_msg = UserNotice().query() \
4989 .filter(UserNotice.user == user) \
4989 .filter(UserNotice.user == user) \
4990 .filter(UserNotice.notice_body == body) \
4990 .filter(UserNotice.notice_body == body) \
4991 .filter(UserNotice.notice_read == false()) \
4991 .filter(UserNotice.notice_read == false()) \
4992 .scalar()
4992 .scalar()
4993 if existing_msg:
4993 if existing_msg:
4994 log.warning('Ignoring duplicate notice for user %s', user)
4994 log.warning('Ignoring duplicate notice for user %s', user)
4995 return
4995 return
4996
4996
4997 new_notice.user = user
4997 new_notice.user = user
4998 new_notice.notice_subject = subject
4998 new_notice.notice_subject = subject
4999 new_notice.notice_body = body
4999 new_notice.notice_body = body
5000 new_notice.notification_level = notice_level
5000 new_notice.notification_level = notice_level
5001 Session().add(new_notice)
5001 Session().add(new_notice)
5002 Session().commit()
5002 Session().commit()
5003
5003
5004
5004
5005 class Gist(Base, BaseModel):
5005 class Gist(Base, BaseModel):
5006 __tablename__ = 'gists'
5006 __tablename__ = 'gists'
5007 __table_args__ = (
5007 __table_args__ = (
5008 Index('g_gist_access_id_idx', 'gist_access_id'),
5008 Index('g_gist_access_id_idx', 'gist_access_id'),
5009 Index('g_created_on_idx', 'created_on'),
5009 Index('g_created_on_idx', 'created_on'),
5010 base_table_args
5010 base_table_args
5011 )
5011 )
5012
5012
5013 GIST_PUBLIC = 'public'
5013 GIST_PUBLIC = 'public'
5014 GIST_PRIVATE = 'private'
5014 GIST_PRIVATE = 'private'
5015 DEFAULT_FILENAME = 'gistfile1.txt'
5015 DEFAULT_FILENAME = 'gistfile1.txt'
5016
5016
5017 ACL_LEVEL_PUBLIC = 'acl_public'
5017 ACL_LEVEL_PUBLIC = 'acl_public'
5018 ACL_LEVEL_PRIVATE = 'acl_private'
5018 ACL_LEVEL_PRIVATE = 'acl_private'
5019
5019
5020 gist_id = Column('gist_id', Integer(), primary_key=True)
5020 gist_id = Column('gist_id', Integer(), primary_key=True)
5021 gist_access_id = Column('gist_access_id', Unicode(250))
5021 gist_access_id = Column('gist_access_id', Unicode(250))
5022 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5022 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5023 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5023 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5024 gist_expires = Column('gist_expires', Float(53), nullable=False)
5024 gist_expires = Column('gist_expires', Float(53), nullable=False)
5025 gist_type = Column('gist_type', Unicode(128), nullable=False)
5025 gist_type = Column('gist_type', Unicode(128), nullable=False)
5026 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5026 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5027 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5027 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5028 acl_level = Column('acl_level', Unicode(128), nullable=True)
5028 acl_level = Column('acl_level', Unicode(128), nullable=True)
5029
5029
5030 owner = relationship('User', back_populates='user_gists')
5030 owner = relationship('User', back_populates='user_gists')
5031
5031
5032 def __repr__(self):
5032 def __repr__(self):
5033 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5033 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5034
5034
5035 @hybrid_property
5035 @hybrid_property
5036 def description_safe(self):
5036 def description_safe(self):
5037 return description_escaper(self.gist_description)
5037 return description_escaper(self.gist_description)
5038
5038
5039 @classmethod
5039 @classmethod
5040 def get_or_404(cls, id_):
5040 def get_or_404(cls, id_):
5041 from pyramid.httpexceptions import HTTPNotFound
5041 from pyramid.httpexceptions import HTTPNotFound
5042
5042
5043 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5043 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5044 if not res:
5044 if not res:
5045 log.debug('WARN: No DB entry with id %s', id_)
5045 log.debug('WARN: No DB entry with id %s', id_)
5046 raise HTTPNotFound()
5046 raise HTTPNotFound()
5047 return res
5047 return res
5048
5048
5049 @classmethod
5049 @classmethod
5050 def get_by_access_id(cls, gist_access_id):
5050 def get_by_access_id(cls, gist_access_id):
5051 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5051 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5052
5052
5053 def gist_url(self):
5053 def gist_url(self):
5054 from rhodecode.model.gist import GistModel
5054 from rhodecode.model.gist import GistModel
5055 return GistModel().get_url(self)
5055 return GistModel().get_url(self)
5056
5056
5057 @classmethod
5057 @classmethod
5058 def base_path(cls):
5058 def base_path(cls):
5059 """
5059 """
5060 Returns base path when all gists are stored
5060 Returns base path when all gists are stored
5061
5061
5062 :param cls:
5062 :param cls:
5063 """
5063 """
5064 from rhodecode.model.gist import GIST_STORE_LOC
5064 from rhodecode.model.gist import GIST_STORE_LOC
5065 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5065 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5066 repo_store_path = get_rhodecode_repo_store_path()
5066 repo_store_path = get_rhodecode_repo_store_path()
5067 return os.path.join(repo_store_path, GIST_STORE_LOC)
5067 return os.path.join(repo_store_path, GIST_STORE_LOC)
5068
5068
5069 def get_api_data(self):
5069 def get_api_data(self):
5070 """
5070 """
5071 Common function for generating gist related data for API
5071 Common function for generating gist related data for API
5072 """
5072 """
5073 gist = self
5073 gist = self
5074 data = {
5074 data = {
5075 'gist_id': gist.gist_id,
5075 'gist_id': gist.gist_id,
5076 'type': gist.gist_type,
5076 'type': gist.gist_type,
5077 'access_id': gist.gist_access_id,
5077 'access_id': gist.gist_access_id,
5078 'description': gist.gist_description,
5078 'description': gist.gist_description,
5079 'url': gist.gist_url(),
5079 'url': gist.gist_url(),
5080 'expires': gist.gist_expires,
5080 'expires': gist.gist_expires,
5081 'created_on': gist.created_on,
5081 'created_on': gist.created_on,
5082 'modified_at': gist.modified_at,
5082 'modified_at': gist.modified_at,
5083 'content': None,
5083 'content': None,
5084 'acl_level': gist.acl_level,
5084 'acl_level': gist.acl_level,
5085 }
5085 }
5086 return data
5086 return data
5087
5087
5088 def __json__(self):
5088 def __json__(self):
5089 data = dict()
5089 data = dict()
5090 data.update(self.get_api_data())
5090 data.update(self.get_api_data())
5091 return data
5091 return data
5092 # SCM functions
5092 # SCM functions
5093
5093
5094 def scm_instance(self, **kwargs):
5094 def scm_instance(self, **kwargs):
5095 """
5095 """
5096 Get an instance of VCS Repository
5096 Get an instance of VCS Repository
5097
5097
5098 :param kwargs:
5098 :param kwargs:
5099 """
5099 """
5100 from rhodecode.model.gist import GistModel
5100 from rhodecode.model.gist import GistModel
5101 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5101 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5102 return get_vcs_instance(
5102 return get_vcs_instance(
5103 repo_path=safe_str(full_repo_path), create=False,
5103 repo_path=safe_str(full_repo_path), create=False,
5104 _vcs_alias=GistModel.vcs_backend)
5104 _vcs_alias=GistModel.vcs_backend)
5105
5105
5106
5106
5107 class ExternalIdentity(Base, BaseModel):
5107 class ExternalIdentity(Base, BaseModel):
5108 __tablename__ = 'external_identities'
5108 __tablename__ = 'external_identities'
5109 __table_args__ = (
5109 __table_args__ = (
5110 Index('local_user_id_idx', 'local_user_id'),
5110 Index('local_user_id_idx', 'local_user_id'),
5111 Index('external_id_idx', 'external_id'),
5111 Index('external_id_idx', 'external_id'),
5112 base_table_args
5112 base_table_args
5113 )
5113 )
5114
5114
5115 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5115 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5116 external_username = Column('external_username', Unicode(1024), default='')
5116 external_username = Column('external_username', Unicode(1024), default='')
5117 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5117 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5118 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5118 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5119 access_token = Column('access_token', String(1024), default='')
5119 access_token = Column('access_token', String(1024), default='')
5120 alt_token = Column('alt_token', String(1024), default='')
5120 alt_token = Column('alt_token', String(1024), default='')
5121 token_secret = Column('token_secret', String(1024), default='')
5121 token_secret = Column('token_secret', String(1024), default='')
5122
5122
5123 @classmethod
5123 @classmethod
5124 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5124 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5125 """
5125 """
5126 Returns ExternalIdentity instance based on search params
5126 Returns ExternalIdentity instance based on search params
5127
5127
5128 :param external_id:
5128 :param external_id:
5129 :param provider_name:
5129 :param provider_name:
5130 :return: ExternalIdentity
5130 :return: ExternalIdentity
5131 """
5131 """
5132 query = cls.query()
5132 query = cls.query()
5133 query = query.filter(cls.external_id == external_id)
5133 query = query.filter(cls.external_id == external_id)
5134 query = query.filter(cls.provider_name == provider_name)
5134 query = query.filter(cls.provider_name == provider_name)
5135 if local_user_id:
5135 if local_user_id:
5136 query = query.filter(cls.local_user_id == local_user_id)
5136 query = query.filter(cls.local_user_id == local_user_id)
5137 return query.first()
5137 return query.first()
5138
5138
5139 @classmethod
5139 @classmethod
5140 def user_by_external_id_and_provider(cls, external_id, provider_name):
5140 def user_by_external_id_and_provider(cls, external_id, provider_name):
5141 """
5141 """
5142 Returns User instance based on search params
5142 Returns User instance based on search params
5143
5143
5144 :param external_id:
5144 :param external_id:
5145 :param provider_name:
5145 :param provider_name:
5146 :return: User
5146 :return: User
5147 """
5147 """
5148 query = User.query()
5148 query = User.query()
5149 query = query.filter(cls.external_id == external_id)
5149 query = query.filter(cls.external_id == external_id)
5150 query = query.filter(cls.provider_name == provider_name)
5150 query = query.filter(cls.provider_name == provider_name)
5151 query = query.filter(User.user_id == cls.local_user_id)
5151 query = query.filter(User.user_id == cls.local_user_id)
5152 return query.first()
5152 return query.first()
5153
5153
5154 @classmethod
5154 @classmethod
5155 def by_local_user_id(cls, local_user_id):
5155 def by_local_user_id(cls, local_user_id):
5156 """
5156 """
5157 Returns all tokens for user
5157 Returns all tokens for user
5158
5158
5159 :param local_user_id:
5159 :param local_user_id:
5160 :return: ExternalIdentity
5160 :return: ExternalIdentity
5161 """
5161 """
5162 query = cls.query()
5162 query = cls.query()
5163 query = query.filter(cls.local_user_id == local_user_id)
5163 query = query.filter(cls.local_user_id == local_user_id)
5164 return query
5164 return query
5165
5165
5166 @classmethod
5166 @classmethod
5167 def load_provider_plugin(cls, plugin_id):
5167 def load_provider_plugin(cls, plugin_id):
5168 from rhodecode.authentication.base import loadplugin
5168 from rhodecode.authentication.base import loadplugin
5169 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5169 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5170 auth_plugin = loadplugin(_plugin_id)
5170 auth_plugin = loadplugin(_plugin_id)
5171 return auth_plugin
5171 return auth_plugin
5172
5172
5173
5173
5174 class Integration(Base, BaseModel):
5174 class Integration(Base, BaseModel):
5175 __tablename__ = 'integrations'
5175 __tablename__ = 'integrations'
5176 __table_args__ = (
5176 __table_args__ = (
5177 base_table_args
5177 base_table_args
5178 )
5178 )
5179
5179
5180 integration_id = Column('integration_id', Integer(), primary_key=True)
5180 integration_id = Column('integration_id', Integer(), primary_key=True)
5181 integration_type = Column('integration_type', String(255))
5181 integration_type = Column('integration_type', String(255))
5182 enabled = Column('enabled', Boolean(), nullable=False)
5182 enabled = Column('enabled', Boolean(), nullable=False)
5183 name = Column('name', String(255), nullable=False)
5183 name = Column('name', String(255), nullable=False)
5184 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5184 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5185
5185
5186 settings = Column(
5186 settings = Column(
5187 'settings_json', MutationObj.as_mutable(
5187 'settings_json', MutationObj.as_mutable(
5188 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5188 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5189 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5190 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5190 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5191
5191
5192 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5192 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5193 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5193 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5194
5194
5195 @property
5195 @property
5196 def scope(self):
5196 def scope(self):
5197 if self.repo:
5197 if self.repo:
5198 return repr(self.repo)
5198 return repr(self.repo)
5199 if self.repo_group:
5199 if self.repo_group:
5200 if self.child_repos_only:
5200 if self.child_repos_only:
5201 return repr(self.repo_group) + ' (child repos only)'
5201 return repr(self.repo_group) + ' (child repos only)'
5202 else:
5202 else:
5203 return repr(self.repo_group) + ' (recursive)'
5203 return repr(self.repo_group) + ' (recursive)'
5204 if self.child_repos_only:
5204 if self.child_repos_only:
5205 return 'root_repos'
5205 return 'root_repos'
5206 return 'global'
5206 return 'global'
5207
5207
5208 def __repr__(self):
5208 def __repr__(self):
5209 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5209 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5210
5210
5211
5211
5212 class RepoReviewRuleUser(Base, BaseModel):
5212 class RepoReviewRuleUser(Base, BaseModel):
5213 __tablename__ = 'repo_review_rules_users'
5213 __tablename__ = 'repo_review_rules_users'
5214 __table_args__ = (
5214 __table_args__ = (
5215 base_table_args
5215 base_table_args
5216 )
5216 )
5217 ROLE_REVIEWER = 'reviewer'
5217 ROLE_REVIEWER = 'reviewer'
5218 ROLE_OBSERVER = 'observer'
5218 ROLE_OBSERVER = 'observer'
5219 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5219 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5220
5220
5221 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5221 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5222 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5222 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5223 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5223 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5224 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5224 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5225 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5225 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5226 user = relationship('User', back_populates='user_review_rules')
5226 user = relationship('User', back_populates='user_review_rules')
5227
5227
5228 def rule_data(self):
5228 def rule_data(self):
5229 return {
5229 return {
5230 'mandatory': self.mandatory,
5230 'mandatory': self.mandatory,
5231 'role': self.role,
5231 'role': self.role,
5232 }
5232 }
5233
5233
5234
5234
5235 class RepoReviewRuleUserGroup(Base, BaseModel):
5235 class RepoReviewRuleUserGroup(Base, BaseModel):
5236 __tablename__ = 'repo_review_rules_users_groups'
5236 __tablename__ = 'repo_review_rules_users_groups'
5237 __table_args__ = (
5237 __table_args__ = (
5238 base_table_args
5238 base_table_args
5239 )
5239 )
5240
5240
5241 VOTE_RULE_ALL = -1
5241 VOTE_RULE_ALL = -1
5242 ROLE_REVIEWER = 'reviewer'
5242 ROLE_REVIEWER = 'reviewer'
5243 ROLE_OBSERVER = 'observer'
5243 ROLE_OBSERVER = 'observer'
5244 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5244 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5245
5245
5246 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5246 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5247 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5247 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5248 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5248 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5249 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5249 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5250 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5250 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5251 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5251 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5252 users_group = relationship('UserGroup')
5252 users_group = relationship('UserGroup')
5253
5253
5254 def rule_data(self):
5254 def rule_data(self):
5255 return {
5255 return {
5256 'mandatory': self.mandatory,
5256 'mandatory': self.mandatory,
5257 'role': self.role,
5257 'role': self.role,
5258 'vote_rule': self.vote_rule
5258 'vote_rule': self.vote_rule
5259 }
5259 }
5260
5260
5261 @property
5261 @property
5262 def vote_rule_label(self):
5262 def vote_rule_label(self):
5263 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5263 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5264 return 'all must vote'
5264 return 'all must vote'
5265 else:
5265 else:
5266 return 'min. vote {}'.format(self.vote_rule)
5266 return 'min. vote {}'.format(self.vote_rule)
5267
5267
5268
5268
5269 class RepoReviewRule(Base, BaseModel):
5269 class RepoReviewRule(Base, BaseModel):
5270 __tablename__ = 'repo_review_rules'
5270 __tablename__ = 'repo_review_rules'
5271 __table_args__ = (
5271 __table_args__ = (
5272 base_table_args
5272 base_table_args
5273 )
5273 )
5274
5274
5275 repo_review_rule_id = Column(
5275 repo_review_rule_id = Column(
5276 'repo_review_rule_id', Integer(), primary_key=True)
5276 'repo_review_rule_id', Integer(), primary_key=True)
5277 repo_id = Column(
5277 repo_id = Column(
5278 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5278 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5279 repo = relationship('Repository', back_populates='review_rules')
5279 repo = relationship('Repository', back_populates='review_rules')
5280
5280
5281 review_rule_name = Column('review_rule_name', String(255))
5281 review_rule_name = Column('review_rule_name', String(255))
5282 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5282 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5283 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5283 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5284 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5284 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5285
5285
5286 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5286 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5287
5287
5288 # Legacy fields, just for backward compat
5288 # Legacy fields, just for backward compat
5289 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5289 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5290 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5290 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5291
5291
5292 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5292 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5293 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5293 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5294
5294
5295 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5295 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5296
5296
5297 rule_users = relationship('RepoReviewRuleUser')
5297 rule_users = relationship('RepoReviewRuleUser')
5298 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5298 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5299
5299
5300 def _validate_pattern(self, value):
5300 def _validate_pattern(self, value):
5301 re.compile('^' + glob2re(value) + '$')
5301 re.compile('^' + glob2re(value) + '$')
5302
5302
5303 @hybrid_property
5303 @hybrid_property
5304 def source_branch_pattern(self):
5304 def source_branch_pattern(self):
5305 return self._branch_pattern or '*'
5305 return self._branch_pattern or '*'
5306
5306
5307 @source_branch_pattern.setter
5307 @source_branch_pattern.setter
5308 def source_branch_pattern(self, value):
5308 def source_branch_pattern(self, value):
5309 self._validate_pattern(value)
5309 self._validate_pattern(value)
5310 self._branch_pattern = value or '*'
5310 self._branch_pattern = value or '*'
5311
5311
5312 @hybrid_property
5312 @hybrid_property
5313 def target_branch_pattern(self):
5313 def target_branch_pattern(self):
5314 return self._target_branch_pattern or '*'
5314 return self._target_branch_pattern or '*'
5315
5315
5316 @target_branch_pattern.setter
5316 @target_branch_pattern.setter
5317 def target_branch_pattern(self, value):
5317 def target_branch_pattern(self, value):
5318 self._validate_pattern(value)
5318 self._validate_pattern(value)
5319 self._target_branch_pattern = value or '*'
5319 self._target_branch_pattern = value or '*'
5320
5320
5321 @hybrid_property
5321 @hybrid_property
5322 def file_pattern(self):
5322 def file_pattern(self):
5323 return self._file_pattern or '*'
5323 return self._file_pattern or '*'
5324
5324
5325 @file_pattern.setter
5325 @file_pattern.setter
5326 def file_pattern(self, value):
5326 def file_pattern(self, value):
5327 self._validate_pattern(value)
5327 self._validate_pattern(value)
5328 self._file_pattern = value or '*'
5328 self._file_pattern = value or '*'
5329
5329
5330 @hybrid_property
5330 @hybrid_property
5331 def forbid_pr_author_to_review(self):
5331 def forbid_pr_author_to_review(self):
5332 return self.pr_author == 'forbid_pr_author'
5332 return self.pr_author == 'forbid_pr_author'
5333
5333
5334 @hybrid_property
5334 @hybrid_property
5335 def include_pr_author_to_review(self):
5335 def include_pr_author_to_review(self):
5336 return self.pr_author == 'include_pr_author'
5336 return self.pr_author == 'include_pr_author'
5337
5337
5338 @hybrid_property
5338 @hybrid_property
5339 def forbid_commit_author_to_review(self):
5339 def forbid_commit_author_to_review(self):
5340 return self.commit_author == 'forbid_commit_author'
5340 return self.commit_author == 'forbid_commit_author'
5341
5341
5342 @hybrid_property
5342 @hybrid_property
5343 def include_commit_author_to_review(self):
5343 def include_commit_author_to_review(self):
5344 return self.commit_author == 'include_commit_author'
5344 return self.commit_author == 'include_commit_author'
5345
5345
5346 def matches(self, source_branch, target_branch, files_changed):
5346 def matches(self, source_branch, target_branch, files_changed):
5347 """
5347 """
5348 Check if this review rule matches a branch/files in a pull request
5348 Check if this review rule matches a branch/files in a pull request
5349
5349
5350 :param source_branch: source branch name for the commit
5350 :param source_branch: source branch name for the commit
5351 :param target_branch: target branch name for the commit
5351 :param target_branch: target branch name for the commit
5352 :param files_changed: list of file paths changed in the pull request
5352 :param files_changed: list of file paths changed in the pull request
5353 """
5353 """
5354
5354
5355 source_branch = source_branch or ''
5355 source_branch = source_branch or ''
5356 target_branch = target_branch or ''
5356 target_branch = target_branch or ''
5357 files_changed = files_changed or []
5357 files_changed = files_changed or []
5358
5358
5359 branch_matches = True
5359 branch_matches = True
5360 if source_branch or target_branch:
5360 if source_branch or target_branch:
5361 if self.source_branch_pattern == '*':
5361 if self.source_branch_pattern == '*':
5362 source_branch_match = True
5362 source_branch_match = True
5363 else:
5363 else:
5364 if self.source_branch_pattern.startswith('re:'):
5364 if self.source_branch_pattern.startswith('re:'):
5365 source_pattern = self.source_branch_pattern[3:]
5365 source_pattern = self.source_branch_pattern[3:]
5366 else:
5366 else:
5367 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5367 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5368 source_branch_regex = re.compile(source_pattern)
5368 source_branch_regex = re.compile(source_pattern)
5369 source_branch_match = bool(source_branch_regex.search(source_branch))
5369 source_branch_match = bool(source_branch_regex.search(source_branch))
5370 if self.target_branch_pattern == '*':
5370 if self.target_branch_pattern == '*':
5371 target_branch_match = True
5371 target_branch_match = True
5372 else:
5372 else:
5373 if self.target_branch_pattern.startswith('re:'):
5373 if self.target_branch_pattern.startswith('re:'):
5374 target_pattern = self.target_branch_pattern[3:]
5374 target_pattern = self.target_branch_pattern[3:]
5375 else:
5375 else:
5376 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5376 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5377 target_branch_regex = re.compile(target_pattern)
5377 target_branch_regex = re.compile(target_pattern)
5378 target_branch_match = bool(target_branch_regex.search(target_branch))
5378 target_branch_match = bool(target_branch_regex.search(target_branch))
5379
5379
5380 branch_matches = source_branch_match and target_branch_match
5380 branch_matches = source_branch_match and target_branch_match
5381
5381
5382 files_matches = True
5382 files_matches = True
5383 if self.file_pattern != '*':
5383 if self.file_pattern != '*':
5384 files_matches = False
5384 files_matches = False
5385 if self.file_pattern.startswith('re:'):
5385 if self.file_pattern.startswith('re:'):
5386 file_pattern = self.file_pattern[3:]
5386 file_pattern = self.file_pattern[3:]
5387 else:
5387 else:
5388 file_pattern = glob2re(self.file_pattern)
5388 file_pattern = glob2re(self.file_pattern)
5389 file_regex = re.compile(file_pattern)
5389 file_regex = re.compile(file_pattern)
5390 for file_data in files_changed:
5390 for file_data in files_changed:
5391 filename = file_data.get('filename')
5391 filename = file_data.get('filename')
5392
5392
5393 if file_regex.search(filename):
5393 if file_regex.search(filename):
5394 files_matches = True
5394 files_matches = True
5395 break
5395 break
5396
5396
5397 return branch_matches and files_matches
5397 return branch_matches and files_matches
5398
5398
5399 @property
5399 @property
5400 def review_users(self):
5400 def review_users(self):
5401 """ Returns the users which this rule applies to """
5401 """ Returns the users which this rule applies to """
5402
5402
5403 users = collections.OrderedDict()
5403 users = collections.OrderedDict()
5404
5404
5405 for rule_user in self.rule_users:
5405 for rule_user in self.rule_users:
5406 if rule_user.user.active:
5406 if rule_user.user.active:
5407 if rule_user.user not in users:
5407 if rule_user.user not in users:
5408 users[rule_user.user.username] = {
5408 users[rule_user.user.username] = {
5409 'user': rule_user.user,
5409 'user': rule_user.user,
5410 'source': 'user',
5410 'source': 'user',
5411 'source_data': {},
5411 'source_data': {},
5412 'data': rule_user.rule_data()
5412 'data': rule_user.rule_data()
5413 }
5413 }
5414
5414
5415 for rule_user_group in self.rule_user_groups:
5415 for rule_user_group in self.rule_user_groups:
5416 source_data = {
5416 source_data = {
5417 'user_group_id': rule_user_group.users_group.users_group_id,
5417 'user_group_id': rule_user_group.users_group.users_group_id,
5418 'name': rule_user_group.users_group.users_group_name,
5418 'name': rule_user_group.users_group.users_group_name,
5419 'members': len(rule_user_group.users_group.members)
5419 'members': len(rule_user_group.users_group.members)
5420 }
5420 }
5421 for member in rule_user_group.users_group.members:
5421 for member in rule_user_group.users_group.members:
5422 if member.user.active:
5422 if member.user.active:
5423 key = member.user.username
5423 key = member.user.username
5424 if key in users:
5424 if key in users:
5425 # skip this member as we have him already
5425 # skip this member as we have him already
5426 # this prevents from override the "first" matched
5426 # this prevents from override the "first" matched
5427 # users with duplicates in multiple groups
5427 # users with duplicates in multiple groups
5428 continue
5428 continue
5429
5429
5430 users[key] = {
5430 users[key] = {
5431 'user': member.user,
5431 'user': member.user,
5432 'source': 'user_group',
5432 'source': 'user_group',
5433 'source_data': source_data,
5433 'source_data': source_data,
5434 'data': rule_user_group.rule_data()
5434 'data': rule_user_group.rule_data()
5435 }
5435 }
5436
5436
5437 return users
5437 return users
5438
5438
5439 def user_group_vote_rule(self, user_id):
5439 def user_group_vote_rule(self, user_id):
5440
5440
5441 rules = []
5441 rules = []
5442 if not self.rule_user_groups:
5442 if not self.rule_user_groups:
5443 return rules
5443 return rules
5444
5444
5445 for user_group in self.rule_user_groups:
5445 for user_group in self.rule_user_groups:
5446 user_group_members = [x.user_id for x in user_group.users_group.members]
5446 user_group_members = [x.user_id for x in user_group.users_group.members]
5447 if user_id in user_group_members:
5447 if user_id in user_group_members:
5448 rules.append(user_group)
5448 rules.append(user_group)
5449 return rules
5449 return rules
5450
5450
5451 def __repr__(self):
5451 def __repr__(self):
5452 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5452 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5453
5453
5454
5454
5455 class ScheduleEntry(Base, BaseModel):
5455 class ScheduleEntry(Base, BaseModel):
5456 __tablename__ = 'schedule_entries'
5456 __tablename__ = 'schedule_entries'
5457 __table_args__ = (
5457 __table_args__ = (
5458 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5458 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5459 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5459 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5460 base_table_args,
5460 base_table_args,
5461 )
5461 )
5462 SCHEDULE_TYPE_INTEGER = "integer"
5462 SCHEDULE_TYPE_INTEGER = "integer"
5463 SCHEDULE_TYPE_CRONTAB = "crontab"
5463 SCHEDULE_TYPE_CRONTAB = "crontab"
5464
5464
5465 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5465 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5466 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5466 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5467
5467
5468 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5468 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5469 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5469 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5470 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5470 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5471
5471
5472 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5472 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5473 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5473 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5474
5474
5475 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5475 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5476 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5476 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5477
5477
5478 # task
5478 # task
5479 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5479 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5480 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5480 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5481 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5481 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5482 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5482 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5483
5483
5484 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5484 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5485 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5485 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5486
5486
5487 @hybrid_property
5487 @hybrid_property
5488 def schedule_type(self):
5488 def schedule_type(self):
5489 return self._schedule_type
5489 return self._schedule_type
5490
5490
5491 @schedule_type.setter
5491 @schedule_type.setter
5492 def schedule_type(self, val):
5492 def schedule_type(self, val):
5493 if val not in self.schedule_types:
5493 if val not in self.schedule_types:
5494 raise ValueError(f'Value must be on of `{val}` and got `{self.schedule_type}`')
5494 raise ValueError(f'Value must be on of `{val}` and got `{self.schedule_type}`')
5495
5495
5496 self._schedule_type = val
5496 self._schedule_type = val
5497
5497
5498 @classmethod
5498 @classmethod
5499 def get_uid(cls, obj):
5499 def get_uid(cls, obj):
5500 args = obj.task_args
5500 args = obj.task_args
5501 kwargs = obj.task_kwargs
5501 kwargs = obj.task_kwargs
5502
5502
5503 if isinstance(args, JsonRaw):
5503 if isinstance(args, JsonRaw):
5504 try:
5504 try:
5505 args = json.loads(str(args))
5505 args = json.loads(str(args))
5506 except ValueError:
5506 except ValueError:
5507 log.exception('json.loads of args failed...')
5507 log.exception('json.loads of args failed...')
5508 args = tuple()
5508 args = tuple()
5509
5509
5510 if isinstance(kwargs, JsonRaw):
5510 if isinstance(kwargs, JsonRaw):
5511 try:
5511 try:
5512 kwargs = json.loads(str(kwargs))
5512 kwargs = json.loads(str(kwargs))
5513 except ValueError:
5513 except ValueError:
5514 log.exception('json.loads of kwargs failed...')
5514 log.exception('json.loads of kwargs failed...')
5515 kwargs = dict()
5515 kwargs = dict()
5516
5516
5517 dot_notation = obj.task_dot_notation
5517 dot_notation = obj.task_dot_notation
5518 val = '.'.join(map(safe_str, [dot_notation, args, sorted(kwargs.items())]))
5518 val = '.'.join(map(safe_str, [dot_notation, args, sorted(kwargs.items())]))
5519 log.debug('calculating task uid using id:`%s`', val)
5519 log.debug('calculating task uid using id:`%s`', val)
5520
5520
5521 return sha1(safe_bytes(val))
5521 return sha1(safe_bytes(val))
5522
5522
5523 @classmethod
5523 @classmethod
5524 def get_by_schedule_name(cls, schedule_name):
5524 def get_by_schedule_name(cls, schedule_name):
5525 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5525 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5526
5526
5527 @classmethod
5527 @classmethod
5528 def get_by_schedule_id(cls, schedule_id):
5528 def get_by_schedule_id(cls, schedule_id):
5529 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5529 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5530
5530
5531 @classmethod
5531 @classmethod
5532 def get_by_task_uid(cls, task_uid):
5532 def get_by_task_uid(cls, task_uid):
5533 return cls.query().filter(cls.task_uid == task_uid).scalar()
5533 return cls.query().filter(cls.task_uid == task_uid).scalar()
5534
5534
5535 @property
5535 @property
5536 def task(self):
5536 def task(self):
5537 return self.task_dot_notation
5537 return self.task_dot_notation
5538
5538
5539 @property
5539 @property
5540 def schedule(self):
5540 def schedule(self):
5541 from rhodecode.lib.celerylib.utils import raw_2_schedule
5541 from rhodecode.lib.celerylib.utils import raw_2_schedule
5542 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5542 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5543 return schedule
5543 return schedule
5544
5544
5545 @property
5545 @property
5546 def args(self):
5546 def args(self):
5547 try:
5547 try:
5548 return list(self.task_args or [])
5548 return list(self.task_args or [])
5549 except ValueError:
5549 except ValueError:
5550 return list()
5550 return list()
5551
5551
5552 @property
5552 @property
5553 def kwargs(self):
5553 def kwargs(self):
5554 try:
5554 try:
5555 return dict(self.task_kwargs or {})
5555 return dict(self.task_kwargs or {})
5556 except ValueError:
5556 except ValueError:
5557 return dict()
5557 return dict()
5558
5558
5559 def _as_raw(self, val, indent=False):
5559 def _as_raw(self, val, indent=False):
5560 if hasattr(val, 'de_coerce'):
5560 if hasattr(val, 'de_coerce'):
5561 val = val.de_coerce()
5561 val = val.de_coerce()
5562 if val:
5562 if val:
5563 if indent:
5563 if indent:
5564 val = ext_json.formatted_str_json(val)
5564 val = ext_json.formatted_str_json(val)
5565 else:
5565 else:
5566 val = ext_json.str_json(val)
5566 val = ext_json.str_json(val)
5567
5567
5568 return val
5568 return val
5569
5569
5570 @property
5570 @property
5571 def schedule_definition_raw(self):
5571 def schedule_definition_raw(self):
5572 return self._as_raw(self.schedule_definition)
5572 return self._as_raw(self.schedule_definition)
5573
5573
5574 def args_raw(self, indent=False):
5574 def args_raw(self, indent=False):
5575 return self._as_raw(self.task_args, indent)
5575 return self._as_raw(self.task_args, indent)
5576
5576
5577 def kwargs_raw(self, indent=False):
5577 def kwargs_raw(self, indent=False):
5578 return self._as_raw(self.task_kwargs, indent)
5578 return self._as_raw(self.task_kwargs, indent)
5579
5579
5580 def __repr__(self):
5580 def __repr__(self):
5581 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5581 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5582
5582
5583
5583
5584 @event.listens_for(ScheduleEntry, 'before_update')
5584 @event.listens_for(ScheduleEntry, 'before_update')
5585 def update_task_uid(mapper, connection, target):
5585 def update_task_uid(mapper, connection, target):
5586 target.task_uid = ScheduleEntry.get_uid(target)
5586 target.task_uid = ScheduleEntry.get_uid(target)
5587
5587
5588
5588
5589 @event.listens_for(ScheduleEntry, 'before_insert')
5589 @event.listens_for(ScheduleEntry, 'before_insert')
5590 def set_task_uid(mapper, connection, target):
5590 def set_task_uid(mapper, connection, target):
5591 target.task_uid = ScheduleEntry.get_uid(target)
5591 target.task_uid = ScheduleEntry.get_uid(target)
5592
5592
5593
5593
5594 class _BaseBranchPerms(BaseModel):
5594 class _BaseBranchPerms(BaseModel):
5595 @classmethod
5595 @classmethod
5596 def compute_hash(cls, value):
5596 def compute_hash(cls, value):
5597 return sha1_safe(value)
5597 return sha1_safe(value)
5598
5598
5599 @hybrid_property
5599 @hybrid_property
5600 def branch_pattern(self):
5600 def branch_pattern(self):
5601 return self._branch_pattern or '*'
5601 return self._branch_pattern or '*'
5602
5602
5603 @hybrid_property
5603 @hybrid_property
5604 def branch_hash(self):
5604 def branch_hash(self):
5605 return self._branch_hash
5605 return self._branch_hash
5606
5606
5607 def _validate_glob(self, value):
5607 def _validate_glob(self, value):
5608 re.compile('^' + glob2re(value) + '$')
5608 re.compile('^' + glob2re(value) + '$')
5609
5609
5610 @branch_pattern.setter
5610 @branch_pattern.setter
5611 def branch_pattern(self, value):
5611 def branch_pattern(self, value):
5612 self._validate_glob(value)
5612 self._validate_glob(value)
5613 self._branch_pattern = value or '*'
5613 self._branch_pattern = value or '*'
5614 # set the Hash when setting the branch pattern
5614 # set the Hash when setting the branch pattern
5615 self._branch_hash = self.compute_hash(self._branch_pattern)
5615 self._branch_hash = self.compute_hash(self._branch_pattern)
5616
5616
5617 def matches(self, branch):
5617 def matches(self, branch):
5618 """
5618 """
5619 Check if this the branch matches entry
5619 Check if this the branch matches entry
5620
5620
5621 :param branch: branch name for the commit
5621 :param branch: branch name for the commit
5622 """
5622 """
5623
5623
5624 branch = branch or ''
5624 branch = branch or ''
5625
5625
5626 branch_matches = True
5626 branch_matches = True
5627 if branch:
5627 if branch:
5628 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5628 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5629 branch_matches = bool(branch_regex.search(branch))
5629 branch_matches = bool(branch_regex.search(branch))
5630
5630
5631 return branch_matches
5631 return branch_matches
5632
5632
5633
5633
5634 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5634 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5635 __tablename__ = 'user_to_repo_branch_permissions'
5635 __tablename__ = 'user_to_repo_branch_permissions'
5636 __table_args__ = (
5636 __table_args__ = (
5637 base_table_args
5637 base_table_args
5638 )
5638 )
5639
5639
5640 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5640 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5641
5641
5642 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5642 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5643 repo = relationship('Repository', back_populates='user_branch_perms')
5643 repo = relationship('Repository', back_populates='user_branch_perms')
5644
5644
5645 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5645 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5646 permission = relationship('Permission')
5646 permission = relationship('Permission')
5647
5647
5648 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5648 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5649 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5649 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5650
5650
5651 rule_order = Column('rule_order', Integer(), nullable=False)
5651 rule_order = Column('rule_order', Integer(), nullable=False)
5652 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5652 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5653 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5653 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5654
5654
5655 def __repr__(self):
5655 def __repr__(self):
5656 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5656 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5657
5657
5658
5658
5659 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5659 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5660 __tablename__ = 'user_group_to_repo_branch_permissions'
5660 __tablename__ = 'user_group_to_repo_branch_permissions'
5661 __table_args__ = (
5661 __table_args__ = (
5662 base_table_args
5662 base_table_args
5663 )
5663 )
5664
5664
5665 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5665 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5666
5666
5667 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5667 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5668 repo = relationship('Repository', back_populates='user_group_branch_perms')
5668 repo = relationship('Repository', back_populates='user_group_branch_perms')
5669
5669
5670 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5670 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5671 permission = relationship('Permission')
5671 permission = relationship('Permission')
5672
5672
5673 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)
5673 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)
5674 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5674 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5675
5675
5676 rule_order = Column('rule_order', Integer(), nullable=False)
5676 rule_order = Column('rule_order', Integer(), nullable=False)
5677 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5677 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5678 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5678 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5679
5679
5680 def __repr__(self):
5680 def __repr__(self):
5681 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5681 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5682
5682
5683
5683
5684 class UserBookmark(Base, BaseModel):
5684 class UserBookmark(Base, BaseModel):
5685 __tablename__ = 'user_bookmarks'
5685 __tablename__ = 'user_bookmarks'
5686 __table_args__ = (
5686 __table_args__ = (
5687 UniqueConstraint('user_id', 'bookmark_repo_id'),
5687 UniqueConstraint('user_id', 'bookmark_repo_id'),
5688 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5688 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5689 UniqueConstraint('user_id', 'bookmark_position'),
5689 UniqueConstraint('user_id', 'bookmark_position'),
5690 base_table_args
5690 base_table_args
5691 )
5691 )
5692
5692
5693 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5693 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5694 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5694 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5695 position = Column("bookmark_position", Integer(), nullable=False)
5695 position = Column("bookmark_position", Integer(), nullable=False)
5696 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5696 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5697 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5697 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5698 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5698 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5699
5699
5700 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5700 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5701 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5701 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5702
5702
5703 user = relationship("User")
5703 user = relationship("User")
5704
5704
5705 repository = relationship("Repository")
5705 repository = relationship("Repository")
5706 repository_group = relationship("RepoGroup")
5706 repository_group = relationship("RepoGroup")
5707
5707
5708 @classmethod
5708 @classmethod
5709 def get_by_position_for_user(cls, position, user_id):
5709 def get_by_position_for_user(cls, position, user_id):
5710 return cls.query() \
5710 return cls.query() \
5711 .filter(UserBookmark.user_id == user_id) \
5711 .filter(UserBookmark.user_id == user_id) \
5712 .filter(UserBookmark.position == position).scalar()
5712 .filter(UserBookmark.position == position).scalar()
5713
5713
5714 @classmethod
5714 @classmethod
5715 def get_bookmarks_for_user(cls, user_id, cache=True):
5715 def get_bookmarks_for_user(cls, user_id, cache=True):
5716 bookmarks = select(
5716 bookmarks = select(
5717 UserBookmark.title,
5717 UserBookmark.title,
5718 UserBookmark.position,
5718 UserBookmark.position,
5719 ) \
5719 ) \
5720 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5720 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5721 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5721 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5722 .where(UserBookmark.user_id == user_id) \
5722 .where(UserBookmark.user_id == user_id) \
5723 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5723 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5724 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5724 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5725 .order_by(UserBookmark.position.asc())
5725 .order_by(UserBookmark.position.asc())
5726
5726
5727 if cache:
5727 if cache:
5728 bookmarks = bookmarks.options(
5728 bookmarks = bookmarks.options(
5729 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5729 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5730 )
5730 )
5731
5731
5732 return Session().execute(bookmarks).all()
5732 return Session().execute(bookmarks).all()
5733
5733
5734 def __repr__(self):
5734 def __repr__(self):
5735 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5735 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5736
5736
5737
5737
5738 class FileStore(Base, BaseModel):
5738 class FileStore(Base, BaseModel):
5739 __tablename__ = 'file_store'
5739 __tablename__ = 'file_store'
5740 __table_args__ = (
5740 __table_args__ = (
5741 base_table_args
5741 base_table_args
5742 )
5742 )
5743
5743
5744 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5744 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5745 file_uid = Column('file_uid', String(1024), nullable=False)
5745 file_uid = Column('file_uid', String(1024), nullable=False)
5746 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5746 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5747 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5747 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5748 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5748 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5749
5749
5750 # sha256 hash
5750 # sha256 hash
5751 file_hash = Column('file_hash', String(512), nullable=False)
5751 file_hash = Column('file_hash', String(512), nullable=False)
5752 file_size = Column('file_size', BigInteger(), nullable=False)
5752 file_size = Column('file_size', BigInteger(), nullable=False)
5753
5753
5754 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5754 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5755 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5755 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5756 accessed_count = Column('accessed_count', Integer(), default=0)
5756 accessed_count = Column('accessed_count', Integer(), default=0)
5757
5757
5758 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5758 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5759
5759
5760 # if repo/repo_group reference is set, check for permissions
5760 # if repo/repo_group reference is set, check for permissions
5761 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5761 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5762
5762
5763 # hidden defines an attachment that should be hidden from showing in artifact listing
5763 # hidden defines an attachment that should be hidden from showing in artifact listing
5764 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5764 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5765
5765
5766 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5766 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5767 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5767 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5768
5768
5769 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5769 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5770
5770
5771 # scope limited to user, which requester have access to
5771 # scope limited to user, which requester have access to
5772 scope_user_id = Column(
5772 scope_user_id = Column(
5773 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5773 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5774 nullable=True, unique=None, default=None)
5774 nullable=True, unique=None, default=None)
5775 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5775 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5776
5776
5777 # scope limited to user group, which requester have access to
5777 # scope limited to user group, which requester have access to
5778 scope_user_group_id = Column(
5778 scope_user_group_id = Column(
5779 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5779 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5780 nullable=True, unique=None, default=None)
5780 nullable=True, unique=None, default=None)
5781 user_group = relationship('UserGroup', lazy='joined')
5781 user_group = relationship('UserGroup', lazy='joined')
5782
5782
5783 # scope limited to repo, which requester have access to
5783 # scope limited to repo, which requester have access to
5784 scope_repo_id = Column(
5784 scope_repo_id = Column(
5785 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5785 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5786 nullable=True, unique=None, default=None)
5786 nullable=True, unique=None, default=None)
5787 repo = relationship('Repository', lazy='joined')
5787 repo = relationship('Repository', lazy='joined')
5788
5788
5789 # scope limited to repo group, which requester have access to
5789 # scope limited to repo group, which requester have access to
5790 scope_repo_group_id = Column(
5790 scope_repo_group_id = Column(
5791 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5791 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5792 nullable=True, unique=None, default=None)
5792 nullable=True, unique=None, default=None)
5793 repo_group = relationship('RepoGroup', lazy='joined')
5793 repo_group = relationship('RepoGroup', lazy='joined')
5794
5794
5795 @classmethod
5795 @classmethod
5796 def get_scope(cls, scope_type, scope_id):
5796 def get_scope(cls, scope_type, scope_id):
5797 if scope_type == 'repo':
5797 if scope_type == 'repo':
5798 return f'repo:{scope_id}'
5798 return f'repo:{scope_id}'
5799 elif scope_type == 'repo-group':
5799 elif scope_type == 'repo-group':
5800 return f'repo-group:{scope_id}'
5800 return f'repo-group:{scope_id}'
5801 elif scope_type == 'user':
5801 elif scope_type == 'user':
5802 return f'user:{scope_id}'
5802 return f'user:{scope_id}'
5803 elif scope_type == 'user-group':
5803 elif scope_type == 'user-group':
5804 return f'user-group:{scope_id}'
5804 return f'user-group:{scope_id}'
5805 else:
5805 else:
5806 return scope_type
5806 return scope_type
5807
5807
5808 @classmethod
5808 @classmethod
5809 def get_by_store_uid(cls, file_store_uid, safe=False):
5809 def get_by_store_uid(cls, file_store_uid, safe=False):
5810 if safe:
5810 if safe:
5811 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5811 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5812 else:
5812 else:
5813 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5813 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5814
5814
5815 @classmethod
5815 @classmethod
5816 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5816 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5817 file_description='', enabled=True, hidden=False, check_acl=True,
5817 file_description='', enabled=True, hidden=False, check_acl=True,
5818 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5818 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5819
5819
5820 store_entry = FileStore()
5820 store_entry = FileStore()
5821 store_entry.file_uid = file_uid
5821 store_entry.file_uid = file_uid
5822 store_entry.file_display_name = file_display_name
5822 store_entry.file_display_name = file_display_name
5823 store_entry.file_org_name = filename
5823 store_entry.file_org_name = filename
5824 store_entry.file_size = file_size
5824 store_entry.file_size = file_size
5825 store_entry.file_hash = file_hash
5825 store_entry.file_hash = file_hash
5826 store_entry.file_description = file_description
5826 store_entry.file_description = file_description
5827
5827
5828 store_entry.check_acl = check_acl
5828 store_entry.check_acl = check_acl
5829 store_entry.enabled = enabled
5829 store_entry.enabled = enabled
5830 store_entry.hidden = hidden
5830 store_entry.hidden = hidden
5831
5831
5832 store_entry.user_id = user_id
5832 store_entry.user_id = user_id
5833 store_entry.scope_user_id = scope_user_id
5833 store_entry.scope_user_id = scope_user_id
5834 store_entry.scope_repo_id = scope_repo_id
5834 store_entry.scope_repo_id = scope_repo_id
5835 store_entry.scope_repo_group_id = scope_repo_group_id
5835 store_entry.scope_repo_group_id = scope_repo_group_id
5836
5836
5837 return store_entry
5837 return store_entry
5838
5838
5839 @classmethod
5839 @classmethod
5840 def store_metadata(cls, file_store_id, args, commit=True):
5840 def store_metadata(cls, file_store_id, args, commit=True):
5841 file_store = FileStore.get(file_store_id)
5841 file_store = FileStore.get(file_store_id)
5842 if file_store is None:
5842 if file_store is None:
5843 return
5843 return
5844
5844
5845 for section, key, value, value_type in args:
5845 for section, key, value, value_type in args:
5846 has_key = FileStoreMetadata().query() \
5846 has_key = FileStoreMetadata().query() \
5847 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5847 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5848 .filter(FileStoreMetadata.file_store_meta_section == section) \
5848 .filter(FileStoreMetadata.file_store_meta_section == section) \
5849 .filter(FileStoreMetadata.file_store_meta_key == key) \
5849 .filter(FileStoreMetadata.file_store_meta_key == key) \
5850 .scalar()
5850 .scalar()
5851 if has_key:
5851 if has_key:
5852 msg = f'key `{key}` already defined under section `{section}` for this file.'
5852 msg = f'key `{key}` already defined under section `{section}` for this file.'
5853 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5853 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5854
5854
5855 # NOTE(marcink): raises ArtifactMetadataBadValueType
5855 # NOTE(marcink): raises ArtifactMetadataBadValueType
5856 FileStoreMetadata.valid_value_type(value_type)
5856 FileStoreMetadata.valid_value_type(value_type)
5857
5857
5858 meta_entry = FileStoreMetadata()
5858 meta_entry = FileStoreMetadata()
5859 meta_entry.file_store = file_store
5859 meta_entry.file_store = file_store
5860 meta_entry.file_store_meta_section = section
5860 meta_entry.file_store_meta_section = section
5861 meta_entry.file_store_meta_key = key
5861 meta_entry.file_store_meta_key = key
5862 meta_entry.file_store_meta_value_type = value_type
5862 meta_entry.file_store_meta_value_type = value_type
5863 meta_entry.file_store_meta_value = value
5863 meta_entry.file_store_meta_value = value
5864
5864
5865 Session().add(meta_entry)
5865 Session().add(meta_entry)
5866
5866
5867 try:
5867 try:
5868 if commit:
5868 if commit:
5869 Session().commit()
5869 Session().commit()
5870 except IntegrityError:
5870 except IntegrityError:
5871 Session().rollback()
5871 Session().rollback()
5872 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5872 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5873
5873
5874 @classmethod
5874 @classmethod
5875 def bump_access_counter(cls, file_uid, commit=True):
5875 def bump_access_counter(cls, file_uid, commit=True):
5876 FileStore().query()\
5876 FileStore().query()\
5877 .filter(FileStore.file_uid == file_uid)\
5877 .filter(FileStore.file_uid == file_uid)\
5878 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5878 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5879 FileStore.accessed_on: datetime.datetime.now()})
5879 FileStore.accessed_on: datetime.datetime.now()})
5880 if commit:
5880 if commit:
5881 Session().commit()
5881 Session().commit()
5882
5882
5883 def __json__(self):
5883 def __json__(self):
5884 data = {
5884 data = {
5885 'filename': self.file_display_name,
5885 'filename': self.file_display_name,
5886 'filename_org': self.file_org_name,
5886 'filename_org': self.file_org_name,
5887 'file_uid': self.file_uid,
5887 'file_uid': self.file_uid,
5888 'description': self.file_description,
5888 'description': self.file_description,
5889 'hidden': self.hidden,
5889 'hidden': self.hidden,
5890 'size': self.file_size,
5890 'size': self.file_size,
5891 'created_on': self.created_on,
5891 'created_on': self.created_on,
5892 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5892 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5893 'downloaded_times': self.accessed_count,
5893 'downloaded_times': self.accessed_count,
5894 'sha256': self.file_hash,
5894 'sha256': self.file_hash,
5895 'metadata': self.file_metadata,
5895 'metadata': self.file_metadata,
5896 }
5896 }
5897
5897
5898 return data
5898 return data
5899
5899
5900 def __repr__(self):
5900 def __repr__(self):
5901 return f'<FileStore({self.file_store_id})>'
5901 return f'<FileStore({self.file_store_id})>'
5902
5902
5903
5903
5904 class FileStoreMetadata(Base, BaseModel):
5904 class FileStoreMetadata(Base, BaseModel):
5905 __tablename__ = 'file_store_metadata'
5905 __tablename__ = 'file_store_metadata'
5906 __table_args__ = (
5906 __table_args__ = (
5907 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5907 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5908 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5908 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5909 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5909 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5910 base_table_args
5910 base_table_args
5911 )
5911 )
5912 SETTINGS_TYPES = {
5912 SETTINGS_TYPES = {
5913 'str': safe_str,
5913 'str': safe_str,
5914 'int': safe_int,
5914 'int': safe_int,
5915 'unicode': safe_str,
5915 'unicode': safe_str,
5916 'bool': str2bool,
5916 'bool': str2bool,
5917 'list': functools.partial(aslist, sep=',')
5917 'list': functools.partial(aslist, sep=',')
5918 }
5918 }
5919
5919
5920 file_store_meta_id = Column(
5920 file_store_meta_id = Column(
5921 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5921 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5922 primary_key=True)
5922 primary_key=True)
5923 _file_store_meta_section = Column(
5923 _file_store_meta_section = Column(
5924 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5924 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5925 nullable=True, unique=None, default=None)
5925 nullable=True, unique=None, default=None)
5926 _file_store_meta_section_hash = Column(
5926 _file_store_meta_section_hash = Column(
5927 "file_store_meta_section_hash", String(255),
5927 "file_store_meta_section_hash", String(255),
5928 nullable=True, unique=None, default=None)
5928 nullable=True, unique=None, default=None)
5929 _file_store_meta_key = Column(
5929 _file_store_meta_key = Column(
5930 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5930 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5931 nullable=True, unique=None, default=None)
5931 nullable=True, unique=None, default=None)
5932 _file_store_meta_key_hash = Column(
5932 _file_store_meta_key_hash = Column(
5933 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5933 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5934 _file_store_meta_value = Column(
5934 _file_store_meta_value = Column(
5935 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5935 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5936 nullable=True, unique=None, default=None)
5936 nullable=True, unique=None, default=None)
5937 _file_store_meta_value_type = Column(
5937 _file_store_meta_value_type = Column(
5938 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5938 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5939 default='unicode')
5939 default='unicode')
5940
5940
5941 file_store_id = Column(
5941 file_store_id = Column(
5942 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5942 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5943 nullable=True, unique=None, default=None)
5943 nullable=True, unique=None, default=None)
5944
5944
5945 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5945 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5946
5946
5947 @classmethod
5947 @classmethod
5948 def valid_value_type(cls, value):
5948 def valid_value_type(cls, value):
5949 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5949 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5950 raise ArtifactMetadataBadValueType(
5950 raise ArtifactMetadataBadValueType(
5951 f'value_type must be one of {cls.SETTINGS_TYPES.keys()} got {value}')
5951 f'value_type must be one of {cls.SETTINGS_TYPES.keys()} got {value}')
5952
5952
5953 @hybrid_property
5953 @hybrid_property
5954 def file_store_meta_section(self):
5954 def file_store_meta_section(self):
5955 return self._file_store_meta_section
5955 return self._file_store_meta_section
5956
5956
5957 @file_store_meta_section.setter
5957 @file_store_meta_section.setter
5958 def file_store_meta_section(self, value):
5958 def file_store_meta_section(self, value):
5959 self._file_store_meta_section = value
5959 self._file_store_meta_section = value
5960 self._file_store_meta_section_hash = _hash_key(value)
5960 self._file_store_meta_section_hash = _hash_key(value)
5961
5961
5962 @hybrid_property
5962 @hybrid_property
5963 def file_store_meta_key(self):
5963 def file_store_meta_key(self):
5964 return self._file_store_meta_key
5964 return self._file_store_meta_key
5965
5965
5966 @file_store_meta_key.setter
5966 @file_store_meta_key.setter
5967 def file_store_meta_key(self, value):
5967 def file_store_meta_key(self, value):
5968 self._file_store_meta_key = value
5968 self._file_store_meta_key = value
5969 self._file_store_meta_key_hash = _hash_key(value)
5969 self._file_store_meta_key_hash = _hash_key(value)
5970
5970
5971 @hybrid_property
5971 @hybrid_property
5972 def file_store_meta_value(self):
5972 def file_store_meta_value(self):
5973 val = self._file_store_meta_value
5973 val = self._file_store_meta_value
5974
5974
5975 if self._file_store_meta_value_type:
5975 if self._file_store_meta_value_type:
5976 # e.g unicode.encrypted == unicode
5976 # e.g unicode.encrypted == unicode
5977 _type = self._file_store_meta_value_type.split('.')[0]
5977 _type = self._file_store_meta_value_type.split('.')[0]
5978 # decode the encrypted value if it's encrypted field type
5978 # decode the encrypted value if it's encrypted field type
5979 if '.encrypted' in self._file_store_meta_value_type:
5979 if '.encrypted' in self._file_store_meta_value_type:
5980 cipher = EncryptedTextValue()
5980 cipher = EncryptedTextValue()
5981 val = safe_str(cipher.process_result_value(val, None))
5981 val = safe_str(cipher.process_result_value(val, None))
5982 # do final type conversion
5982 # do final type conversion
5983 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5983 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5984 val = converter(val)
5984 val = converter(val)
5985
5985
5986 return val
5986 return val
5987
5987
5988 @file_store_meta_value.setter
5988 @file_store_meta_value.setter
5989 def file_store_meta_value(self, val):
5989 def file_store_meta_value(self, val):
5990 val = safe_str(val)
5990 val = safe_str(val)
5991 # encode the encrypted value
5991 # encode the encrypted value
5992 if '.encrypted' in self.file_store_meta_value_type:
5992 if '.encrypted' in self.file_store_meta_value_type:
5993 cipher = EncryptedTextValue()
5993 cipher = EncryptedTextValue()
5994 val = safe_str(cipher.process_bind_param(val, None))
5994 val = safe_str(cipher.process_bind_param(val, None))
5995 self._file_store_meta_value = val
5995 self._file_store_meta_value = val
5996
5996
5997 @hybrid_property
5997 @hybrid_property
5998 def file_store_meta_value_type(self):
5998 def file_store_meta_value_type(self):
5999 return self._file_store_meta_value_type
5999 return self._file_store_meta_value_type
6000
6000
6001 @file_store_meta_value_type.setter
6001 @file_store_meta_value_type.setter
6002 def file_store_meta_value_type(self, val):
6002 def file_store_meta_value_type(self, val):
6003 # e.g unicode.encrypted
6003 # e.g unicode.encrypted
6004 self.valid_value_type(val)
6004 self.valid_value_type(val)
6005 self._file_store_meta_value_type = val
6005 self._file_store_meta_value_type = val
6006
6006
6007 def __json__(self):
6007 def __json__(self):
6008 data = {
6008 data = {
6009 'artifact': self.file_store.file_uid,
6009 'artifact': self.file_store.file_uid,
6010 'section': self.file_store_meta_section,
6010 'section': self.file_store_meta_section,
6011 'key': self.file_store_meta_key,
6011 'key': self.file_store_meta_key,
6012 'value': self.file_store_meta_value,
6012 'value': self.file_store_meta_value,
6013 }
6013 }
6014
6014
6015 return data
6015 return data
6016
6016
6017 def __repr__(self):
6017 def __repr__(self):
6018 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6018 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6019 self.file_store_meta_key, self.file_store_meta_value)
6019 self.file_store_meta_key, self.file_store_meta_value)
6020
6020
6021
6021
6022 class DbMigrateVersion(Base, BaseModel):
6022 class DbMigrateVersion(Base, BaseModel):
6023 __tablename__ = 'db_migrate_version'
6023 __tablename__ = 'db_migrate_version'
6024 __table_args__ = (
6024 __table_args__ = (
6025 base_table_args,
6025 base_table_args,
6026 )
6026 )
6027
6027
6028 repository_id = Column('repository_id', String(250), primary_key=True)
6028 repository_id = Column('repository_id', String(250), primary_key=True)
6029 repository_path = Column('repository_path', Text)
6029 repository_path = Column('repository_path', Text)
6030 version = Column('version', Integer)
6030 version = Column('version', Integer)
6031
6031
6032 @classmethod
6032 @classmethod
6033 def set_version(cls, version):
6033 def set_version(cls, version):
6034 """
6034 """
6035 Helper for forcing a different version, usually for debugging purposes via ishell.
6035 Helper for forcing a different version, usually for debugging purposes via ishell.
6036 """
6036 """
6037 ver = DbMigrateVersion.query().first()
6037 ver = DbMigrateVersion.query().first()
6038 ver.version = version
6038 ver.version = version
6039 Session().commit()
6039 Session().commit()
6040
6040
6041
6041
6042 class DbSession(Base, BaseModel):
6042 class DbSession(Base, BaseModel):
6043 __tablename__ = 'db_session'
6043 __tablename__ = 'db_session'
6044 __table_args__ = (
6044 __table_args__ = (
6045 base_table_args,
6045 base_table_args,
6046 )
6046 )
6047
6047
6048 def __repr__(self):
6048 def __repr__(self):
6049 return f'<DB:DbSession({self.id})>'
6049 return f'<DB:DbSession({self.id})>'
6050
6050
6051 id = Column('id', Integer())
6051 id = Column('id', Integer())
6052 namespace = Column('namespace', String(255), primary_key=True)
6052 namespace = Column('namespace', String(255), primary_key=True)
6053 accessed = Column('accessed', DateTime, nullable=False)
6053 accessed = Column('accessed', DateTime, nullable=False)
6054 created = Column('created', DateTime, nullable=False)
6054 created = Column('created', DateTime, nullable=False)
6055 data = Column('data', PickleType, nullable=False)
6055 data = Column('data', PickleType, nullable=False)
@@ -1,1055 +1,1056 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 users model for RhodeCode
20 users model for RhodeCode
21 """
21 """
22
22
23 import logging
23 import logging
24 import traceback
24 import traceback
25 import datetime
25 import datetime
26 import ipaddress
26 import ipaddress
27
27
28 from pyramid.threadlocal import get_current_request
28 from pyramid.threadlocal import get_current_request
29 from sqlalchemy.exc import DatabaseError
29 from sqlalchemy.exc import DatabaseError
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.lib.user_log_filter import user_log_filter
32 from rhodecode.lib.user_log_filter import user_log_filter
33 from rhodecode.lib.utils2 import (
33 from rhodecode.lib.utils2 import (
34 get_current_rhodecode_user, action_logger_generic,
34 get_current_rhodecode_user, action_logger_generic,
35 AttributeDict, str2bool)
35 AttributeDict, str2bool)
36 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.lib.str_utils import safe_str
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 from rhodecode.lib.caching_query import FromCache
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 UserEmailMap, UserIpMap, UserLog)
45 UserEmailMap, UserIpMap, UserLog)
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.repo_group import RepoGroupModel
48 from rhodecode.model.repo_group import RepoGroupModel
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class UserModel(BaseModel):
53 class UserModel(BaseModel):
54 cls = User
54 cls = User
55
55
56 def get(self, user_id, cache=False):
56 def get(self, user_id, cache=False):
57 user = self.sa.query(User)
57 cls = self.cls
58 q = cls.select().where(cls.user_id == user_id)
58 if cache:
59 if cache:
59 user = user.options(
60 q = q.options(
60 FromCache("sql_cache_short", f"get_user_{user_id}"))
61 FromCache("sql_cache_short", f"get_users_{user_id}"))
61 return user.get(user_id)
62 return cls.execute(q).scalar_one_or_none()
62
63
63 def get_user(self, user):
64 def get_user(self, user):
64 return self._get_user(user)
65 return self._get_user(user)
65
66
66 def _serialize_user(self, user):
67 def _serialize_user(self, user):
67 import rhodecode.lib.helpers as h
68 import rhodecode.lib.helpers as h
68
69
69 return {
70 return {
70 'id': user.user_id,
71 'id': user.user_id,
71 'first_name': user.first_name,
72 'first_name': user.first_name,
72 'last_name': user.last_name,
73 'last_name': user.last_name,
73 'username': user.username,
74 'username': user.username,
74 'email': user.email,
75 'email': user.email,
75 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
76 'profile_link': h.link_to_user(user),
77 'profile_link': h.link_to_user(user),
77 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
78 'value': user.username,
79 'value': user.username,
79 'value_type': 'user',
80 'value_type': 'user',
80 'active': user.active,
81 'active': user.active,
81 }
82 }
82
83
83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 def get_users(self, name_contains=None, limit=20, only_active=True):
84
85
85 query = self.sa.query(User)
86 query = self.sa.query(User)
86 if only_active:
87 if only_active:
87 query = query.filter(User.active == true())
88 query = query.filter(User.active == true())
88
89
89 if name_contains:
90 if name_contains:
90 ilike_expression = f'%{safe_str(name_contains)}%'
91 ilike_expression = f'%{safe_str(name_contains)}%'
91 query = query.filter(
92 query = query.filter(
92 or_(
93 or_(
93 User.name.ilike(ilike_expression),
94 User.name.ilike(ilike_expression),
94 User.lastname.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
95 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
96 )
97 )
97 )
98 )
98 # sort by len to have top most matches first
99 # sort by len to have top most matches first
99 query = query.order_by(func.length(User.username))\
100 query = query.order_by(func.length(User.username))\
100 .order_by(User.username)
101 .order_by(User.username)
101 query = query.limit(limit)
102 query = query.limit(limit)
102
103
103 users = query.all()
104 users = query.all()
104
105
105 _users = [
106 _users = [
106 self._serialize_user(user) for user in users
107 self._serialize_user(user) for user in users
107 ]
108 ]
108 return _users
109 return _users
109
110
110 def get_by_username(self, username, cache=False, case_insensitive=False):
111 def get_by_username(self, username, cache=False, case_insensitive=False):
111
112
112 if case_insensitive:
113 if case_insensitive:
113 user = self.sa.query(User).filter(User.username.ilike(username))
114 user = self.sa.query(User).filter(User.username.ilike(username))
114 else:
115 else:
115 user = self.sa.query(User)\
116 user = self.sa.query(User)\
116 .filter(User.username == username)
117 .filter(User.username == username)
117
118
118 if cache:
119 if cache:
119 name_key = _hash_key(username)
120 name_key = _hash_key(username)
120 user = user.options(
121 user = user.options(
121 FromCache("sql_cache_short", f"get_user_{name_key}"))
122 FromCache("sql_cache_short", f"get_user_{name_key}"))
122 return user.scalar()
123 return user.scalar()
123
124
124 def get_by_email(self, email, cache=False, case_insensitive=False):
125 def get_by_email(self, email, cache=False, case_insensitive=False):
125 return User.get_by_email(email, case_insensitive, cache)
126 return User.get_by_email(email, case_insensitive, cache)
126
127
127 def get_by_auth_token(self, auth_token, cache=False):
128 def get_by_auth_token(self, auth_token, cache=False):
128 return User.get_by_auth_token(auth_token, cache)
129 return User.get_by_auth_token(auth_token, cache)
129
130
130 def get_active_user_count(self, cache=False):
131 def get_active_user_count(self, cache=False):
131 qry = User.query().filter(
132 qry = User.query().filter(
132 User.active == true()).filter(
133 User.active == true()).filter(
133 User.username != User.DEFAULT_USER)
134 User.username != User.DEFAULT_USER)
134 if cache:
135 if cache:
135 qry = qry.options(
136 qry = qry.options(
136 FromCache("sql_cache_short", "get_active_users"))
137 FromCache("sql_cache_short", "get_active_users"))
137 return qry.count()
138 return qry.count()
138
139
139 def create(self, form_data, cur_user=None):
140 def create(self, form_data, cur_user=None):
140 if not cur_user:
141 if not cur_user:
141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
142 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
142
143
143 user_data = {
144 user_data = {
144 'username': form_data['username'],
145 'username': form_data['username'],
145 'password': form_data['password'],
146 'password': form_data['password'],
146 'email': form_data['email'],
147 'email': form_data['email'],
147 'firstname': form_data['firstname'],
148 'firstname': form_data['firstname'],
148 'lastname': form_data['lastname'],
149 'lastname': form_data['lastname'],
149 'active': form_data['active'],
150 'active': form_data['active'],
150 'extern_type': form_data['extern_type'],
151 'extern_type': form_data['extern_type'],
151 'extern_name': form_data['extern_name'],
152 'extern_name': form_data['extern_name'],
152 'admin': False,
153 'admin': False,
153 'cur_user': cur_user
154 'cur_user': cur_user
154 }
155 }
155
156
156 if 'create_repo_group' in form_data:
157 if 'create_repo_group' in form_data:
157 user_data['create_repo_group'] = str2bool(
158 user_data['create_repo_group'] = str2bool(
158 form_data.get('create_repo_group'))
159 form_data.get('create_repo_group'))
159
160
160 try:
161 try:
161 if form_data.get('password_change'):
162 if form_data.get('password_change'):
162 user_data['force_password_change'] = True
163 user_data['force_password_change'] = True
163 return UserModel().create_or_update(**user_data)
164 return UserModel().create_or_update(**user_data)
164 except Exception:
165 except Exception:
165 log.error(traceback.format_exc())
166 log.error(traceback.format_exc())
166 raise
167 raise
167
168
168 def update_user(self, user, skip_attrs=None, **kwargs):
169 def update_user(self, user, skip_attrs=None, **kwargs):
169 from rhodecode.lib.auth import get_crypt_password
170 from rhodecode.lib.auth import get_crypt_password
170
171
171 user = self._get_user(user)
172 user = self._get_user(user)
172 if user.username == User.DEFAULT_USER:
173 if user.username == User.DEFAULT_USER:
173 raise DefaultUserException(
174 raise DefaultUserException(
174 "You can't edit this user (`%(username)s`) since it's "
175 "You can't edit this user (`%(username)s`) since it's "
175 "crucial for entire application" % {
176 "crucial for entire application" % {
176 'username': user.username})
177 'username': user.username})
177
178
178 # first store only defaults
179 # first store only defaults
179 user_attrs = {
180 user_attrs = {
180 'updating_user_id': user.user_id,
181 'updating_user_id': user.user_id,
181 'username': user.username,
182 'username': user.username,
182 'password': user.password,
183 'password': user.password,
183 'email': user.email,
184 'email': user.email,
184 'firstname': user.name,
185 'firstname': user.name,
185 'lastname': user.lastname,
186 'lastname': user.lastname,
186 'description': user.description,
187 'description': user.description,
187 'active': user.active,
188 'active': user.active,
188 'admin': user.admin,
189 'admin': user.admin,
189 'extern_name': user.extern_name,
190 'extern_name': user.extern_name,
190 'extern_type': user.extern_type,
191 'extern_type': user.extern_type,
191 'language': user.user_data.get('language')
192 'language': user.user_data.get('language')
192 }
193 }
193
194
194 # in case there's new_password, that comes from form, use it to
195 # in case there's new_password, that comes from form, use it to
195 # store password
196 # store password
196 if kwargs.get('new_password'):
197 if kwargs.get('new_password'):
197 kwargs['password'] = kwargs['new_password']
198 kwargs['password'] = kwargs['new_password']
198
199
199 # cleanups, my_account password change form
200 # cleanups, my_account password change form
200 kwargs.pop('current_password', None)
201 kwargs.pop('current_password', None)
201 kwargs.pop('new_password', None)
202 kwargs.pop('new_password', None)
202
203
203 # cleanups, user edit password change form
204 # cleanups, user edit password change form
204 kwargs.pop('password_confirmation', None)
205 kwargs.pop('password_confirmation', None)
205 kwargs.pop('password_change', None)
206 kwargs.pop('password_change', None)
206
207
207 # create repo group on user creation
208 # create repo group on user creation
208 kwargs.pop('create_repo_group', None)
209 kwargs.pop('create_repo_group', None)
209
210
210 # legacy forms send name, which is the firstname
211 # legacy forms send name, which is the firstname
211 firstname = kwargs.pop('name', None)
212 firstname = kwargs.pop('name', None)
212 if firstname:
213 if firstname:
213 kwargs['firstname'] = firstname
214 kwargs['firstname'] = firstname
214
215
215 for k, v in kwargs.items():
216 for k, v in kwargs.items():
216 # skip if we don't want to update this
217 # skip if we don't want to update this
217 if skip_attrs and k in skip_attrs:
218 if skip_attrs and k in skip_attrs:
218 continue
219 continue
219
220
220 user_attrs[k] = v
221 user_attrs[k] = v
221
222
222 try:
223 try:
223 return self.create_or_update(**user_attrs)
224 return self.create_or_update(**user_attrs)
224 except Exception:
225 except Exception:
225 log.error(traceback.format_exc())
226 log.error(traceback.format_exc())
226 raise
227 raise
227
228
228 def create_or_update(
229 def create_or_update(
229 self, username, password, email, firstname='', lastname='',
230 self, username, password, email, firstname='', lastname='',
230 active=True, admin=False, extern_type=None, extern_name=None,
231 active=True, admin=False, extern_type=None, extern_name=None,
231 cur_user=None, plugin=None, force_password_change=False,
232 cur_user=None, plugin=None, force_password_change=False,
232 allow_to_create_user=True, create_repo_group=None,
233 allow_to_create_user=True, create_repo_group=None,
233 updating_user_id=None, language=None, description='',
234 updating_user_id=None, language=None, description='',
234 strict_creation_check=True):
235 strict_creation_check=True):
235 """
236 """
236 Creates a new instance if not found, or updates current one
237 Creates a new instance if not found, or updates current one
237
238
238 :param username:
239 :param username:
239 :param password:
240 :param password:
240 :param email:
241 :param email:
241 :param firstname:
242 :param firstname:
242 :param lastname:
243 :param lastname:
243 :param active:
244 :param active:
244 :param admin:
245 :param admin:
245 :param extern_type:
246 :param extern_type:
246 :param extern_name:
247 :param extern_name:
247 :param cur_user:
248 :param cur_user:
248 :param plugin: optional plugin this method was called from
249 :param plugin: optional plugin this method was called from
249 :param force_password_change: toggles new or existing user flag
250 :param force_password_change: toggles new or existing user flag
250 for password change
251 for password change
251 :param allow_to_create_user: Defines if the method can actually create
252 :param allow_to_create_user: Defines if the method can actually create
252 new users
253 new users
253 :param create_repo_group: Defines if the method should also
254 :param create_repo_group: Defines if the method should also
254 create an repo group with user name, and owner
255 create an repo group with user name, and owner
255 :param updating_user_id: if we set it up this is the user we want to
256 :param updating_user_id: if we set it up this is the user we want to
256 update this allows to editing username.
257 update this allows to editing username.
257 :param language: language of user from interface.
258 :param language: language of user from interface.
258 :param description: user description
259 :param description: user description
259 :param strict_creation_check: checks for allowed creation license wise etc.
260 :param strict_creation_check: checks for allowed creation license wise etc.
260
261
261 :returns: new User object with injected `is_new_user` attribute.
262 :returns: new User object with injected `is_new_user` attribute.
262 """
263 """
263
264
264 if not cur_user:
265 if not cur_user:
265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
266 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
266
267
267 from rhodecode.lib.auth import (
268 from rhodecode.lib.auth import (
268 get_crypt_password, check_password)
269 get_crypt_password, check_password)
269 from rhodecode.lib import hooks_base
270 from rhodecode.lib import hooks_base
270
271
271 def _password_change(new_user, password):
272 def _password_change(new_user, password):
272 old_password = new_user.password or ''
273 old_password = new_user.password or ''
273 # empty password
274 # empty password
274 if not old_password:
275 if not old_password:
275 return False
276 return False
276
277
277 # password check is only needed for RhodeCode internal auth calls
278 # password check is only needed for RhodeCode internal auth calls
278 # in case it's a plugin we don't care
279 # in case it's a plugin we don't care
279 if not plugin:
280 if not plugin:
280
281
281 # first check if we gave crypted password back, and if it
282 # first check if we gave crypted password back, and if it
282 # matches it's not password change
283 # matches it's not password change
283 if new_user.password == password:
284 if new_user.password == password:
284 return False
285 return False
285
286
286 password_match = check_password(password, old_password)
287 password_match = check_password(password, old_password)
287 if not password_match:
288 if not password_match:
288 return True
289 return True
289
290
290 return False
291 return False
291
292
292 # read settings on default personal repo group creation
293 # read settings on default personal repo group creation
293 if create_repo_group is None:
294 if create_repo_group is None:
294 default_create_repo_group = RepoGroupModel()\
295 default_create_repo_group = RepoGroupModel()\
295 .get_default_create_personal_repo_group()
296 .get_default_create_personal_repo_group()
296 create_repo_group = default_create_repo_group
297 create_repo_group = default_create_repo_group
297
298
298 user_data = {
299 user_data = {
299 'username': username,
300 'username': username,
300 'password': password,
301 'password': password,
301 'email': email,
302 'email': email,
302 'firstname': firstname,
303 'firstname': firstname,
303 'lastname': lastname,
304 'lastname': lastname,
304 'active': active,
305 'active': active,
305 'admin': admin
306 'admin': admin
306 }
307 }
307
308
308 if updating_user_id:
309 if updating_user_id:
309 log.debug('Checking for existing account in RhodeCode '
310 log.debug('Checking for existing account in RhodeCode '
310 'database with user_id `%s` ', updating_user_id)
311 'database with user_id `%s` ', updating_user_id)
311 user = User.get(updating_user_id)
312 user = User.get(updating_user_id)
312 # now also validate if USERNAME belongs to potentially other user
313 # now also validate if USERNAME belongs to potentially other user
313 maybe_other_user = User.get_by_username(username, case_insensitive=True)
314 maybe_other_user = User.get_by_username(username, case_insensitive=True)
314 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
315 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
315 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
316 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
316 else:
317 else:
317 log.debug('Checking for existing account in RhodeCode '
318 log.debug('Checking for existing account in RhodeCode '
318 'database with username `%s` ', username)
319 'database with username `%s` ', username)
319 user = User.get_by_username(username, case_insensitive=True)
320 user = User.get_by_username(username, case_insensitive=True)
320
321
321 if user is None:
322 if user is None:
322 # we check internal flag if this method is actually allowed to
323 # we check internal flag if this method is actually allowed to
323 # create new user
324 # create new user
324 if not allow_to_create_user:
325 if not allow_to_create_user:
325 msg = ('Method wants to create new user, but it is not '
326 msg = ('Method wants to create new user, but it is not '
326 'allowed to do so')
327 'allowed to do so')
327 log.warning(msg)
328 log.warning(msg)
328 raise NotAllowedToCreateUserError(msg)
329 raise NotAllowedToCreateUserError(msg)
329
330
330 log.debug('Creating new user %s', username)
331 log.debug('Creating new user %s', username)
331
332
332 # only if we create user that is active
333 # only if we create user that is active
333 new_active_user = active
334 new_active_user = active
334 if new_active_user and strict_creation_check:
335 if new_active_user and strict_creation_check:
335 # raises UserCreationError if it's not allowed for any reason to
336 # raises UserCreationError if it's not allowed for any reason to
336 # create new active user, this also executes pre-create hooks
337 # create new active user, this also executes pre-create hooks
337 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
338 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
338 events.trigger(events.UserPreCreate(user_data))
339 events.trigger(events.UserPreCreate(user_data))
339 new_user = User()
340 new_user = User()
340 edit = False
341 edit = False
341 else:
342 else:
342 log.debug('updating user `%s`', username)
343 log.debug('updating user `%s`', username)
343 events.trigger(events.UserPreUpdate(user, user_data))
344 events.trigger(events.UserPreUpdate(user, user_data))
344 new_user = user
345 new_user = user
345 edit = True
346 edit = True
346
347
347 # we're not allowed to edit default user
348 # we're not allowed to edit default user
348 if user.username == User.DEFAULT_USER:
349 if user.username == User.DEFAULT_USER:
349 raise DefaultUserException(
350 raise DefaultUserException(
350 "You can't edit this user (`%(username)s`) since it's "
351 "You can't edit this user (`%(username)s`) since it's "
351 "crucial for entire application"
352 "crucial for entire application"
352 % {'username': user.username})
353 % {'username': user.username})
353
354
354 # inject special attribute that will tell us if User is new or old
355 # inject special attribute that will tell us if User is new or old
355 new_user.is_new_user = not edit
356 new_user.is_new_user = not edit
356 # for users that didn's specify auth type, we use RhodeCode built in
357 # for users that didn's specify auth type, we use RhodeCode built in
357 from rhodecode.authentication.plugins import auth_rhodecode
358 from rhodecode.authentication.plugins import auth_rhodecode
358 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
359 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
359 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
360 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
360
361
361 try:
362 try:
362 new_user.username = username
363 new_user.username = username
363 new_user.admin = admin
364 new_user.admin = admin
364 new_user.email = email
365 new_user.email = email
365 new_user.active = active
366 new_user.active = active
366 new_user.extern_name = safe_str(extern_name)
367 new_user.extern_name = safe_str(extern_name)
367 new_user.extern_type = safe_str(extern_type)
368 new_user.extern_type = safe_str(extern_type)
368 new_user.name = firstname
369 new_user.name = firstname
369 new_user.lastname = lastname
370 new_user.lastname = lastname
370 new_user.description = description
371 new_user.description = description
371
372
372 # set password only if creating an user or password is changed
373 # set password only if creating an user or password is changed
373 if not edit or _password_change(new_user, password):
374 if not edit or _password_change(new_user, password):
374 reason = 'new password' if edit else 'new user'
375 reason = 'new password' if edit else 'new user'
375 log.debug('Updating password reason=>%s', reason)
376 log.debug('Updating password reason=>%s', reason)
376 new_user.password = get_crypt_password(password) if password else None
377 new_user.password = get_crypt_password(password) if password else None
377
378
378 if force_password_change:
379 if force_password_change:
379 new_user.update_userdata(force_password_change=True)
380 new_user.update_userdata(force_password_change=True)
380 if language:
381 if language:
381 new_user.update_userdata(language=language)
382 new_user.update_userdata(language=language)
382 new_user.update_userdata(notification_status=True)
383 new_user.update_userdata(notification_status=True)
383
384
384 self.sa.add(new_user)
385 self.sa.add(new_user)
385
386
386 if not edit and create_repo_group:
387 if not edit and create_repo_group:
387 RepoGroupModel().create_personal_repo_group(
388 RepoGroupModel().create_personal_repo_group(
388 new_user, commit_early=False)
389 new_user, commit_early=False)
389
390
390 if not edit:
391 if not edit:
391 # add the RSS token
392 # add the RSS token
392 self.add_auth_token(
393 self.add_auth_token(
393 user=username, lifetime_minutes=-1,
394 user=username, lifetime_minutes=-1,
394 role=self.auth_token_role.ROLE_FEED,
395 role=self.auth_token_role.ROLE_FEED,
395 description='Generated feed token')
396 description='Generated feed token')
396
397
397 kwargs = new_user.get_dict()
398 kwargs = new_user.get_dict()
398 # backward compat, require api_keys present
399 # backward compat, require api_keys present
399 kwargs['api_keys'] = kwargs['auth_tokens']
400 kwargs['api_keys'] = kwargs['auth_tokens']
400 hooks_base.create_user(created_by=cur_user, **kwargs)
401 hooks_base.create_user(created_by=cur_user, **kwargs)
401 events.trigger(events.UserPostCreate(user_data))
402 events.trigger(events.UserPostCreate(user_data))
402 return new_user
403 return new_user
403 except (DatabaseError,):
404 except (DatabaseError,):
404 log.error(traceback.format_exc())
405 log.error(traceback.format_exc())
405 raise
406 raise
406
407
407 def create_registration(self, form_data,
408 def create_registration(self, form_data,
408 extern_name='rhodecode', extern_type='rhodecode'):
409 extern_name='rhodecode', extern_type='rhodecode'):
409 from rhodecode.model.notification import NotificationModel
410 from rhodecode.model.notification import NotificationModel
410 from rhodecode.model.notification import EmailNotificationModel
411 from rhodecode.model.notification import EmailNotificationModel
411
412
412 try:
413 try:
413 form_data['admin'] = False
414 form_data['admin'] = False
414 form_data['extern_name'] = extern_name
415 form_data['extern_name'] = extern_name
415 form_data['extern_type'] = extern_type
416 form_data['extern_type'] = extern_type
416 new_user = self.create(form_data)
417 new_user = self.create(form_data)
417
418
418 self.sa.add(new_user)
419 self.sa.add(new_user)
419 self.sa.flush()
420 self.sa.flush()
420
421
421 user_data = new_user.get_dict()
422 user_data = new_user.get_dict()
422 user_data.update({
423 user_data.update({
423 'first_name': user_data.get('firstname'),
424 'first_name': user_data.get('firstname'),
424 'last_name': user_data.get('lastname'),
425 'last_name': user_data.get('lastname'),
425 })
426 })
426 kwargs = {
427 kwargs = {
427 # use SQLALCHEMY safe dump of user data
428 # use SQLALCHEMY safe dump of user data
428 'user': AttributeDict(user_data),
429 'user': AttributeDict(user_data),
429 'date': datetime.datetime.now()
430 'date': datetime.datetime.now()
430 }
431 }
431 notification_type = EmailNotificationModel.TYPE_REGISTRATION
432 notification_type = EmailNotificationModel.TYPE_REGISTRATION
432
433
433 # create notification objects, and emails
434 # create notification objects, and emails
434 NotificationModel().create(
435 NotificationModel().create(
435 created_by=new_user,
436 created_by=new_user,
436 notification_subject='', # Filled in based on the notification_type
437 notification_subject='', # Filled in based on the notification_type
437 notification_body='', # Filled in based on the notification_type
438 notification_body='', # Filled in based on the notification_type
438 notification_type=notification_type,
439 notification_type=notification_type,
439 recipients=None, # all admins
440 recipients=None, # all admins
440 email_kwargs=kwargs,
441 email_kwargs=kwargs,
441 )
442 )
442
443
443 return new_user
444 return new_user
444 except Exception:
445 except Exception:
445 log.error(traceback.format_exc())
446 log.error(traceback.format_exc())
446 raise
447 raise
447
448
448 def _handle_user_repos(self, username, repositories, handle_user,
449 def _handle_user_repos(self, username, repositories, handle_user,
449 handle_mode=None):
450 handle_mode=None):
450
451
451 left_overs = True
452 left_overs = True
452
453
453 from rhodecode.model.repo import RepoModel
454 from rhodecode.model.repo import RepoModel
454
455
455 if handle_mode == 'detach':
456 if handle_mode == 'detach':
456 for obj in repositories:
457 for obj in repositories:
457 obj.user = handle_user
458 obj.user = handle_user
458 # set description we know why we super admin now owns
459 # set description we know why we super admin now owns
459 # additional repositories that were orphaned !
460 # additional repositories that were orphaned !
460 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
461 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
461 self.sa.add(obj)
462 self.sa.add(obj)
462 left_overs = False
463 left_overs = False
463 elif handle_mode == 'delete':
464 elif handle_mode == 'delete':
464 for obj in repositories:
465 for obj in repositories:
465 RepoModel().delete(obj, forks='detach')
466 RepoModel().delete(obj, forks='detach')
466 left_overs = False
467 left_overs = False
467
468
468 # if nothing is done we have left overs left
469 # if nothing is done we have left overs left
469 return left_overs
470 return left_overs
470
471
471 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
472 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
472 handle_mode=None):
473 handle_mode=None):
473
474
474 left_overs = True
475 left_overs = True
475
476
476 from rhodecode.model.repo_group import RepoGroupModel
477 from rhodecode.model.repo_group import RepoGroupModel
477
478
478 if handle_mode == 'detach':
479 if handle_mode == 'detach':
479 for r in repository_groups:
480 for r in repository_groups:
480 r.user = handle_user
481 r.user = handle_user
481 # set description we know why we super admin now owns
482 # set description we know why we super admin now owns
482 # additional repositories that were orphaned !
483 # additional repositories that were orphaned !
483 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
484 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
484 r.personal = False
485 r.personal = False
485 self.sa.add(r)
486 self.sa.add(r)
486 left_overs = False
487 left_overs = False
487 elif handle_mode == 'delete':
488 elif handle_mode == 'delete':
488 for r in repository_groups:
489 for r in repository_groups:
489 RepoGroupModel().delete(r)
490 RepoGroupModel().delete(r)
490 left_overs = False
491 left_overs = False
491
492
492 # if nothing is done we have left overs left
493 # if nothing is done we have left overs left
493 return left_overs
494 return left_overs
494
495
495 def _handle_user_user_groups(self, username, user_groups, handle_user,
496 def _handle_user_user_groups(self, username, user_groups, handle_user,
496 handle_mode=None):
497 handle_mode=None):
497
498
498 left_overs = True
499 left_overs = True
499
500
500 from rhodecode.model.user_group import UserGroupModel
501 from rhodecode.model.user_group import UserGroupModel
501
502
502 if handle_mode == 'detach':
503 if handle_mode == 'detach':
503 for r in user_groups:
504 for r in user_groups:
504 for user_user_group_to_perm in r.user_user_group_to_perm:
505 for user_user_group_to_perm in r.user_user_group_to_perm:
505 if user_user_group_to_perm.user.username == username:
506 if user_user_group_to_perm.user.username == username:
506 user_user_group_to_perm.user = handle_user
507 user_user_group_to_perm.user = handle_user
507 r.user = handle_user
508 r.user = handle_user
508 # set description we know why we super admin now owns
509 # set description we know why we super admin now owns
509 # additional repositories that were orphaned !
510 # additional repositories that were orphaned !
510 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
511 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
511 self.sa.add(r)
512 self.sa.add(r)
512 left_overs = False
513 left_overs = False
513 elif handle_mode == 'delete':
514 elif handle_mode == 'delete':
514 for r in user_groups:
515 for r in user_groups:
515 UserGroupModel().delete(r)
516 UserGroupModel().delete(r)
516 left_overs = False
517 left_overs = False
517
518
518 # if nothing is done we have left overs left
519 # if nothing is done we have left overs left
519 return left_overs
520 return left_overs
520
521
521 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
522 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
522 handle_mode=None):
523 handle_mode=None):
523 left_overs = True
524 left_overs = True
524
525
525 from rhodecode.model.pull_request import PullRequestModel
526 from rhodecode.model.pull_request import PullRequestModel
526
527
527 if handle_mode == 'detach':
528 if handle_mode == 'detach':
528 for pr in pull_requests:
529 for pr in pull_requests:
529 pr.user_id = handle_user.user_id
530 pr.user_id = handle_user.user_id
530 # set description we know why we super admin now owns
531 # set description we know why we super admin now owns
531 # additional repositories that were orphaned !
532 # additional repositories that were orphaned !
532 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
533 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
533 self.sa.add(pr)
534 self.sa.add(pr)
534 left_overs = False
535 left_overs = False
535 elif handle_mode == 'delete':
536 elif handle_mode == 'delete':
536 for pr in pull_requests:
537 for pr in pull_requests:
537 PullRequestModel().delete(pr)
538 PullRequestModel().delete(pr)
538
539
539 left_overs = False
540 left_overs = False
540
541
541 # if nothing is done we have leftovers left
542 # if nothing is done we have leftovers left
542 return left_overs
543 return left_overs
543
544
544 def _handle_user_artifacts(self, username, artifacts, handle_user,
545 def _handle_user_artifacts(self, username, artifacts, handle_user,
545 handle_mode=None):
546 handle_mode=None):
546
547
547 left_overs = True
548 left_overs = True
548
549
549 if handle_mode == 'detach':
550 if handle_mode == 'detach':
550 for a in artifacts:
551 for a in artifacts:
551 a.upload_user = handle_user
552 a.upload_user = handle_user
552 # set description we know why we super admin now owns
553 # set description we know why we super admin now owns
553 # additional artifacts that were orphaned !
554 # additional artifacts that were orphaned !
554 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
555 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
555 self.sa.add(a)
556 self.sa.add(a)
556 left_overs = False
557 left_overs = False
557 elif handle_mode == 'delete':
558 elif handle_mode == 'delete':
558 from rhodecode.apps.file_store import utils as store_utils
559 from rhodecode.apps.file_store import utils as store_utils
559 request = get_current_request()
560 request = get_current_request()
560 f_store = store_utils.get_filestore_backend(request.registry.settings)
561 f_store = store_utils.get_filestore_backend(request.registry.settings)
561 for a in artifacts:
562 for a in artifacts:
562 file_uid = a.file_uid
563 file_uid = a.file_uid
563 f_store.delete(file_uid)
564 f_store.delete(file_uid)
564 self.sa.delete(a)
565 self.sa.delete(a)
565
566
566 left_overs = False
567 left_overs = False
567
568
568 # if nothing is done we have left overs left
569 # if nothing is done we have left overs left
569 return left_overs
570 return left_overs
570
571
571 def delete(self, user, cur_user=None, handle_repos=None,
572 def delete(self, user, cur_user=None, handle_repos=None,
572 handle_repo_groups=None, handle_user_groups=None,
573 handle_repo_groups=None, handle_user_groups=None,
573 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
574 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
574 from rhodecode.lib import hooks_base
575 from rhodecode.lib import hooks_base
575
576
576 if not cur_user:
577 if not cur_user:
577 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
578 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
578
579
579 user = self._get_user(user)
580 user = self._get_user(user)
580
581
581 try:
582 try:
582 if user.username == User.DEFAULT_USER:
583 if user.username == User.DEFAULT_USER:
583 raise DefaultUserException(
584 raise DefaultUserException(
584 "You can't remove this user since it's"
585 "You can't remove this user since it's"
585 " crucial for entire application")
586 " crucial for entire application")
586 handle_user = handle_new_owner or self.cls.get_first_super_admin()
587 handle_user = handle_new_owner or self.cls.get_first_super_admin()
587 log.debug('New detached objects owner %s', handle_user)
588 log.debug('New detached objects owner %s', handle_user)
588
589
589 left_overs = self._handle_user_repos(
590 left_overs = self._handle_user_repos(
590 user.username, user.repositories, handle_user, handle_repos)
591 user.username, user.repositories, handle_user, handle_repos)
591 if left_overs and user.repositories:
592 if left_overs and user.repositories:
592 repos = [x.repo_name for x in user.repositories]
593 repos = [x.repo_name for x in user.repositories]
593 raise UserOwnsReposException(
594 raise UserOwnsReposException(
594 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
595 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
595 'removed. Switch owners or remove those repositories:%(list_repos)s'
596 'removed. Switch owners or remove those repositories:%(list_repos)s'
596 % {'username': user.username, 'len_repos': len(repos),
597 % {'username': user.username, 'len_repos': len(repos),
597 'list_repos': ', '.join(repos)})
598 'list_repos': ', '.join(repos)})
598
599
599 left_overs = self._handle_user_repo_groups(
600 left_overs = self._handle_user_repo_groups(
600 user.username, user.repository_groups, handle_user, handle_repo_groups)
601 user.username, user.repository_groups, handle_user, handle_repo_groups)
601 if left_overs and user.repository_groups:
602 if left_overs and user.repository_groups:
602 repo_groups = [x.group_name for x in user.repository_groups]
603 repo_groups = [x.group_name for x in user.repository_groups]
603 raise UserOwnsRepoGroupsException(
604 raise UserOwnsRepoGroupsException(
604 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
605 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
605 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
606 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
606 % {'username': user.username, 'len_repo_groups': len(repo_groups),
607 % {'username': user.username, 'len_repo_groups': len(repo_groups),
607 'list_repo_groups': ', '.join(repo_groups)})
608 'list_repo_groups': ', '.join(repo_groups)})
608
609
609 left_overs = self._handle_user_user_groups(
610 left_overs = self._handle_user_user_groups(
610 user.username, user.user_groups, handle_user, handle_user_groups)
611 user.username, user.user_groups, handle_user, handle_user_groups)
611 if left_overs and user.user_groups:
612 if left_overs and user.user_groups:
612 user_groups = [x.users_group_name for x in user.user_groups]
613 user_groups = [x.users_group_name for x in user.user_groups]
613 raise UserOwnsUserGroupsException(
614 raise UserOwnsUserGroupsException(
614 'user "%s" still owns %s user groups and cannot be '
615 'user "%s" still owns %s user groups and cannot be '
615 'removed. Switch owners or remove those user groups:%s'
616 'removed. Switch owners or remove those user groups:%s'
616 % (user.username, len(user_groups), ', '.join(user_groups)))
617 % (user.username, len(user_groups), ', '.join(user_groups)))
617
618
618 left_overs = self._handle_user_pull_requests(
619 left_overs = self._handle_user_pull_requests(
619 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
620 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
620 if left_overs and user.user_pull_requests:
621 if left_overs and user.user_pull_requests:
621 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
622 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
622 raise UserOwnsPullRequestsException(
623 raise UserOwnsPullRequestsException(
623 'user "%s" still owns %s pull requests and cannot be '
624 'user "%s" still owns %s pull requests and cannot be '
624 'removed. Switch owners or remove those pull requests:%s'
625 'removed. Switch owners or remove those pull requests:%s'
625 % (user.username, len(pull_requests), ', '.join(pull_requests)))
626 % (user.username, len(pull_requests), ', '.join(pull_requests)))
626
627
627 left_overs = self._handle_user_artifacts(
628 left_overs = self._handle_user_artifacts(
628 user.username, user.artifacts, handle_user, handle_artifacts)
629 user.username, user.artifacts, handle_user, handle_artifacts)
629 if left_overs and user.artifacts:
630 if left_overs and user.artifacts:
630 artifacts = [x.file_uid for x in user.artifacts]
631 artifacts = [x.file_uid for x in user.artifacts]
631 raise UserOwnsArtifactsException(
632 raise UserOwnsArtifactsException(
632 'user "%s" still owns %s artifacts and cannot be '
633 'user "%s" still owns %s artifacts and cannot be '
633 'removed. Switch owners or remove those artifacts:%s'
634 'removed. Switch owners or remove those artifacts:%s'
634 % (user.username, len(artifacts), ', '.join(artifacts)))
635 % (user.username, len(artifacts), ', '.join(artifacts)))
635
636
636 user_data = user.get_dict() # fetch user data before expire
637 user_data = user.get_dict() # fetch user data before expire
637
638
638 # we might change the user data with detach/delete, make sure
639 # we might change the user data with detach/delete, make sure
639 # the object is marked as expired before actually deleting !
640 # the object is marked as expired before actually deleting !
640 self.sa.expire(user)
641 self.sa.expire(user)
641 self.sa.delete(user)
642 self.sa.delete(user)
642
643
643 hooks_base.delete_user(deleted_by=cur_user, **user_data)
644 hooks_base.delete_user(deleted_by=cur_user, **user_data)
644 except Exception:
645 except Exception:
645 log.error(traceback.format_exc())
646 log.error(traceback.format_exc())
646 raise
647 raise
647
648
648 def reset_password_link(self, data, pwd_reset_url):
649 def reset_password_link(self, data, pwd_reset_url):
649 from rhodecode.lib.celerylib import tasks, run_task
650 from rhodecode.lib.celerylib import tasks, run_task
650 from rhodecode.model.notification import EmailNotificationModel
651 from rhodecode.model.notification import EmailNotificationModel
651 user_email = data['email']
652 user_email = data['email']
652 try:
653 try:
653 user = User.get_by_email(user_email)
654 user = User.get_by_email(user_email)
654 if user:
655 if user:
655 log.debug('password reset user found %s', user)
656 log.debug('password reset user found %s', user)
656
657
657 email_kwargs = {
658 email_kwargs = {
658 'password_reset_url': pwd_reset_url,
659 'password_reset_url': pwd_reset_url,
659 'user': user,
660 'user': user,
660 'email': user_email,
661 'email': user_email,
661 'date': datetime.datetime.now(),
662 'date': datetime.datetime.now(),
662 'first_admin_email': User.get_first_super_admin().email
663 'first_admin_email': User.get_first_super_admin().email
663 }
664 }
664
665
665 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
666 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
666 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
667 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
667
668
668 recipients = [user_email]
669 recipients = [user_email]
669
670
670 action_logger_generic(
671 action_logger_generic(
671 'sending password reset email to user: {}'.format(
672 'sending password reset email to user: {}'.format(
672 user), namespace='security.password_reset')
673 user), namespace='security.password_reset')
673
674
674 run_task(tasks.send_email, recipients, subject,
675 run_task(tasks.send_email, recipients, subject,
675 email_body_plaintext, email_body)
676 email_body_plaintext, email_body)
676
677
677 else:
678 else:
678 log.debug("password reset email %s not found", user_email)
679 log.debug("password reset email %s not found", user_email)
679 except Exception:
680 except Exception:
680 log.error(traceback.format_exc())
681 log.error(traceback.format_exc())
681 return False
682 return False
682
683
683 return True
684 return True
684
685
685 def reset_password(self, data):
686 def reset_password(self, data):
686 from rhodecode.lib.celerylib import tasks, run_task
687 from rhodecode.lib.celerylib import tasks, run_task
687 from rhodecode.model.notification import EmailNotificationModel
688 from rhodecode.model.notification import EmailNotificationModel
688 from rhodecode.lib import auth
689 from rhodecode.lib import auth
689 user_email = data['email']
690 user_email = data['email']
690 pre_db = True
691 pre_db = True
691 try:
692 try:
692 user = User.get_by_email(user_email)
693 user = User.get_by_email(user_email)
693 new_passwd = auth.PasswordGenerator().gen_password(
694 new_passwd = auth.PasswordGenerator().gen_password(
694 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
695 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
695 if user:
696 if user:
696 user.password = auth.get_crypt_password(new_passwd)
697 user.password = auth.get_crypt_password(new_passwd)
697 # also force this user to reset his password !
698 # also force this user to reset his password !
698 user.update_userdata(force_password_change=True)
699 user.update_userdata(force_password_change=True)
699
700
700 Session().add(user)
701 Session().add(user)
701
702
702 # now delete the token in question
703 # now delete the token in question
703 UserApiKeys = AuthTokenModel.cls
704 UserApiKeys = AuthTokenModel.cls
704 UserApiKeys().query().filter(
705 UserApiKeys().query().filter(
705 UserApiKeys.api_key == data['token']).delete()
706 UserApiKeys.api_key == data['token']).delete()
706
707
707 Session().commit()
708 Session().commit()
708 log.info('successfully reset password for `%s`', user_email)
709 log.info('successfully reset password for `%s`', user_email)
709
710
710 if new_passwd is None:
711 if new_passwd is None:
711 raise Exception('unable to generate new password')
712 raise Exception('unable to generate new password')
712
713
713 pre_db = False
714 pre_db = False
714
715
715 email_kwargs = {
716 email_kwargs = {
716 'new_password': new_passwd,
717 'new_password': new_passwd,
717 'user': user,
718 'user': user,
718 'email': user_email,
719 'email': user_email,
719 'date': datetime.datetime.now(),
720 'date': datetime.datetime.now(),
720 'first_admin_email': User.get_first_super_admin().email
721 'first_admin_email': User.get_first_super_admin().email
721 }
722 }
722
723
723 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
724 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 **email_kwargs)
726 **email_kwargs)
726
727
727 recipients = [user_email]
728 recipients = [user_email]
728
729
729 action_logger_generic(
730 action_logger_generic(
730 'sent new password to user: {} with email: {}'.format(
731 'sent new password to user: {} with email: {}'.format(
731 user, user_email), namespace='security.password_reset')
732 user, user_email), namespace='security.password_reset')
732
733
733 run_task(tasks.send_email, recipients, subject,
734 run_task(tasks.send_email, recipients, subject,
734 email_body_plaintext, email_body)
735 email_body_plaintext, email_body)
735
736
736 except Exception:
737 except Exception:
737 log.error('Failed to update user password')
738 log.error('Failed to update user password')
738 log.error(traceback.format_exc())
739 log.error(traceback.format_exc())
739 if pre_db:
740 if pre_db:
740 # we rollback only if local db stuff fails. If it goes into
741 # we rollback only if local db stuff fails. If it goes into
741 # run_task, we're pass rollback state this wouldn't work then
742 # run_task, we're pass rollback state this wouldn't work then
742 Session().rollback()
743 Session().rollback()
743
744
744 return True
745 return True
745
746
746 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
747 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
747 """
748 """
748 Fetches auth_user by user_id,or api_key if present.
749 Fetches auth_user by user_id,or api_key if present.
749 Fills auth_user attributes with those taken from database.
750 Fills auth_user attributes with those taken from database.
750 Additionally set's is_authenitated if lookup fails
751 Additionally set's is_authenitated if lookup fails
751 present in database
752 present in database
752
753
753 :param auth_user: instance of user to set attributes
754 :param auth_user: instance of user to set attributes
754 :param user_id: user id to fetch by
755 :param user_id: user id to fetch by
755 :param api_key: api key to fetch by
756 :param api_key: api key to fetch by
756 :param username: username to fetch by
757 :param username: username to fetch by
757 """
758 """
758 def token_obfuscate(token):
759 def token_obfuscate(token):
759 if token:
760 if token:
760 return token[:4] + "****"
761 return token[:4] + "****"
761
762
762 if user_id is None and api_key is None and username is None:
763 if user_id is None and api_key is None and username is None:
763 raise Exception('You need to pass user_id, api_key or username')
764 raise Exception('You need to pass user_id, api_key or username')
764
765
765 log.debug(
766 log.debug(
766 'AuthUser: fill data execution based on: '
767 'AuthUser: fill data execution based on: '
767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
768 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
768 try:
769 try:
769 found_with = ''
770 found_with = ''
770 dbuser = None
771 dbuser = None
771 if user_id:
772 if user_id:
772 dbuser = self.get(user_id)
773 dbuser = self.get(user_id)
773 found_with = 'user_id'
774 found_with = 'user_id'
774 elif api_key:
775 elif api_key:
775 dbuser = self.get_by_auth_token(api_key)
776 dbuser = self.get_by_auth_token(api_key)
776 found_with = 'auth_token'
777 found_with = 'auth_token'
777 elif username:
778 elif username:
778 dbuser = self.get_by_username(username)
779 dbuser = self.get_by_username(username)
779 found_with = 'username'
780 found_with = 'username'
780
781
781 if not dbuser:
782 if not dbuser:
782 log.warning(
783 log.warning(
783 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
784 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
784 user_id, token_obfuscate(api_key), username, found_with)
785 user_id, token_obfuscate(api_key), username, found_with)
785 return False
786 return False
786 if not dbuser.active:
787 if not dbuser.active:
787 log.debug('User `%s:%s` is inactive, skipping fill data',
788 log.debug('User `%s:%s` is inactive, skipping fill data',
788 username, user_id)
789 username, user_id)
789 return False
790 return False
790
791
791 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
792 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
792
793
793 attrs = {
794 attrs = {
794 'user_id': dbuser.user_id,
795 'user_id': dbuser.user_id,
795 'username': dbuser.username,
796 'username': dbuser.username,
796 'name': dbuser.name,
797 'name': dbuser.name,
797 'first_name': dbuser.first_name,
798 'first_name': dbuser.first_name,
798 'firstname': dbuser.firstname,
799 'firstname': dbuser.firstname,
799 'last_name': dbuser.last_name,
800 'last_name': dbuser.last_name,
800 'lastname': dbuser.lastname,
801 'lastname': dbuser.lastname,
801 'admin': dbuser.admin,
802 'admin': dbuser.admin,
802 'active': dbuser.active,
803 'active': dbuser.active,
803
804
804 'email': dbuser.email,
805 'email': dbuser.email,
805 'emails': dbuser.emails_cached(),
806 'emails': dbuser.emails_cached(),
806 'short_contact': dbuser.short_contact,
807 'short_contact': dbuser.short_contact,
807 'full_contact': dbuser.full_contact,
808 'full_contact': dbuser.full_contact,
808 'full_name': dbuser.full_name,
809 'full_name': dbuser.full_name,
809 'full_name_or_username': dbuser.full_name_or_username,
810 'full_name_or_username': dbuser.full_name_or_username,
810
811
811 '_api_key': dbuser._api_key,
812 '_api_key': dbuser._api_key,
812 '_user_data': dbuser._user_data,
813 '_user_data': dbuser._user_data,
813
814
814 'created_on': dbuser.created_on,
815 'created_on': dbuser.created_on,
815 'extern_name': dbuser.extern_name,
816 'extern_name': dbuser.extern_name,
816 'extern_type': dbuser.extern_type,
817 'extern_type': dbuser.extern_type,
817
818
818 'inherit_default_permissions': dbuser.inherit_default_permissions,
819 'inherit_default_permissions': dbuser.inherit_default_permissions,
819
820
820 'language': dbuser.language,
821 'language': dbuser.language,
821 'last_activity': dbuser.last_activity,
822 'last_activity': dbuser.last_activity,
822 'last_login': dbuser.last_login,
823 'last_login': dbuser.last_login,
823 'password': dbuser.password,
824 'password': dbuser.password,
824 }
825 }
825 auth_user.__dict__.update(attrs)
826 auth_user.__dict__.update(attrs)
826 except Exception:
827 except Exception:
827 log.error(traceback.format_exc())
828 log.error(traceback.format_exc())
828 auth_user.is_authenticated = False
829 auth_user.is_authenticated = False
829 return False
830 return False
830
831
831 return True
832 return True
832
833
833 def has_perm(self, user, perm):
834 def has_perm(self, user, perm):
834 perm = self._get_perm(perm)
835 perm = self._get_perm(perm)
835 user = self._get_user(user)
836 user = self._get_user(user)
836
837
837 return UserToPerm.query().filter(UserToPerm.user == user)\
838 return UserToPerm.query().filter(UserToPerm.user == user)\
838 .filter(UserToPerm.permission == perm).scalar() is not None
839 .filter(UserToPerm.permission == perm).scalar() is not None
839
840
840 def grant_perm(self, user, perm):
841 def grant_perm(self, user, perm):
841 """
842 """
842 Grant user global permissions
843 Grant user global permissions
843
844
844 :param user:
845 :param user:
845 :param perm:
846 :param perm:
846 """
847 """
847 user = self._get_user(user)
848 user = self._get_user(user)
848 perm = self._get_perm(perm)
849 perm = self._get_perm(perm)
849 # if this permission is already granted skip it
850 # if this permission is already granted skip it
850 _perm = UserToPerm.query()\
851 _perm = UserToPerm.query()\
851 .filter(UserToPerm.user == user)\
852 .filter(UserToPerm.user == user)\
852 .filter(UserToPerm.permission == perm)\
853 .filter(UserToPerm.permission == perm)\
853 .scalar()
854 .scalar()
854 if _perm:
855 if _perm:
855 return
856 return
856 new = UserToPerm()
857 new = UserToPerm()
857 new.user = user
858 new.user = user
858 new.permission = perm
859 new.permission = perm
859 self.sa.add(new)
860 self.sa.add(new)
860 return new
861 return new
861
862
862 def revoke_perm(self, user, perm):
863 def revoke_perm(self, user, perm):
863 """
864 """
864 Revoke users global permissions
865 Revoke users global permissions
865
866
866 :param user:
867 :param user:
867 :param perm:
868 :param perm:
868 """
869 """
869 user = self._get_user(user)
870 user = self._get_user(user)
870 perm = self._get_perm(perm)
871 perm = self._get_perm(perm)
871
872
872 obj = UserToPerm.query()\
873 obj = UserToPerm.query()\
873 .filter(UserToPerm.user == user)\
874 .filter(UserToPerm.user == user)\
874 .filter(UserToPerm.permission == perm)\
875 .filter(UserToPerm.permission == perm)\
875 .scalar()
876 .scalar()
876 if obj:
877 if obj:
877 self.sa.delete(obj)
878 self.sa.delete(obj)
878
879
879 def add_extra_email(self, user, email):
880 def add_extra_email(self, user, email):
880 """
881 """
881 Adds email address to UserEmailMap
882 Adds email address to UserEmailMap
882
883
883 :param user:
884 :param user:
884 :param email:
885 :param email:
885 """
886 """
886
887
887 user = self._get_user(user)
888 user = self._get_user(user)
888
889
889 obj = UserEmailMap()
890 obj = UserEmailMap()
890 obj.user = user
891 obj.user = user
891 obj.email = email
892 obj.email = email
892 self.sa.add(obj)
893 self.sa.add(obj)
893 return obj
894 return obj
894
895
895 def delete_extra_email(self, user, email_id):
896 def delete_extra_email(self, user, email_id):
896 """
897 """
897 Removes email address from UserEmailMap
898 Removes email address from UserEmailMap
898
899
899 :param user:
900 :param user:
900 :param email_id:
901 :param email_id:
901 """
902 """
902 user = self._get_user(user)
903 user = self._get_user(user)
903 obj = UserEmailMap.query().get(email_id)
904 obj = UserEmailMap.query().get(email_id)
904 if obj and obj.user_id == user.user_id:
905 if obj and obj.user_id == user.user_id:
905 self.sa.delete(obj)
906 self.sa.delete(obj)
906
907
907 def parse_ip_range(self, ip_range):
908 def parse_ip_range(self, ip_range):
908 ip_list = []
909 ip_list = []
909
910
910 def make_unique(value):
911 def make_unique(value):
911 seen = []
912 seen = []
912 return [c for c in value if not (c in seen or seen.append(c))]
913 return [c for c in value if not (c in seen or seen.append(c))]
913
914
914 # firsts split by commas
915 # firsts split by commas
915 for ip_range in ip_range.split(','):
916 for ip_range in ip_range.split(','):
916 if not ip_range:
917 if not ip_range:
917 continue
918 continue
918 ip_range = ip_range.strip()
919 ip_range = ip_range.strip()
919 if '-' in ip_range:
920 if '-' in ip_range:
920 start_ip, end_ip = ip_range.split('-', 1)
921 start_ip, end_ip = ip_range.split('-', 1)
921 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
922 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
922 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
923 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
923 parsed_ip_range = []
924 parsed_ip_range = []
924
925
925 for index in range(int(start_ip), int(end_ip) + 1):
926 for index in range(int(start_ip), int(end_ip) + 1):
926 new_ip = ipaddress.ip_address(index)
927 new_ip = ipaddress.ip_address(index)
927 parsed_ip_range.append(str(new_ip))
928 parsed_ip_range.append(str(new_ip))
928 ip_list.extend(parsed_ip_range)
929 ip_list.extend(parsed_ip_range)
929 else:
930 else:
930 ip_list.append(ip_range)
931 ip_list.append(ip_range)
931
932
932 return make_unique(ip_list)
933 return make_unique(ip_list)
933
934
934 def add_extra_ip(self, user, ip, description=None):
935 def add_extra_ip(self, user, ip, description=None):
935 """
936 """
936 Adds ip address to UserIpMap
937 Adds ip address to UserIpMap
937
938
938 :param user:
939 :param user:
939 :param ip:
940 :param ip:
940 """
941 """
941
942
942 user = self._get_user(user)
943 user = self._get_user(user)
943 obj = UserIpMap()
944 obj = UserIpMap()
944 obj.user = user
945 obj.user = user
945 obj.ip_addr = ip
946 obj.ip_addr = ip
946 obj.description = description
947 obj.description = description
947 self.sa.add(obj)
948 self.sa.add(obj)
948 return obj
949 return obj
949
950
950 auth_token_role = AuthTokenModel.cls
951 auth_token_role = AuthTokenModel.cls
951
952
952 def add_auth_token(self, user, lifetime_minutes, role, description='',
953 def add_auth_token(self, user, lifetime_minutes, role, description='',
953 scope_callback=None):
954 scope_callback=None):
954 """
955 """
955 Add AuthToken for user.
956 Add AuthToken for user.
956
957
957 :param user: username/user_id
958 :param user: username/user_id
958 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
959 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
959 :param role: one of AuthTokenModel.cls.ROLE_*
960 :param role: one of AuthTokenModel.cls.ROLE_*
960 :param description: optional string description
961 :param description: optional string description
961 """
962 """
962
963
963 token = AuthTokenModel().create(
964 token = AuthTokenModel().create(
964 user, description, lifetime_minutes, role)
965 user, description, lifetime_minutes, role)
965 if scope_callback and callable(scope_callback):
966 if scope_callback and callable(scope_callback):
966 # call the callback if we provide, used to attach scope for EE edition
967 # call the callback if we provide, used to attach scope for EE edition
967 scope_callback(token)
968 scope_callback(token)
968 return token
969 return token
969
970
970 def delete_extra_ip(self, user, ip_id):
971 def delete_extra_ip(self, user, ip_id):
971 """
972 """
972 Removes ip address from UserIpMap
973 Removes ip address from UserIpMap
973
974
974 :param user:
975 :param user:
975 :param ip_id:
976 :param ip_id:
976 """
977 """
977 user = self._get_user(user)
978 user = self._get_user(user)
978 obj = UserIpMap.query().get(ip_id)
979 obj = UserIpMap.query().get(ip_id)
979 if obj and obj.user_id == user.user_id:
980 if obj and obj.user_id == user.user_id:
980 self.sa.delete(obj)
981 self.sa.delete(obj)
981
982
982 def get_accounts_in_creation_order(self, current_user=None):
983 def get_accounts_in_creation_order(self, current_user=None):
983 """
984 """
984 Get accounts in order of creation for deactivation for license limits
985 Get accounts in order of creation for deactivation for license limits
985
986
986 pick currently logged in user, and append to the list in position 0
987 pick currently logged in user, and append to the list in position 0
987 pick all super-admins in order of creation date and add it to the list
988 pick all super-admins in order of creation date and add it to the list
988 pick all other accounts in order of creation and add it to the list.
989 pick all other accounts in order of creation and add it to the list.
989
990
990 Based on that list, the last accounts can be disabled as they are
991 Based on that list, the last accounts can be disabled as they are
991 created at the end and don't include any of the super admins as well
992 created at the end and don't include any of the super admins as well
992 as the current user.
993 as the current user.
993
994
994 :param current_user: optionally current user running this operation
995 :param current_user: optionally current user running this operation
995 """
996 """
996
997
997 if not current_user:
998 if not current_user:
998 current_user = get_current_rhodecode_user()
999 current_user = get_current_rhodecode_user()
999 active_super_admins = [
1000 active_super_admins = [
1000 x.user_id for x in User.query()
1001 x.user_id for x in User.query()
1001 .filter(User.user_id != current_user.user_id)
1002 .filter(User.user_id != current_user.user_id)
1002 .filter(User.active == true())
1003 .filter(User.active == true())
1003 .filter(User.admin == true())
1004 .filter(User.admin == true())
1004 .order_by(User.created_on.asc())]
1005 .order_by(User.created_on.asc())]
1005
1006
1006 active_regular_users = [
1007 active_regular_users = [
1007 x.user_id for x in User.query()
1008 x.user_id for x in User.query()
1008 .filter(User.user_id != current_user.user_id)
1009 .filter(User.user_id != current_user.user_id)
1009 .filter(User.active == true())
1010 .filter(User.active == true())
1010 .filter(User.admin == false())
1011 .filter(User.admin == false())
1011 .order_by(User.created_on.asc())]
1012 .order_by(User.created_on.asc())]
1012
1013
1013 list_of_accounts = [current_user.user_id]
1014 list_of_accounts = [current_user.user_id]
1014 list_of_accounts += active_super_admins
1015 list_of_accounts += active_super_admins
1015 list_of_accounts += active_regular_users
1016 list_of_accounts += active_regular_users
1016
1017
1017 return list_of_accounts
1018 return list_of_accounts
1018
1019
1019 def deactivate_last_users(self, expected_users, current_user=None):
1020 def deactivate_last_users(self, expected_users, current_user=None):
1020 """
1021 """
1021 Deactivate accounts that are over the license limits.
1022 Deactivate accounts that are over the license limits.
1022 Algorithm of which accounts to disabled is based on the formula:
1023 Algorithm of which accounts to disabled is based on the formula:
1023
1024
1024 Get current user, then super admins in creation order, then regular
1025 Get current user, then super admins in creation order, then regular
1025 active users in creation order.
1026 active users in creation order.
1026
1027
1027 Using that list we mark all accounts from the end of it as inactive.
1028 Using that list we mark all accounts from the end of it as inactive.
1028 This way we block only latest created accounts.
1029 This way we block only latest created accounts.
1029
1030
1030 :param expected_users: list of users in special order, we deactivate
1031 :param expected_users: list of users in special order, we deactivate
1031 the end N amount of users from that list
1032 the end N amount of users from that list
1032 """
1033 """
1033
1034
1034 list_of_accounts = self.get_accounts_in_creation_order(
1035 list_of_accounts = self.get_accounts_in_creation_order(
1035 current_user=current_user)
1036 current_user=current_user)
1036
1037
1037 for acc_id in list_of_accounts[expected_users + 1:]:
1038 for acc_id in list_of_accounts[expected_users + 1:]:
1038 user = User.get(acc_id)
1039 user = User.get(acc_id)
1039 log.info('Deactivating account %s for license unlock', user)
1040 log.info('Deactivating account %s for license unlock', user)
1040 user.active = False
1041 user.active = False
1041 Session().add(user)
1042 Session().add(user)
1042 Session().commit()
1043 Session().commit()
1043
1044
1044 return
1045 return
1045
1046
1046 def get_user_log(self, user, filter_term):
1047 def get_user_log(self, user, filter_term):
1047 user_log = UserLog.query()\
1048 user_log = UserLog.query()\
1048 .filter(or_(UserLog.user_id == user.user_id,
1049 .filter(or_(UserLog.user_id == user.user_id,
1049 UserLog.username == user.username))\
1050 UserLog.username == user.username))\
1050 .options(joinedload(UserLog.user))\
1051 .options(joinedload(UserLog.user))\
1051 .options(joinedload(UserLog.repository))\
1052 .options(joinedload(UserLog.repository))\
1052 .order_by(UserLog.action_date.desc())
1053 .order_by(UserLog.action_date.desc())
1053
1054
1054 user_log = user_log_filter(user_log, filter_term)
1055 user_log = user_log_filter(user_log, filter_term)
1055 return user_log
1056 return user_log
General Comments 0
You need to be logged in to leave comments. Login now