##// END OF EJS Templates
auth: use cache_ttl from a plugin to also cache permissions....
marcink -
r2154:574d07a8 default
parent child Browse files
Show More
@@ -1,711 +1,730 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Authentication modules
22 Authentication modules
23 """
23 """
24
24
25 import colander
25 import colander
26 import copy
26 import copy
27 import logging
27 import logging
28 import time
28 import time
29 import traceback
29 import traceback
30 import warnings
30 import warnings
31 import functools
31 import functools
32
32
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34 from zope.cachedescriptors.property import Lazy as LazyProperty
35
35
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import caches
38 from rhodecode.lib import caches
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import md5_safe, safe_int
40 from rhodecode.lib.utils2 import safe_int
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80
80
81 class LazyFormencode(object):
81 class LazyFormencode(object):
82 def __init__(self, formencode_obj, *args, **kwargs):
82 def __init__(self, formencode_obj, *args, **kwargs):
83 self.formencode_obj = formencode_obj
83 self.formencode_obj = formencode_obj
84 self.args = args
84 self.args = args
85 self.kwargs = kwargs
85 self.kwargs = kwargs
86
86
87 def __call__(self, *args, **kwargs):
87 def __call__(self, *args, **kwargs):
88 from inspect import isfunction
88 from inspect import isfunction
89 formencode_obj = self.formencode_obj
89 formencode_obj = self.formencode_obj
90 if isfunction(formencode_obj):
90 if isfunction(formencode_obj):
91 # case we wrap validators into functions
91 # case we wrap validators into functions
92 formencode_obj = self.formencode_obj(*args, **kwargs)
92 formencode_obj = self.formencode_obj(*args, **kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
94
94
95
95
96 class RhodeCodeAuthPluginBase(object):
96 class RhodeCodeAuthPluginBase(object):
97 # cache the authentication request for N amount of seconds. Some kind
97 # cache the authentication request for N amount of seconds. Some kind
98 # of authentication methods are very heavy and it's very efficient to cache
98 # of authentication methods are very heavy and it's very efficient to cache
99 # the result of a call. If it's set to None (default) cache is off
99 # the result of a call. If it's set to None (default) cache is off
100 AUTH_CACHE_TTL = None
100 AUTH_CACHE_TTL = None
101 AUTH_CACHE = {}
101 AUTH_CACHE = {}
102
102
103 auth_func_attrs = {
103 auth_func_attrs = {
104 "username": "unique username",
104 "username": "unique username",
105 "firstname": "first name",
105 "firstname": "first name",
106 "lastname": "last name",
106 "lastname": "last name",
107 "email": "email address",
107 "email": "email address",
108 "groups": '["list", "of", "groups"]',
108 "groups": '["list", "of", "groups"]',
109 "extern_name": "name in external source of record",
109 "extern_name": "name in external source of record",
110 "extern_type": "type of external source of record",
110 "extern_type": "type of external source of record",
111 "admin": 'True|False defines if user should be RhodeCode super admin',
111 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "active":
112 "active":
113 'True|False defines active state of user internally for RhodeCode',
113 'True|False defines active state of user internally for RhodeCode',
114 "active_from_extern":
114 "active_from_extern":
115 "True|False\None, active state from the external auth, "
115 "True|False\None, active state from the external auth, "
116 "None means use definition from RhodeCode extern_type active value"
116 "None means use definition from RhodeCode extern_type active value"
117 }
117 }
118 # set on authenticate() method and via set_auth_type func.
118 # set on authenticate() method and via set_auth_type func.
119 auth_type = None
119 auth_type = None
120
120
121 # set on authenticate() method and via set_calling_scope_repo, this is a
121 # set on authenticate() method and via set_calling_scope_repo, this is a
122 # calling scope repository when doing authentication most likely on VCS
122 # calling scope repository when doing authentication most likely on VCS
123 # operations
123 # operations
124 acl_repo_name = None
124 acl_repo_name = None
125
125
126 # List of setting names to store encrypted. Plugins may override this list
126 # List of setting names to store encrypted. Plugins may override this list
127 # to store settings encrypted.
127 # to store settings encrypted.
128 _settings_encrypted = []
128 _settings_encrypted = []
129
129
130 # Mapping of python to DB settings model types. Plugins may override or
130 # Mapping of python to DB settings model types. Plugins may override or
131 # extend this mapping.
131 # extend this mapping.
132 _settings_type_map = {
132 _settings_type_map = {
133 colander.String: 'unicode',
133 colander.String: 'unicode',
134 colander.Integer: 'int',
134 colander.Integer: 'int',
135 colander.Boolean: 'bool',
135 colander.Boolean: 'bool',
136 colander.List: 'list',
136 colander.List: 'list',
137 }
137 }
138
138
139 # list of keys in settings that are unsafe to be logged, should be passwords
139 # list of keys in settings that are unsafe to be logged, should be passwords
140 # or other crucial credentials
140 # or other crucial credentials
141 _settings_unsafe_keys = []
141 _settings_unsafe_keys = []
142
142
143 def __init__(self, plugin_id):
143 def __init__(self, plugin_id):
144 self._plugin_id = plugin_id
144 self._plugin_id = plugin_id
145
145
146 def __str__(self):
146 def __str__(self):
147 return self.get_id()
147 return self.get_id()
148
148
149 def _get_setting_full_name(self, name):
149 def _get_setting_full_name(self, name):
150 """
150 """
151 Return the full setting name used for storing values in the database.
151 Return the full setting name used for storing values in the database.
152 """
152 """
153 # TODO: johbo: Using the name here is problematic. It would be good to
153 # TODO: johbo: Using the name here is problematic. It would be good to
154 # introduce either new models in the database to hold Plugin and
154 # introduce either new models in the database to hold Plugin and
155 # PluginSetting or to use the plugin id here.
155 # PluginSetting or to use the plugin id here.
156 return 'auth_{}_{}'.format(self.name, name)
156 return 'auth_{}_{}'.format(self.name, name)
157
157
158 def _get_setting_type(self, name):
158 def _get_setting_type(self, name):
159 """
159 """
160 Return the type of a setting. This type is defined by the SettingsModel
160 Return the type of a setting. This type is defined by the SettingsModel
161 and determines how the setting is stored in DB. Optionally the suffix
161 and determines how the setting is stored in DB. Optionally the suffix
162 `.encrypted` is appended to instruct SettingsModel to store it
162 `.encrypted` is appended to instruct SettingsModel to store it
163 encrypted.
163 encrypted.
164 """
164 """
165 schema_node = self.get_settings_schema().get(name)
165 schema_node = self.get_settings_schema().get(name)
166 db_type = self._settings_type_map.get(
166 db_type = self._settings_type_map.get(
167 type(schema_node.typ), 'unicode')
167 type(schema_node.typ), 'unicode')
168 if name in self._settings_encrypted:
168 if name in self._settings_encrypted:
169 db_type = '{}.encrypted'.format(db_type)
169 db_type = '{}.encrypted'.format(db_type)
170 return db_type
170 return db_type
171
171
172 @LazyProperty
172 @LazyProperty
173 def plugin_settings(self):
173 def plugin_settings(self):
174 settings = SettingsModel().get_all_settings()
174 settings = SettingsModel().get_all_settings()
175 return settings
175 return settings
176
176
177 def is_enabled(self):
177 def is_enabled(self):
178 """
178 """
179 Returns true if this plugin is enabled. An enabled plugin can be
179 Returns true if this plugin is enabled. An enabled plugin can be
180 configured in the admin interface but it is not consulted during
180 configured in the admin interface but it is not consulted during
181 authentication.
181 authentication.
182 """
182 """
183 auth_plugins = SettingsModel().get_auth_plugins()
183 auth_plugins = SettingsModel().get_auth_plugins()
184 return self.get_id() in auth_plugins
184 return self.get_id() in auth_plugins
185
185
186 def is_active(self):
186 def is_active(self):
187 """
187 """
188 Returns true if the plugin is activated. An activated plugin is
188 Returns true if the plugin is activated. An activated plugin is
189 consulted during authentication, assumed it is also enabled.
189 consulted during authentication, assumed it is also enabled.
190 """
190 """
191 return self.get_setting_by_name('enabled')
191 return self.get_setting_by_name('enabled')
192
192
193 def get_id(self):
193 def get_id(self):
194 """
194 """
195 Returns the plugin id.
195 Returns the plugin id.
196 """
196 """
197 return self._plugin_id
197 return self._plugin_id
198
198
199 def get_display_name(self):
199 def get_display_name(self):
200 """
200 """
201 Returns a translation string for displaying purposes.
201 Returns a translation string for displaying purposes.
202 """
202 """
203 raise NotImplementedError('Not implemented in base class')
203 raise NotImplementedError('Not implemented in base class')
204
204
205 def get_settings_schema(self):
205 def get_settings_schema(self):
206 """
206 """
207 Returns a colander schema, representing the plugin settings.
207 Returns a colander schema, representing the plugin settings.
208 """
208 """
209 return AuthnPluginSettingsSchemaBase()
209 return AuthnPluginSettingsSchemaBase()
210
210
211 def get_setting_by_name(self, name, default=None):
211 def get_setting_by_name(self, name, default=None):
212 """
212 """
213 Returns a plugin setting by name.
213 Returns a plugin setting by name.
214 """
214 """
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
216 plugin_settings = self.plugin_settings
216 plugin_settings = self.plugin_settings
217
217
218 return plugin_settings.get(full_name) or default
218 return plugin_settings.get(full_name) or default
219
219
220 def create_or_update_setting(self, name, value):
220 def create_or_update_setting(self, name, value):
221 """
221 """
222 Create or update a setting for this plugin in the persistent storage.
222 Create or update a setting for this plugin in the persistent storage.
223 """
223 """
224 full_name = self._get_setting_full_name(name)
224 full_name = self._get_setting_full_name(name)
225 type_ = self._get_setting_type(name)
225 type_ = self._get_setting_type(name)
226 db_setting = SettingsModel().create_or_update_setting(
226 db_setting = SettingsModel().create_or_update_setting(
227 full_name, value, type_)
227 full_name, value, type_)
228 return db_setting.app_settings_value
228 return db_setting.app_settings_value
229
229
230 def get_settings(self):
230 def get_settings(self):
231 """
231 """
232 Returns the plugin settings as dictionary.
232 Returns the plugin settings as dictionary.
233 """
233 """
234 settings = {}
234 settings = {}
235 for node in self.get_settings_schema():
235 for node in self.get_settings_schema():
236 settings[node.name] = self.get_setting_by_name(node.name)
236 settings[node.name] = self.get_setting_by_name(node.name)
237 return settings
237 return settings
238
238
239 def log_safe_settings(self, settings):
239 def log_safe_settings(self, settings):
240 """
240 """
241 returns a log safe representation of settings, without any secrets
241 returns a log safe representation of settings, without any secrets
242 """
242 """
243 settings_copy = copy.deepcopy(settings)
243 settings_copy = copy.deepcopy(settings)
244 for k in self._settings_unsafe_keys:
244 for k in self._settings_unsafe_keys:
245 if k in settings_copy:
245 if k in settings_copy:
246 del settings_copy[k]
246 del settings_copy[k]
247 return settings_copy
247 return settings_copy
248
248
249 @property
249 @property
250 def validators(self):
250 def validators(self):
251 """
251 """
252 Exposes RhodeCode validators modules
252 Exposes RhodeCode validators modules
253 """
253 """
254 # this is a hack to overcome issues with pylons threadlocals and
254 # this is a hack to overcome issues with pylons threadlocals and
255 # translator object _() not being registered properly.
255 # translator object _() not being registered properly.
256 class LazyCaller(object):
256 class LazyCaller(object):
257 def __init__(self, name):
257 def __init__(self, name):
258 self.validator_name = name
258 self.validator_name = name
259
259
260 def __call__(self, *args, **kwargs):
260 def __call__(self, *args, **kwargs):
261 from rhodecode.model import validators as v
261 from rhodecode.model import validators as v
262 obj = getattr(v, self.validator_name)
262 obj = getattr(v, self.validator_name)
263 # log.debug('Initializing lazy formencode object: %s', obj)
263 # log.debug('Initializing lazy formencode object: %s', obj)
264 return LazyFormencode(obj, *args, **kwargs)
264 return LazyFormencode(obj, *args, **kwargs)
265
265
266 class ProxyGet(object):
266 class ProxyGet(object):
267 def __getattribute__(self, name):
267 def __getattribute__(self, name):
268 return LazyCaller(name)
268 return LazyCaller(name)
269
269
270 return ProxyGet()
270 return ProxyGet()
271
271
272 @hybrid_property
272 @hybrid_property
273 def name(self):
273 def name(self):
274 """
274 """
275 Returns the name of this authentication plugin.
275 Returns the name of this authentication plugin.
276
276
277 :returns: string
277 :returns: string
278 """
278 """
279 raise NotImplementedError("Not implemented in base class")
279 raise NotImplementedError("Not implemented in base class")
280
280
281 def get_url_slug(self):
281 def get_url_slug(self):
282 """
282 """
283 Returns a slug which should be used when constructing URLs which refer
283 Returns a slug which should be used when constructing URLs which refer
284 to this plugin. By default it returns the plugin name. If the name is
284 to this plugin. By default it returns the plugin name. If the name is
285 not suitable for using it in an URL the plugin should override this
285 not suitable for using it in an URL the plugin should override this
286 method.
286 method.
287 """
287 """
288 return self.name
288 return self.name
289
289
290 @property
290 @property
291 def is_headers_auth(self):
291 def is_headers_auth(self):
292 """
292 """
293 Returns True if this authentication plugin uses HTTP headers as
293 Returns True if this authentication plugin uses HTTP headers as
294 authentication method.
294 authentication method.
295 """
295 """
296 return False
296 return False
297
297
298 @hybrid_property
298 @hybrid_property
299 def is_container_auth(self):
299 def is_container_auth(self):
300 """
300 """
301 Deprecated method that indicates if this authentication plugin uses
301 Deprecated method that indicates if this authentication plugin uses
302 HTTP headers as authentication method.
302 HTTP headers as authentication method.
303 """
303 """
304 warnings.warn(
304 warnings.warn(
305 'Use is_headers_auth instead.', category=DeprecationWarning)
305 'Use is_headers_auth instead.', category=DeprecationWarning)
306 return self.is_headers_auth
306 return self.is_headers_auth
307
307
308 @hybrid_property
308 @hybrid_property
309 def allows_creating_users(self):
309 def allows_creating_users(self):
310 """
310 """
311 Defines if Plugin allows users to be created on-the-fly when
311 Defines if Plugin allows users to be created on-the-fly when
312 authentication is called. Controls how external plugins should behave
312 authentication is called. Controls how external plugins should behave
313 in terms if they are allowed to create new users, or not. Base plugins
313 in terms if they are allowed to create new users, or not. Base plugins
314 should not be allowed to, but External ones should be !
314 should not be allowed to, but External ones should be !
315
315
316 :return: bool
316 :return: bool
317 """
317 """
318 return False
318 return False
319
319
320 def set_auth_type(self, auth_type):
320 def set_auth_type(self, auth_type):
321 self.auth_type = auth_type
321 self.auth_type = auth_type
322
322
323 def set_calling_scope_repo(self, acl_repo_name):
323 def set_calling_scope_repo(self, acl_repo_name):
324 self.acl_repo_name = acl_repo_name
324 self.acl_repo_name = acl_repo_name
325
325
326 def allows_authentication_from(
326 def allows_authentication_from(
327 self, user, allows_non_existing_user=True,
327 self, user, allows_non_existing_user=True,
328 allowed_auth_plugins=None, allowed_auth_sources=None):
328 allowed_auth_plugins=None, allowed_auth_sources=None):
329 """
329 """
330 Checks if this authentication module should accept a request for
330 Checks if this authentication module should accept a request for
331 the current user.
331 the current user.
332
332
333 :param user: user object fetched using plugin's get_user() method.
333 :param user: user object fetched using plugin's get_user() method.
334 :param allows_non_existing_user: if True, don't allow the
334 :param allows_non_existing_user: if True, don't allow the
335 user to be empty, meaning not existing in our database
335 user to be empty, meaning not existing in our database
336 :param allowed_auth_plugins: if provided, users extern_type will be
336 :param allowed_auth_plugins: if provided, users extern_type will be
337 checked against a list of provided extern types, which are plugin
337 checked against a list of provided extern types, which are plugin
338 auth_names in the end
338 auth_names in the end
339 :param allowed_auth_sources: authentication type allowed,
339 :param allowed_auth_sources: authentication type allowed,
340 `http` or `vcs` default is both.
340 `http` or `vcs` default is both.
341 defines if plugin will accept only http authentication vcs
341 defines if plugin will accept only http authentication vcs
342 authentication(git/hg) or both
342 authentication(git/hg) or both
343 :returns: boolean
343 :returns: boolean
344 """
344 """
345 if not user and not allows_non_existing_user:
345 if not user and not allows_non_existing_user:
346 log.debug('User is empty but plugin does not allow empty users,'
346 log.debug('User is empty but plugin does not allow empty users,'
347 'not allowed to authenticate')
347 'not allowed to authenticate')
348 return False
348 return False
349
349
350 expected_auth_plugins = allowed_auth_plugins or [self.name]
350 expected_auth_plugins = allowed_auth_plugins or [self.name]
351 if user and (user.extern_type and
351 if user and (user.extern_type and
352 user.extern_type not in expected_auth_plugins):
352 user.extern_type not in expected_auth_plugins):
353 log.debug(
353 log.debug(
354 'User `%s` is bound to `%s` auth type. Plugin allows only '
354 'User `%s` is bound to `%s` auth type. Plugin allows only '
355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
356
356
357 return False
357 return False
358
358
359 # by default accept both
359 # by default accept both
360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
361 if self.auth_type not in expected_auth_from:
361 if self.auth_type not in expected_auth_from:
362 log.debug('Current auth source is %s but plugin only allows %s',
362 log.debug('Current auth source is %s but plugin only allows %s',
363 self.auth_type, expected_auth_from)
363 self.auth_type, expected_auth_from)
364 return False
364 return False
365
365
366 return True
366 return True
367
367
368 def get_user(self, username=None, **kwargs):
368 def get_user(self, username=None, **kwargs):
369 """
369 """
370 Helper method for user fetching in plugins, by default it's using
370 Helper method for user fetching in plugins, by default it's using
371 simple fetch by username, but this method can be custimized in plugins
371 simple fetch by username, but this method can be custimized in plugins
372 eg. headers auth plugin to fetch user by environ params
372 eg. headers auth plugin to fetch user by environ params
373
373
374 :param username: username if given to fetch from database
374 :param username: username if given to fetch from database
375 :param kwargs: extra arguments needed for user fetching.
375 :param kwargs: extra arguments needed for user fetching.
376 """
376 """
377 user = None
377 user = None
378 log.debug(
378 log.debug(
379 'Trying to fetch user `%s` from RhodeCode database', username)
379 'Trying to fetch user `%s` from RhodeCode database', username)
380 if username:
380 if username:
381 user = User.get_by_username(username)
381 user = User.get_by_username(username)
382 if not user:
382 if not user:
383 log.debug('User not found, fallback to fetch user in '
383 log.debug('User not found, fallback to fetch user in '
384 'case insensitive mode')
384 'case insensitive mode')
385 user = User.get_by_username(username, case_insensitive=True)
385 user = User.get_by_username(username, case_insensitive=True)
386 else:
386 else:
387 log.debug('provided username:`%s` is empty skipping...', username)
387 log.debug('provided username:`%s` is empty skipping...', username)
388 if not user:
388 if not user:
389 log.debug('User `%s` not found in database', username)
389 log.debug('User `%s` not found in database', username)
390 else:
390 else:
391 log.debug('Got DB user:%s', user)
391 log.debug('Got DB user:%s', user)
392 return user
392 return user
393
393
394 def user_activation_state(self):
394 def user_activation_state(self):
395 """
395 """
396 Defines user activation state when creating new users
396 Defines user activation state when creating new users
397
397
398 :returns: boolean
398 :returns: boolean
399 """
399 """
400 raise NotImplementedError("Not implemented in base class")
400 raise NotImplementedError("Not implemented in base class")
401
401
402 def auth(self, userobj, username, passwd, settings, **kwargs):
402 def auth(self, userobj, username, passwd, settings, **kwargs):
403 """
403 """
404 Given a user object (which may be null), username, a plaintext
404 Given a user object (which may be null), username, a plaintext
405 password, and a settings object (containing all the keys needed as
405 password, and a settings object (containing all the keys needed as
406 listed in settings()), authenticate this user's login attempt.
406 listed in settings()), authenticate this user's login attempt.
407
407
408 Return None on failure. On success, return a dictionary of the form:
408 Return None on failure. On success, return a dictionary of the form:
409
409
410 see: RhodeCodeAuthPluginBase.auth_func_attrs
410 see: RhodeCodeAuthPluginBase.auth_func_attrs
411 This is later validated for correctness
411 This is later validated for correctness
412 """
412 """
413 raise NotImplementedError("not implemented in base class")
413 raise NotImplementedError("not implemented in base class")
414
414
415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
416 """
416 """
417 Wrapper to call self.auth() that validates call on it
417 Wrapper to call self.auth() that validates call on it
418
418
419 :param userobj: userobj
419 :param userobj: userobj
420 :param username: username
420 :param username: username
421 :param passwd: plaintext password
421 :param passwd: plaintext password
422 :param settings: plugin settings
422 :param settings: plugin settings
423 """
423 """
424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
425 if auth:
425 if auth:
426 auth['_plugin'] = self.name
427 auth['_ttl_cache'] = self.get_ttl_cache(settings)
426 # check if hash should be migrated ?
428 # check if hash should be migrated ?
427 new_hash = auth.get('_hash_migrate')
429 new_hash = auth.get('_hash_migrate')
428 if new_hash:
430 if new_hash:
429 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
431 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
430 return self._validate_auth_return(auth)
432 return self._validate_auth_return(auth)
433
431 return auth
434 return auth
432
435
433 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
436 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
434 new_hash_cypher = _RhodeCodeCryptoBCrypt()
437 new_hash_cypher = _RhodeCodeCryptoBCrypt()
435 # extra checks, so make sure new hash is correct.
438 # extra checks, so make sure new hash is correct.
436 password_encoded = safe_str(password)
439 password_encoded = safe_str(password)
437 if new_hash and new_hash_cypher.hash_check(
440 if new_hash and new_hash_cypher.hash_check(
438 password_encoded, new_hash):
441 password_encoded, new_hash):
439 cur_user = User.get_by_username(username)
442 cur_user = User.get_by_username(username)
440 cur_user.password = new_hash
443 cur_user.password = new_hash
441 Session().add(cur_user)
444 Session().add(cur_user)
442 Session().flush()
445 Session().flush()
443 log.info('Migrated user %s hash to bcrypt', cur_user)
446 log.info('Migrated user %s hash to bcrypt', cur_user)
444
447
445 def _validate_auth_return(self, ret):
448 def _validate_auth_return(self, ret):
446 if not isinstance(ret, dict):
449 if not isinstance(ret, dict):
447 raise Exception('returned value from auth must be a dict')
450 raise Exception('returned value from auth must be a dict')
448 for k in self.auth_func_attrs:
451 for k in self.auth_func_attrs:
449 if k not in ret:
452 if k not in ret:
450 raise Exception('Missing %s attribute from returned data' % k)
453 raise Exception('Missing %s attribute from returned data' % k)
451 return ret
454 return ret
452
455
456 def get_ttl_cache(self, settings=None):
457 plugin_settings = settings or self.get_settings()
458 cache_ttl = 0
459
460 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
461 # plugin cache set inside is more important than the settings value
462 cache_ttl = self.AUTH_CACHE_TTL
463 elif plugin_settings.get('cache_ttl'):
464 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
465
466 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
467 return plugin_cache_active, cache_ttl
468
453
469
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
470 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455
471
456 @hybrid_property
472 @hybrid_property
457 def allows_creating_users(self):
473 def allows_creating_users(self):
458 return True
474 return True
459
475
460 def use_fake_password(self):
476 def use_fake_password(self):
461 """
477 """
462 Return a boolean that indicates whether or not we should set the user's
478 Return a boolean that indicates whether or not we should set the user's
463 password to a random value when it is authenticated by this plugin.
479 password to a random value when it is authenticated by this plugin.
464 If your plugin provides authentication, then you will generally
480 If your plugin provides authentication, then you will generally
465 want this.
481 want this.
466
482
467 :returns: boolean
483 :returns: boolean
468 """
484 """
469 raise NotImplementedError("Not implemented in base class")
485 raise NotImplementedError("Not implemented in base class")
470
486
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
487 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 # at this point _authenticate calls plugin's `auth()` function
488 # at this point _authenticate calls plugin's `auth()` function
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
489 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 userobj, username, passwd, settings, **kwargs)
490 userobj, username, passwd, settings, **kwargs)
475
491
476 if auth:
492 if auth:
477 # maybe plugin will clean the username ?
493 # maybe plugin will clean the username ?
478 # we should use the return value
494 # we should use the return value
479 username = auth['username']
495 username = auth['username']
480
496
481 # if external source tells us that user is not active, we should
497 # if external source tells us that user is not active, we should
482 # skip rest of the process. This can prevent from creating users in
498 # skip rest of the process. This can prevent from creating users in
483 # RhodeCode when using external authentication, but if it's
499 # RhodeCode when using external authentication, but if it's
484 # inactive user we shouldn't create that user anyway
500 # inactive user we shouldn't create that user anyway
485 if auth['active_from_extern'] is False:
501 if auth['active_from_extern'] is False:
486 log.warning(
502 log.warning(
487 "User %s authenticated against %s, but is inactive",
503 "User %s authenticated against %s, but is inactive",
488 username, self.__module__)
504 username, self.__module__)
489 return None
505 return None
490
506
491 cur_user = User.get_by_username(username, case_insensitive=True)
507 cur_user = User.get_by_username(username, case_insensitive=True)
492 is_user_existing = cur_user is not None
508 is_user_existing = cur_user is not None
493
509
494 if is_user_existing:
510 if is_user_existing:
495 log.debug('Syncing user `%s` from '
511 log.debug('Syncing user `%s` from '
496 '`%s` plugin', username, self.name)
512 '`%s` plugin', username, self.name)
497 else:
513 else:
498 log.debug('Creating non existing user `%s` from '
514 log.debug('Creating non existing user `%s` from '
499 '`%s` plugin', username, self.name)
515 '`%s` plugin', username, self.name)
500
516
501 if self.allows_creating_users:
517 if self.allows_creating_users:
502 log.debug('Plugin `%s` allows to '
518 log.debug('Plugin `%s` allows to '
503 'create new users', self.name)
519 'create new users', self.name)
504 else:
520 else:
505 log.debug('Plugin `%s` does not allow to '
521 log.debug('Plugin `%s` does not allow to '
506 'create new users', self.name)
522 'create new users', self.name)
507
523
508 user_parameters = {
524 user_parameters = {
509 'username': username,
525 'username': username,
510 'email': auth["email"],
526 'email': auth["email"],
511 'firstname': auth["firstname"],
527 'firstname': auth["firstname"],
512 'lastname': auth["lastname"],
528 'lastname': auth["lastname"],
513 'active': auth["active"],
529 'active': auth["active"],
514 'admin': auth["admin"],
530 'admin': auth["admin"],
515 'extern_name': auth["extern_name"],
531 'extern_name': auth["extern_name"],
516 'extern_type': self.name,
532 'extern_type': self.name,
517 'plugin': self,
533 'plugin': self,
518 'allow_to_create_user': self.allows_creating_users,
534 'allow_to_create_user': self.allows_creating_users,
519 }
535 }
520
536
521 if not is_user_existing:
537 if not is_user_existing:
522 if self.use_fake_password():
538 if self.use_fake_password():
523 # Randomize the PW because we don't need it, but don't want
539 # Randomize the PW because we don't need it, but don't want
524 # them blank either
540 # them blank either
525 passwd = PasswordGenerator().gen_password(length=16)
541 passwd = PasswordGenerator().gen_password(length=16)
526 user_parameters['password'] = passwd
542 user_parameters['password'] = passwd
527 else:
543 else:
528 # Since the password is required by create_or_update method of
544 # Since the password is required by create_or_update method of
529 # UserModel, we need to set it explicitly.
545 # UserModel, we need to set it explicitly.
530 # The create_or_update method is smart and recognises the
546 # The create_or_update method is smart and recognises the
531 # password hashes as well.
547 # password hashes as well.
532 user_parameters['password'] = cur_user.password
548 user_parameters['password'] = cur_user.password
533
549
534 # we either create or update users, we also pass the flag
550 # we either create or update users, we also pass the flag
535 # that controls if this method can actually do that.
551 # that controls if this method can actually do that.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
552 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 user = UserModel().create_or_update(**user_parameters)
553 user = UserModel().create_or_update(**user_parameters)
538 Session().flush()
554 Session().flush()
539 # enforce user is just in given groups, all of them has to be ones
555 # enforce user is just in given groups, all of them has to be ones
540 # created from plugins. We store this info in _group_data JSON
556 # created from plugins. We store this info in _group_data JSON
541 # field
557 # field
542 try:
558 try:
543 groups = auth['groups'] or []
559 groups = auth['groups'] or []
544 log.debug(
560 log.debug(
545 'Performing user_group sync based on set `%s` '
561 'Performing user_group sync based on set `%s` '
546 'returned by this plugin', groups)
562 'returned by this plugin', groups)
547 UserGroupModel().enforce_groups(user, groups, self.name)
563 UserGroupModel().enforce_groups(user, groups, self.name)
548 except Exception:
564 except Exception:
549 # for any reason group syncing fails, we should
565 # for any reason group syncing fails, we should
550 # proceed with login
566 # proceed with login
551 log.error(traceback.format_exc())
567 log.error(traceback.format_exc())
552 Session().commit()
568 Session().commit()
553 return auth
569 return auth
554
570
555
571
556 def loadplugin(plugin_id):
572 def loadplugin(plugin_id):
557 """
573 """
558 Loads and returns an instantiated authentication plugin.
574 Loads and returns an instantiated authentication plugin.
559 Returns the RhodeCodeAuthPluginBase subclass on success,
575 Returns the RhodeCodeAuthPluginBase subclass on success,
560 or None on failure.
576 or None on failure.
561 """
577 """
562 # TODO: Disusing pyramids thread locals to retrieve the registry.
578 # TODO: Disusing pyramids thread locals to retrieve the registry.
563 authn_registry = get_authn_registry()
579 authn_registry = get_authn_registry()
564 plugin = authn_registry.get_plugin(plugin_id)
580 plugin = authn_registry.get_plugin(plugin_id)
565 if plugin is None:
581 if plugin is None:
566 log.error('Authentication plugin not found: "%s"', plugin_id)
582 log.error('Authentication plugin not found: "%s"', plugin_id)
567 return plugin
583 return plugin
568
584
569
585
570 def get_authn_registry(registry=None):
586 def get_authn_registry(registry=None):
571 registry = registry or get_current_registry()
587 registry = registry or get_current_registry()
572 authn_registry = registry.getUtility(IAuthnPluginRegistry)
588 authn_registry = registry.getUtility(IAuthnPluginRegistry)
573 return authn_registry
589 return authn_registry
574
590
575
591
576 def get_auth_cache_manager(custom_ttl=None):
592 def get_auth_cache_manager(custom_ttl=None):
577 return caches.get_cache_manager(
593 return caches.get_cache_manager(
578 'auth_plugins', 'rhodecode.authentication', custom_ttl)
594 'auth_plugins', 'rhodecode.authentication', custom_ttl)
579
595
580
596
597 def get_perms_cache_manager(custom_ttl=None):
598 return caches.get_cache_manager(
599 'auth_plugins', 'rhodecode.permissions', custom_ttl)
600
601
581 def authenticate(username, password, environ=None, auth_type=None,
602 def authenticate(username, password, environ=None, auth_type=None,
582 skip_missing=False, registry=None, acl_repo_name=None):
603 skip_missing=False, registry=None, acl_repo_name=None):
583 """
604 """
584 Authentication function used for access control,
605 Authentication function used for access control,
585 It tries to authenticate based on enabled authentication modules.
606 It tries to authenticate based on enabled authentication modules.
586
607
587 :param username: username can be empty for headers auth
608 :param username: username can be empty for headers auth
588 :param password: password can be empty for headers auth
609 :param password: password can be empty for headers auth
589 :param environ: environ headers passed for headers auth
610 :param environ: environ headers passed for headers auth
590 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
611 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
591 :param skip_missing: ignores plugins that are in db but not in environment
612 :param skip_missing: ignores plugins that are in db but not in environment
592 :returns: None if auth failed, plugin_user dict if auth is correct
613 :returns: None if auth failed, plugin_user dict if auth is correct
593 """
614 """
594 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
615 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
595 raise ValueError('auth type must be on of http, vcs got "%s" instead'
616 raise ValueError('auth type must be on of http, vcs got "%s" instead'
596 % auth_type)
617 % auth_type)
597 headers_only = environ and not (username and password)
618 headers_only = environ and not (username and password)
598
619
599 authn_registry = get_authn_registry(registry)
620 authn_registry = get_authn_registry(registry)
600 plugins_to_check = authn_registry.get_plugins_for_authentication()
621 plugins_to_check = authn_registry.get_plugins_for_authentication()
601 log.debug('Starting ordered authentication chain using %s plugins',
622 log.debug('Starting ordered authentication chain using %s plugins',
602 plugins_to_check)
623 plugins_to_check)
603 for plugin in plugins_to_check:
624 for plugin in plugins_to_check:
604 plugin.set_auth_type(auth_type)
625 plugin.set_auth_type(auth_type)
605 plugin.set_calling_scope_repo(acl_repo_name)
626 plugin.set_calling_scope_repo(acl_repo_name)
606
627
607 if headers_only and not plugin.is_headers_auth:
628 if headers_only and not plugin.is_headers_auth:
608 log.debug('Auth type is for headers only and plugin `%s` is not '
629 log.debug('Auth type is for headers only and plugin `%s` is not '
609 'headers plugin, skipping...', plugin.get_id())
630 'headers plugin, skipping...', plugin.get_id())
610 continue
631 continue
611
632
612 # load plugin settings from RhodeCode database
633 # load plugin settings from RhodeCode database
613 plugin_settings = plugin.get_settings()
634 plugin_settings = plugin.get_settings()
614 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
635 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
615 log.debug('Plugin settings:%s', plugin_sanitized_settings)
636 log.debug('Plugin settings:%s', plugin_sanitized_settings)
616
637
617 log.debug('Trying authentication using ** %s **', plugin.get_id())
638 log.debug('Trying authentication using ** %s **', plugin.get_id())
618 # use plugin's method of user extraction.
639 # use plugin's method of user extraction.
619 user = plugin.get_user(username, environ=environ,
640 user = plugin.get_user(username, environ=environ,
620 settings=plugin_settings)
641 settings=plugin_settings)
621 display_user = user.username if user else username
642 display_user = user.username if user else username
622 log.debug(
643 log.debug(
623 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
644 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
624
645
625 if not plugin.allows_authentication_from(user):
646 if not plugin.allows_authentication_from(user):
626 log.debug('Plugin %s does not accept user `%s` for authentication',
647 log.debug('Plugin %s does not accept user `%s` for authentication',
627 plugin.get_id(), display_user)
648 plugin.get_id(), display_user)
628 continue
649 continue
629 else:
650 else:
630 log.debug('Plugin %s accepted user `%s` for authentication',
651 log.debug('Plugin %s accepted user `%s` for authentication',
631 plugin.get_id(), display_user)
652 plugin.get_id(), display_user)
632
653
633 log.info('Authenticating user `%s` using %s plugin',
654 log.info('Authenticating user `%s` using %s plugin',
634 display_user, plugin.get_id())
655 display_user, plugin.get_id())
635
656
636 _cache_ttl = 0
657 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
637
638 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
639 # plugin cache set inside is more important than the settings value
640 _cache_ttl = plugin.AUTH_CACHE_TTL
641 elif plugin_settings.get('cache_ttl'):
642 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
643
644 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
645
658
646 # get instance of cache manager configured for a namespace
659 # get instance of cache manager configured for a namespace
647 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
660 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
648
661
649 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
662 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
650 plugin.get_id(), plugin_cache_active, _cache_ttl)
663 plugin.get_id(), plugin_cache_active, cache_ttl)
651
664
652 # for environ based password can be empty, but then the validation is
665 # for environ based password can be empty, but then the validation is
653 # on the server that fills in the env data needed for authentication
666 # on the server that fills in the env data needed for authentication
654 _password_hash = md5_safe(plugin.name + username + (password or ''))
667
668 _password_hash = caches.compute_key_from_params(
669 plugin.name, username, (password or ''))
655
670
656 # _authenticate is a wrapper for .auth() method of plugin.
671 # _authenticate is a wrapper for .auth() method of plugin.
657 # it checks if .auth() sends proper data.
672 # it checks if .auth() sends proper data.
658 # For RhodeCodeExternalAuthPlugin it also maps users to
673 # For RhodeCodeExternalAuthPlugin it also maps users to
659 # Database and maps the attributes returned from .auth()
674 # Database and maps the attributes returned from .auth()
660 # to RhodeCode database. If this function returns data
675 # to RhodeCode database. If this function returns data
661 # then auth is correct.
676 # then auth is correct.
662 start = time.time()
677 start = time.time()
663 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
678 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
664
679
665 def auth_func():
680 def auth_func():
666 """
681 """
667 This function is used internally in Cache of Beaker to calculate
682 This function is used internally in Cache of Beaker to calculate
668 Results
683 Results
669 """
684 """
685 log.debug('auth: calculating password access now...')
670 return plugin._authenticate(
686 return plugin._authenticate(
671 user, username, password, plugin_settings,
687 user, username, password, plugin_settings,
672 environ=environ or {})
688 environ=environ or {})
673
689
674 if plugin_cache_active:
690 if plugin_cache_active:
691 log.debug('Trying to fetch cached auth by %s', _password_hash[:6])
675 plugin_user = cache_manager.get(
692 plugin_user = cache_manager.get(
676 _password_hash, createfunc=auth_func)
693 _password_hash, createfunc=auth_func)
677 else:
694 else:
678 plugin_user = auth_func()
695 plugin_user = auth_func()
679
696
680 auth_time = time.time() - start
697 auth_time = time.time() - start
681 log.debug('Authentication for plugin `%s` completed in %.3fs, '
698 log.debug('Authentication for plugin `%s` completed in %.3fs, '
682 'expiration time of fetched cache %.1fs.',
699 'expiration time of fetched cache %.1fs.',
683 plugin.get_id(), auth_time, _cache_ttl)
700 plugin.get_id(), auth_time, cache_ttl)
684
701
685 log.debug('PLUGIN USER DATA: %s', plugin_user)
702 log.debug('PLUGIN USER DATA: %s', plugin_user)
686
703
687 if plugin_user:
704 if plugin_user:
688 log.debug('Plugin returned proper authentication data')
705 log.debug('Plugin returned proper authentication data')
689 return plugin_user
706 return plugin_user
690 # we failed to Auth because .auth() method didn't return proper user
707 # we failed to Auth because .auth() method didn't return proper user
691 log.debug("User `%s` failed to authenticate against %s",
708 log.debug("User `%s` failed to authenticate against %s",
692 display_user, plugin.get_id())
709 display_user, plugin.get_id())
710
711 # case when we failed to authenticate against all defined plugins
693 return None
712 return None
694
713
695
714
696 def chop_at(s, sub, inclusive=False):
715 def chop_at(s, sub, inclusive=False):
697 """Truncate string ``s`` at the first occurrence of ``sub``.
716 """Truncate string ``s`` at the first occurrence of ``sub``.
698
717
699 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
718 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
700
719
701 >>> chop_at("plutocratic brats", "rat")
720 >>> chop_at("plutocratic brats", "rat")
702 'plutoc'
721 'plutoc'
703 >>> chop_at("plutocratic brats", "rat", True)
722 >>> chop_at("plutocratic brats", "rat", True)
704 'plutocrat'
723 'plutocrat'
705 """
724 """
706 pos = s.find(sub)
725 pos = s.find(sub)
707 if pos == -1:
726 if pos == -1:
708 return s
727 return s
709 if inclusive:
728 if inclusive:
710 return s[:pos+len(sub)]
729 return s[:pos+len(sub)]
711 return s[:pos]
730 return s[:pos]
@@ -1,52 +1,51 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22
22
23 from rhodecode.translation import _
23 from rhodecode.translation import _
24
24
25
25
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 """
27 """
28 This base schema is intended for use in authentication plugins.
28 This base schema is intended for use in authentication plugins.
29 It adds a few default settings (e.g., "enabled"), so that plugin
29 It adds a few default settings (e.g., "enabled"), so that plugin
30 authors don't have to maintain a bunch of boilerplate.
30 authors don't have to maintain a bunch of boilerplate.
31 """
31 """
32 enabled = colander.SchemaNode(
32 enabled = colander.SchemaNode(
33 colander.Bool(),
33 colander.Bool(),
34 default=False,
34 default=False,
35 description=_('Enable or disable this authentication plugin.'),
35 description=_('Enable or disable this authentication plugin.'),
36 missing=False,
36 missing=False,
37 title=_('Enabled'),
37 title=_('Enabled'),
38 widget='bool',
38 widget='bool',
39 )
39 )
40 cache_ttl = colander.SchemaNode(
40 cache_ttl = colander.SchemaNode(
41 colander.Int(),
41 colander.Int(),
42 default=0,
42 default=0,
43 description=_('Amount of seconds to cache the authentication response'
43 description=_('Amount of seconds to cache the authentication and '
44 'call for this plugin. \n'
44 'permissions check response call for this plugin. \n'
45 'Useful for long calls like LDAP to improve the '
45 'Useful for expensive calls like LDAP to improve the '
46 'performance of the authentication system '
46 'performance of the system (0 means disabled).'),
47 '(0 means disabled).'),
48 missing=0,
47 missing=0,
49 title=_('Auth Cache TTL'),
48 title=_('Auth Cache TTL'),
50 validator=colander.Range(min=0, max=None),
49 validator=colander.Range(min=0, max=None),
51 widget='int',
50 widget='int',
52 )
51 )
@@ -1,630 +1,631 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
47 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
50 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
51 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
54
55 # NOTE(marcink): remove after base controller is no longer required
55 # NOTE(marcink): remove after base controller is no longer required
56 from pylons.controllers import WSGIController
56 from pylons.controllers import WSGIController
57 from pylons.i18n import translation
57 from pylons.i18n import translation
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 # hack to make the migration to pyramid easier
62 # hack to make the migration to pyramid easier
63 def render(template_name, extra_vars=None, cache_key=None,
63 def render(template_name, extra_vars=None, cache_key=None,
64 cache_type=None, cache_expire=None):
64 cache_type=None, cache_expire=None):
65 """Render a template with Mako
65 """Render a template with Mako
66
66
67 Accepts the cache options ``cache_key``, ``cache_type``, and
67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 ``cache_expire``.
68 ``cache_expire``.
69
69
70 """
70 """
71 from pylons.templating import literal
71 from pylons.templating import literal
72 from pylons.templating import cached_template, pylons_globals
72 from pylons.templating import cached_template, pylons_globals
73
73
74 # Create a render callable for the cache function
74 # Create a render callable for the cache function
75 def render_template():
75 def render_template():
76 # Pull in extra vars if needed
76 # Pull in extra vars if needed
77 globs = extra_vars or {}
77 globs = extra_vars or {}
78
78
79 # Second, get the globals
79 # Second, get the globals
80 globs.update(pylons_globals())
80 globs.update(pylons_globals())
81
81
82 globs['_ungettext'] = globs['ungettext']
82 globs['_ungettext'] = globs['ungettext']
83 # Grab a template reference
83 # Grab a template reference
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85
85
86 return literal(template.render_unicode(**globs))
86 return literal(template.render_unicode(**globs))
87
87
88 return cached_template(template_name, render_template, cache_key=cache_key,
88 return cached_template(template_name, render_template, cache_key=cache_key,
89 cache_type=cache_type, cache_expire=cache_expire)
89 cache_type=cache_type, cache_expire=cache_expire)
90
90
91 def _filter_proxy(ip):
91 def _filter_proxy(ip):
92 """
92 """
93 Passed in IP addresses in HEADERS can be in a special format of multiple
93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 ips. Those comma separated IPs are passed from various proxies in the
94 ips. Those comma separated IPs are passed from various proxies in the
95 chain of request processing. The left-most being the original client.
95 chain of request processing. The left-most being the original client.
96 We only care about the first IP which came from the org. client.
96 We only care about the first IP which came from the org. client.
97
97
98 :param ip: ip string from headers
98 :param ip: ip string from headers
99 """
99 """
100 if ',' in ip:
100 if ',' in ip:
101 _ips = ip.split(',')
101 _ips = ip.split(',')
102 _first_ip = _ips[0].strip()
102 _first_ip = _ips[0].strip()
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 return _first_ip
104 return _first_ip
105 return ip
105 return ip
106
106
107
107
108 def _filter_port(ip):
108 def _filter_port(ip):
109 """
109 """
110 Removes a port from ip, there are 4 main cases to handle here.
110 Removes a port from ip, there are 4 main cases to handle here.
111 - ipv4 eg. 127.0.0.1
111 - ipv4 eg. 127.0.0.1
112 - ipv6 eg. ::1
112 - ipv6 eg. ::1
113 - ipv4+port eg. 127.0.0.1:8080
113 - ipv4+port eg. 127.0.0.1:8080
114 - ipv6+port eg. [::1]:8080
114 - ipv6+port eg. [::1]:8080
115
115
116 :param ip:
116 :param ip:
117 """
117 """
118 def is_ipv6(ip_addr):
118 def is_ipv6(ip_addr):
119 if hasattr(socket, 'inet_pton'):
119 if hasattr(socket, 'inet_pton'):
120 try:
120 try:
121 socket.inet_pton(socket.AF_INET6, ip_addr)
121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 except socket.error:
122 except socket.error:
123 return False
123 return False
124 else:
124 else:
125 # fallback to ipaddress
125 # fallback to ipaddress
126 try:
126 try:
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 except Exception:
128 except Exception:
129 return False
129 return False
130 return True
130 return True
131
131
132 if ':' not in ip: # must be ipv4 pure ip
132 if ':' not in ip: # must be ipv4 pure ip
133 return ip
133 return ip
134
134
135 if '[' in ip and ']' in ip: # ipv6 with port
135 if '[' in ip and ']' in ip: # ipv6 with port
136 return ip.split(']')[0][1:].lower()
136 return ip.split(']')[0][1:].lower()
137
137
138 # must be ipv6 or ipv4 with port
138 # must be ipv6 or ipv4 with port
139 if is_ipv6(ip):
139 if is_ipv6(ip):
140 return ip
140 return ip
141 else:
141 else:
142 ip, _port = ip.split(':')[:2] # means ipv4+port
142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 return ip
143 return ip
144
144
145
145
146 def get_ip_addr(environ):
146 def get_ip_addr(environ):
147 proxy_key = 'HTTP_X_REAL_IP'
147 proxy_key = 'HTTP_X_REAL_IP'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 def_key = 'REMOTE_ADDR'
149 def_key = 'REMOTE_ADDR'
150 _filters = lambda x: _filter_port(_filter_proxy(x))
150 _filters = lambda x: _filter_port(_filter_proxy(x))
151
151
152 ip = environ.get(proxy_key)
152 ip = environ.get(proxy_key)
153 if ip:
153 if ip:
154 return _filters(ip)
154 return _filters(ip)
155
155
156 ip = environ.get(proxy_key2)
156 ip = environ.get(proxy_key2)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(def_key, '0.0.0.0')
160 ip = environ.get(def_key, '0.0.0.0')
161 return _filters(ip)
161 return _filters(ip)
162
162
163
163
164 def get_server_ip_addr(environ, log_errors=True):
164 def get_server_ip_addr(environ, log_errors=True):
165 hostname = environ.get('SERVER_NAME')
165 hostname = environ.get('SERVER_NAME')
166 try:
166 try:
167 return socket.gethostbyname(hostname)
167 return socket.gethostbyname(hostname)
168 except Exception as e:
168 except Exception as e:
169 if log_errors:
169 if log_errors:
170 # in some cases this lookup is not possible, and we don't want to
170 # in some cases this lookup is not possible, and we don't want to
171 # make it an exception in logs
171 # make it an exception in logs
172 log.exception('Could not retrieve server ip address: %s', e)
172 log.exception('Could not retrieve server ip address: %s', e)
173 return hostname
173 return hostname
174
174
175
175
176 def get_server_port(environ):
176 def get_server_port(environ):
177 return environ.get('SERVER_PORT')
177 return environ.get('SERVER_PORT')
178
178
179
179
180 def get_access_path(environ):
180 def get_access_path(environ):
181 path = environ.get('PATH_INFO')
181 path = environ.get('PATH_INFO')
182 org_req = environ.get('pylons.original_request')
182 org_req = environ.get('pylons.original_request')
183 if org_req:
183 if org_req:
184 path = org_req.environ.get('PATH_INFO')
184 path = org_req.environ.get('PATH_INFO')
185 return path
185 return path
186
186
187
187
188 def get_user_agent(environ):
188 def get_user_agent(environ):
189 return environ.get('HTTP_USER_AGENT')
189 return environ.get('HTTP_USER_AGENT')
190
190
191
191
192 def vcs_operation_context(
192 def vcs_operation_context(
193 environ, repo_name, username, action, scm, check_locking=True,
193 environ, repo_name, username, action, scm, check_locking=True,
194 is_shadow_repo=False):
194 is_shadow_repo=False):
195 """
195 """
196 Generate the context for a vcs operation, e.g. push or pull.
196 Generate the context for a vcs operation, e.g. push or pull.
197
197
198 This context is passed over the layers so that hooks triggered by the
198 This context is passed over the layers so that hooks triggered by the
199 vcs operation know details like the user, the user's IP address etc.
199 vcs operation know details like the user, the user's IP address etc.
200
200
201 :param check_locking: Allows to switch of the computation of the locking
201 :param check_locking: Allows to switch of the computation of the locking
202 data. This serves mainly the need of the simplevcs middleware to be
202 data. This serves mainly the need of the simplevcs middleware to be
203 able to disable this for certain operations.
203 able to disable this for certain operations.
204
204
205 """
205 """
206 # Tri-state value: False: unlock, None: nothing, True: lock
206 # Tri-state value: False: unlock, None: nothing, True: lock
207 make_lock = None
207 make_lock = None
208 locked_by = [None, None, None]
208 locked_by = [None, None, None]
209 is_anonymous = username == User.DEFAULT_USER
209 is_anonymous = username == User.DEFAULT_USER
210 if not is_anonymous and check_locking:
210 if not is_anonymous and check_locking:
211 log.debug('Checking locking on repository "%s"', repo_name)
211 log.debug('Checking locking on repository "%s"', repo_name)
212 user = User.get_by_username(username)
212 user = User.get_by_username(username)
213 repo = Repository.get_by_repo_name(repo_name)
213 repo = Repository.get_by_repo_name(repo_name)
214 make_lock, __, locked_by = repo.get_locking_state(
214 make_lock, __, locked_by = repo.get_locking_state(
215 action, user.user_id)
215 action, user.user_id)
216
216
217 settings_model = VcsSettingsModel(repo=repo_name)
217 settings_model = VcsSettingsModel(repo=repo_name)
218 ui_settings = settings_model.get_ui_settings()
218 ui_settings = settings_model.get_ui_settings()
219
219
220 extras = {
220 extras = {
221 'ip': get_ip_addr(environ),
221 'ip': get_ip_addr(environ),
222 'username': username,
222 'username': username,
223 'action': action,
223 'action': action,
224 'repository': repo_name,
224 'repository': repo_name,
225 'scm': scm,
225 'scm': scm,
226 'config': rhodecode.CONFIG['__file__'],
226 'config': rhodecode.CONFIG['__file__'],
227 'make_lock': make_lock,
227 'make_lock': make_lock,
228 'locked_by': locked_by,
228 'locked_by': locked_by,
229 'server_url': utils2.get_server_url(environ),
229 'server_url': utils2.get_server_url(environ),
230 'user_agent': get_user_agent(environ),
230 'user_agent': get_user_agent(environ),
231 'hooks': get_enabled_hook_classes(ui_settings),
231 'hooks': get_enabled_hook_classes(ui_settings),
232 'is_shadow_repo': is_shadow_repo,
232 'is_shadow_repo': is_shadow_repo,
233 }
233 }
234 return extras
234 return extras
235
235
236
236
237 class BasicAuth(AuthBasicAuthenticator):
237 class BasicAuth(AuthBasicAuthenticator):
238
238
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 initial_call_detection=False, acl_repo_name=None):
240 initial_call_detection=False, acl_repo_name=None):
241 self.realm = realm
241 self.realm = realm
242 self.initial_call = initial_call_detection
242 self.initial_call = initial_call_detection
243 self.authfunc = authfunc
243 self.authfunc = authfunc
244 self.registry = registry
244 self.registry = registry
245 self.acl_repo_name = acl_repo_name
245 self.acl_repo_name = acl_repo_name
246 self._rc_auth_http_code = auth_http_code
246 self._rc_auth_http_code = auth_http_code
247
247
248 def _get_response_from_code(self, http_code):
248 def _get_response_from_code(self, http_code):
249 try:
249 try:
250 return get_exception(safe_int(http_code))
250 return get_exception(safe_int(http_code))
251 except Exception:
251 except Exception:
252 log.exception('Failed to fetch response for code %s' % http_code)
252 log.exception('Failed to fetch response for code %s' % http_code)
253 return HTTPForbidden
253 return HTTPForbidden
254
254
255 def get_rc_realm(self):
255 def get_rc_realm(self):
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257
257
258 def build_authentication(self):
258 def build_authentication(self):
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 if self._rc_auth_http_code and not self.initial_call:
260 if self._rc_auth_http_code and not self.initial_call:
261 # return alternative HTTP code if alternative http return code
261 # return alternative HTTP code if alternative http return code
262 # is specified in RhodeCode config, but ONLY if it's not the
262 # is specified in RhodeCode config, but ONLY if it's not the
263 # FIRST call
263 # FIRST call
264 custom_response_klass = self._get_response_from_code(
264 custom_response_klass = self._get_response_from_code(
265 self._rc_auth_http_code)
265 self._rc_auth_http_code)
266 return custom_response_klass(headers=head)
266 return custom_response_klass(headers=head)
267 return HTTPUnauthorized(headers=head)
267 return HTTPUnauthorized(headers=head)
268
268
269 def authenticate(self, environ):
269 def authenticate(self, environ):
270 authorization = AUTHORIZATION(environ)
270 authorization = AUTHORIZATION(environ)
271 if not authorization:
271 if not authorization:
272 return self.build_authentication()
272 return self.build_authentication()
273 (authmeth, auth) = authorization.split(' ', 1)
273 (authmeth, auth) = authorization.split(' ', 1)
274 if 'basic' != authmeth.lower():
274 if 'basic' != authmeth.lower():
275 return self.build_authentication()
275 return self.build_authentication()
276 auth = auth.strip().decode('base64')
276 auth = auth.strip().decode('base64')
277 _parts = auth.split(':', 1)
277 _parts = auth.split(':', 1)
278 if len(_parts) == 2:
278 if len(_parts) == 2:
279 username, password = _parts
279 username, password = _parts
280 if self.authfunc(
280 auth_data = self.authfunc(
281 username, password, environ, VCS_TYPE,
281 username, password, environ, VCS_TYPE,
282 registry=self.registry, acl_repo_name=self.acl_repo_name):
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 return username
283 if auth_data:
284 return {'username': username, 'auth_data': auth_data}
284 if username and password:
285 if username and password:
285 # we mark that we actually executed authentication once, at
286 # we mark that we actually executed authentication once, at
286 # that point we can use the alternative auth code
287 # that point we can use the alternative auth code
287 self.initial_call = False
288 self.initial_call = False
288
289
289 return self.build_authentication()
290 return self.build_authentication()
290
291
291 __call__ = authenticate
292 __call__ = authenticate
292
293
293
294
294 def calculate_version_hash(config):
295 def calculate_version_hash(config):
295 return md5(
296 return md5(
296 config.get('beaker.session.secret', '') +
297 config.get('beaker.session.secret', '') +
297 rhodecode.__version__)[:8]
298 rhodecode.__version__)[:8]
298
299
299
300
300 def get_current_lang(request):
301 def get_current_lang(request):
301 # NOTE(marcink): remove after pyramid move
302 # NOTE(marcink): remove after pyramid move
302 try:
303 try:
303 return translation.get_lang()[0]
304 return translation.get_lang()[0]
304 except:
305 except:
305 pass
306 pass
306
307
307 return getattr(request, '_LOCALE_', request.locale_name)
308 return getattr(request, '_LOCALE_', request.locale_name)
308
309
309
310
310 def attach_context_attributes(context, request, user_id):
311 def attach_context_attributes(context, request, user_id):
311 """
312 """
312 Attach variables into template context called `c`, please note that
313 Attach variables into template context called `c`, please note that
313 request could be pylons or pyramid request in here.
314 request could be pylons or pyramid request in here.
314 """
315 """
315 # NOTE(marcink): remove check after pyramid migration
316 # NOTE(marcink): remove check after pyramid migration
316 if hasattr(request, 'registry'):
317 if hasattr(request, 'registry'):
317 config = request.registry.settings
318 config = request.registry.settings
318 else:
319 else:
319 from pylons import config
320 from pylons import config
320
321
321 rc_config = SettingsModel().get_all_settings(cache=True)
322 rc_config = SettingsModel().get_all_settings(cache=True)
322
323
323 context.rhodecode_version = rhodecode.__version__
324 context.rhodecode_version = rhodecode.__version__
324 context.rhodecode_edition = config.get('rhodecode.edition')
325 context.rhodecode_edition = config.get('rhodecode.edition')
325 # unique secret + version does not leak the version but keep consistency
326 # unique secret + version does not leak the version but keep consistency
326 context.rhodecode_version_hash = calculate_version_hash(config)
327 context.rhodecode_version_hash = calculate_version_hash(config)
327
328
328 # Default language set for the incoming request
329 # Default language set for the incoming request
329 context.language = get_current_lang(request)
330 context.language = get_current_lang(request)
330
331
331 # Visual options
332 # Visual options
332 context.visual = AttributeDict({})
333 context.visual = AttributeDict({})
333
334
334 # DB stored Visual Items
335 # DB stored Visual Items
335 context.visual.show_public_icon = str2bool(
336 context.visual.show_public_icon = str2bool(
336 rc_config.get('rhodecode_show_public_icon'))
337 rc_config.get('rhodecode_show_public_icon'))
337 context.visual.show_private_icon = str2bool(
338 context.visual.show_private_icon = str2bool(
338 rc_config.get('rhodecode_show_private_icon'))
339 rc_config.get('rhodecode_show_private_icon'))
339 context.visual.stylify_metatags = str2bool(
340 context.visual.stylify_metatags = str2bool(
340 rc_config.get('rhodecode_stylify_metatags'))
341 rc_config.get('rhodecode_stylify_metatags'))
341 context.visual.dashboard_items = safe_int(
342 context.visual.dashboard_items = safe_int(
342 rc_config.get('rhodecode_dashboard_items', 100))
343 rc_config.get('rhodecode_dashboard_items', 100))
343 context.visual.admin_grid_items = safe_int(
344 context.visual.admin_grid_items = safe_int(
344 rc_config.get('rhodecode_admin_grid_items', 100))
345 rc_config.get('rhodecode_admin_grid_items', 100))
345 context.visual.repository_fields = str2bool(
346 context.visual.repository_fields = str2bool(
346 rc_config.get('rhodecode_repository_fields'))
347 rc_config.get('rhodecode_repository_fields'))
347 context.visual.show_version = str2bool(
348 context.visual.show_version = str2bool(
348 rc_config.get('rhodecode_show_version'))
349 rc_config.get('rhodecode_show_version'))
349 context.visual.use_gravatar = str2bool(
350 context.visual.use_gravatar = str2bool(
350 rc_config.get('rhodecode_use_gravatar'))
351 rc_config.get('rhodecode_use_gravatar'))
351 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 context.visual.default_renderer = rc_config.get(
353 context.visual.default_renderer = rc_config.get(
353 'rhodecode_markup_renderer', 'rst')
354 'rhodecode_markup_renderer', 'rst')
354 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 context.visual.rhodecode_support_url = \
356 context.visual.rhodecode_support_url = \
356 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357
358
358 context.visual.affected_files_cut_off = 60
359 context.visual.affected_files_cut_off = 60
359
360
360 context.pre_code = rc_config.get('rhodecode_pre_code')
361 context.pre_code = rc_config.get('rhodecode_pre_code')
361 context.post_code = rc_config.get('rhodecode_post_code')
362 context.post_code = rc_config.get('rhodecode_post_code')
362 context.rhodecode_name = rc_config.get('rhodecode_title')
363 context.rhodecode_name = rc_config.get('rhodecode_title')
363 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 # if we have specified default_encoding in the request, it has more
365 # if we have specified default_encoding in the request, it has more
365 # priority
366 # priority
366 if request.GET.get('default_encoding'):
367 if request.GET.get('default_encoding'):
367 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369
370
370 # INI stored
371 # INI stored
371 context.labs_active = str2bool(
372 context.labs_active = str2bool(
372 config.get('labs_settings_active', 'false'))
373 config.get('labs_settings_active', 'false'))
373 context.visual.allow_repo_location_change = str2bool(
374 context.visual.allow_repo_location_change = str2bool(
374 config.get('allow_repo_location_change', True))
375 config.get('allow_repo_location_change', True))
375 context.visual.allow_custom_hooks_settings = str2bool(
376 context.visual.allow_custom_hooks_settings = str2bool(
376 config.get('allow_custom_hooks_settings', True))
377 config.get('allow_custom_hooks_settings', True))
377 context.debug_style = str2bool(config.get('debug_style', False))
378 context.debug_style = str2bool(config.get('debug_style', False))
378
379
379 context.rhodecode_instanceid = config.get('instance_id')
380 context.rhodecode_instanceid = config.get('instance_id')
380
381
381 context.visual.cut_off_limit_diff = safe_int(
382 context.visual.cut_off_limit_diff = safe_int(
382 config.get('cut_off_limit_diff'))
383 config.get('cut_off_limit_diff'))
383 context.visual.cut_off_limit_file = safe_int(
384 context.visual.cut_off_limit_file = safe_int(
384 config.get('cut_off_limit_file'))
385 config.get('cut_off_limit_file'))
385
386
386 # AppEnlight
387 # AppEnlight
387 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 context.appenlight_api_public_key = config.get(
389 context.appenlight_api_public_key = config.get(
389 'appenlight.api_public_key', '')
390 'appenlight.api_public_key', '')
390 context.appenlight_server_url = config.get('appenlight.server_url', '')
391 context.appenlight_server_url = config.get('appenlight.server_url', '')
391
392
392 # JS template context
393 # JS template context
393 context.template_context = {
394 context.template_context = {
394 'repo_name': None,
395 'repo_name': None,
395 'repo_type': None,
396 'repo_type': None,
396 'repo_landing_commit': None,
397 'repo_landing_commit': None,
397 'rhodecode_user': {
398 'rhodecode_user': {
398 'username': None,
399 'username': None,
399 'email': None,
400 'email': None,
400 'notification_status': False
401 'notification_status': False
401 },
402 },
402 'visual': {
403 'visual': {
403 'default_renderer': None
404 'default_renderer': None
404 },
405 },
405 'commit_data': {
406 'commit_data': {
406 'commit_id': None
407 'commit_id': None
407 },
408 },
408 'pull_request_data': {'pull_request_id': None},
409 'pull_request_data': {'pull_request_id': None},
409 'timeago': {
410 'timeago': {
410 'refresh_time': 120 * 1000,
411 'refresh_time': 120 * 1000,
411 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 },
413 },
413 'pyramid_dispatch': {
414 'pyramid_dispatch': {
414
415
415 },
416 },
416 'extra': {'plugins': {}}
417 'extra': {'plugins': {}}
417 }
418 }
418 # END CONFIG VARS
419 # END CONFIG VARS
419
420
420 # TODO: This dosn't work when called from pylons compatibility tween.
421 # TODO: This dosn't work when called from pylons compatibility tween.
421 # Fix this and remove it from base controller.
422 # Fix this and remove it from base controller.
422 # context.repo_name = get_repo_slug(request) # can be empty
423 # context.repo_name = get_repo_slug(request) # can be empty
423
424
424 diffmode = 'sideside'
425 diffmode = 'sideside'
425 if request.GET.get('diffmode'):
426 if request.GET.get('diffmode'):
426 if request.GET['diffmode'] == 'unified':
427 if request.GET['diffmode'] == 'unified':
427 diffmode = 'unified'
428 diffmode = 'unified'
428 elif request.session.get('diffmode'):
429 elif request.session.get('diffmode'):
429 diffmode = request.session['diffmode']
430 diffmode = request.session['diffmode']
430
431
431 context.diffmode = diffmode
432 context.diffmode = diffmode
432
433
433 if request.session.get('diffmode') != diffmode:
434 if request.session.get('diffmode') != diffmode:
434 request.session['diffmode'] = diffmode
435 request.session['diffmode'] = diffmode
435
436
436 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.backends = rhodecode.BACKENDS.keys()
438 context.backends = rhodecode.BACKENDS.keys()
438 context.backends.sort()
439 context.backends.sort()
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440
441
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # given request will ALWAYS be pyramid one
443 # given request will ALWAYS be pyramid one
443 pyramid_request = pyramid.threadlocal.get_current_request()
444 pyramid_request = pyramid.threadlocal.get_current_request()
444 context.pyramid_request = pyramid_request
445 context.pyramid_request = pyramid_request
445
446
446 # web case
447 # web case
447 if hasattr(pyramid_request, 'user'):
448 if hasattr(pyramid_request, 'user'):
448 context.auth_user = pyramid_request.user
449 context.auth_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
450 context.rhodecode_user = pyramid_request.user
450
451
451 # api case
452 # api case
452 if hasattr(pyramid_request, 'rpc_user'):
453 if hasattr(pyramid_request, 'rpc_user'):
453 context.auth_user = pyramid_request.rpc_user
454 context.auth_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
455 context.rhodecode_user = pyramid_request.rpc_user
455
456
456 # attach the whole call context to the request
457 # attach the whole call context to the request
457 request.call_context = context
458 request.call_context = context
458
459
459
460
460 def get_auth_user(request):
461 def get_auth_user(request):
461 environ = request.environ
462 environ = request.environ
462 session = request.session
463 session = request.session
463
464
464 ip_addr = get_ip_addr(environ)
465 ip_addr = get_ip_addr(environ)
465 # make sure that we update permissions each time we call controller
466 # make sure that we update permissions each time we call controller
466 _auth_token = (request.GET.get('auth_token', '') or
467 _auth_token = (request.GET.get('auth_token', '') or
467 request.GET.get('api_key', ''))
468 request.GET.get('api_key', ''))
468
469
469 if _auth_token:
470 if _auth_token:
470 # when using API_KEY we assume user exists, and
471 # when using API_KEY we assume user exists, and
471 # doesn't need auth based on cookies.
472 # doesn't need auth based on cookies.
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 authenticated = False
474 authenticated = False
474 else:
475 else:
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 try:
477 try:
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 ip_addr=ip_addr)
479 ip_addr=ip_addr)
479 except UserCreationError as e:
480 except UserCreationError as e:
480 h.flash(e, 'error')
481 h.flash(e, 'error')
481 # container auth or other auth functions that create users
482 # container auth or other auth functions that create users
482 # on the fly can throw this exception signaling that there's
483 # on the fly can throw this exception signaling that there's
483 # issue with user creation, explanation should be provided
484 # issue with user creation, explanation should be provided
484 # in Exception itself. We then create a simple blank
485 # in Exception itself. We then create a simple blank
485 # AuthUser
486 # AuthUser
486 auth_user = AuthUser(ip_addr=ip_addr)
487 auth_user = AuthUser(ip_addr=ip_addr)
487
488
488 if password_changed(auth_user, session):
489 if password_changed(auth_user, session):
489 session.invalidate()
490 session.invalidate()
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 auth_user = AuthUser(ip_addr=ip_addr)
492 auth_user = AuthUser(ip_addr=ip_addr)
492
493
493 authenticated = cookie_store.get('is_authenticated')
494 authenticated = cookie_store.get('is_authenticated')
494
495
495 if not auth_user.is_authenticated and auth_user.is_user_object:
496 if not auth_user.is_authenticated and auth_user.is_user_object:
496 # user is not authenticated and not empty
497 # user is not authenticated and not empty
497 auth_user.set_authenticated(authenticated)
498 auth_user.set_authenticated(authenticated)
498
499
499 return auth_user
500 return auth_user
500
501
501
502
502 class BaseController(WSGIController):
503 class BaseController(WSGIController):
503
504
504 def __before__(self):
505 def __before__(self):
505 """
506 """
506 __before__ is called before controller methods and after __call__
507 __before__ is called before controller methods and after __call__
507 """
508 """
508 # on each call propagate settings calls into global settings.
509 # on each call propagate settings calls into global settings.
509 from pylons import config
510 from pylons import config
510 from pylons import tmpl_context as c, request, url
511 from pylons import tmpl_context as c, request, url
511 set_rhodecode_config(config)
512 set_rhodecode_config(config)
512 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513
514
514 # TODO: Remove this when fixed in attach_context_attributes()
515 # TODO: Remove this when fixed in attach_context_attributes()
515 c.repo_name = get_repo_slug(request) # can be empty
516 c.repo_name = get_repo_slug(request) # can be empty
516
517
517 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 self.sa = meta.Session
520 self.sa = meta.Session
520 self.scm_model = ScmModel(self.sa)
521 self.scm_model = ScmModel(self.sa)
521
522
522 # set user language
523 # set user language
523 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 if user_lang:
525 if user_lang:
525 translation.set_lang(user_lang)
526 translation.set_lang(user_lang)
526 log.debug('set language to %s for user %s',
527 log.debug('set language to %s for user %s',
527 user_lang, self._rhodecode_user)
528 user_lang, self._rhodecode_user)
528
529
529 def _dispatch_redirect(self, with_url, environ, start_response):
530 def _dispatch_redirect(self, with_url, environ, start_response):
530 from webob.exc import HTTPFound
531 from webob.exc import HTTPFound
531 resp = HTTPFound(with_url)
532 resp = HTTPFound(with_url)
532 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 environ['PATH_INFO'] = with_url
534 environ['PATH_INFO'] = with_url
534 return resp(environ, start_response)
535 return resp(environ, start_response)
535
536
536 def __call__(self, environ, start_response):
537 def __call__(self, environ, start_response):
537 """Invoke the Controller"""
538 """Invoke the Controller"""
538 # WSGIController.__call__ dispatches to the Controller method
539 # WSGIController.__call__ dispatches to the Controller method
539 # the request is routed to. This routing information is
540 # the request is routed to. This routing information is
540 # available in environ['pylons.routes_dict']
541 # available in environ['pylons.routes_dict']
541 from rhodecode.lib import helpers as h
542 from rhodecode.lib import helpers as h
542 from pylons import tmpl_context as c, request, url
543 from pylons import tmpl_context as c, request, url
543
544
544 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 if environ.get('debugtoolbar.wants_pylons_context', False):
546 if environ.get('debugtoolbar.wants_pylons_context', False):
546 environ['debugtoolbar.pylons_context'] = c._current_obj()
547 environ['debugtoolbar.pylons_context'] = c._current_obj()
547
548
548 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 environ['pylons.routes_dict']['action']])
550 environ['pylons.routes_dict']['action']])
550
551
551 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 self.ip_addr = get_ip_addr(environ)
553 self.ip_addr = get_ip_addr(environ)
553
554
554 # The rhodecode auth user is looked up and passed through the
555 # The rhodecode auth user is looked up and passed through the
555 # environ by the pylons compatibility tween in pyramid.
556 # environ by the pylons compatibility tween in pyramid.
556 # So we can just grab it from there.
557 # So we can just grab it from there.
557 auth_user = environ['rc_auth_user']
558 auth_user = environ['rc_auth_user']
558
559
559 # set globals for auth user
560 # set globals for auth user
560 request.user = auth_user
561 request.user = auth_user
561 self._rhodecode_user = auth_user
562 self._rhodecode_user = auth_user
562
563
563 log.info('IP: %s User: %s accessed %s [%s]' % (
564 log.info('IP: %s User: %s accessed %s [%s]' % (
564 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 _route_name)
566 _route_name)
566 )
567 )
567
568
568 user_obj = auth_user.get_instance()
569 user_obj = auth_user.get_instance()
569 if user_obj and user_obj.user_data.get('force_password_change'):
570 if user_obj and user_obj.user_data.get('force_password_change'):
570 h.flash('You are required to change your password', 'warning',
571 h.flash('You are required to change your password', 'warning',
571 ignore_duplicate=True)
572 ignore_duplicate=True)
572 return self._dispatch_redirect(
573 return self._dispatch_redirect(
573 url('my_account_password'), environ, start_response)
574 url('my_account_password'), environ, start_response)
574
575
575 return WSGIController.__call__(self, environ, start_response)
576 return WSGIController.__call__(self, environ, start_response)
576
577
577
578
578 def h_filter(s):
579 def h_filter(s):
579 """
580 """
580 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 we wrap this with additional functionality that converts None to empty
582 we wrap this with additional functionality that converts None to empty
582 strings
583 strings
583 """
584 """
584 if s is None:
585 if s is None:
585 return markupsafe.Markup()
586 return markupsafe.Markup()
586 return markupsafe.escape(s)
587 return markupsafe.escape(s)
587
588
588
589
589 def add_events_routes(config):
590 def add_events_routes(config):
590 """
591 """
591 Adds routing that can be used in events. Because some events are triggered
592 Adds routing that can be used in events. Because some events are triggered
592 outside of pyramid context, we need to bootstrap request with some
593 outside of pyramid context, we need to bootstrap request with some
593 routing registered
594 routing registered
594 """
595 """
595 config.add_route(name='home', pattern='/')
596 config.add_route(name='home', pattern='/')
596
597
597 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600
601
601 config.add_route(name='pullrequest_show',
602 config.add_route(name='pullrequest_show',
602 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 config.add_route(name='pull_requests_global',
604 config.add_route(name='pull_requests_global',
604 pattern='/pull-request/{pull_request_id}')
605 pattern='/pull-request/{pull_request_id}')
605
606
606 config.add_route(name='repo_commit',
607 config.add_route(name='repo_commit',
607 pattern='/{repo_name}/changeset/{commit_id}')
608 pattern='/{repo_name}/changeset/{commit_id}')
608 config.add_route(name='repo_files',
609 config.add_route(name='repo_files',
609 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610
611
611
612
612 def bootstrap_request(**kwargs):
613 def bootstrap_request(**kwargs):
613 import pyramid.testing
614 import pyramid.testing
614
615
615 class TestRequest(pyramid.testing.DummyRequest):
616 class TestRequest(pyramid.testing.DummyRequest):
616 application_url = kwargs.pop('application_url', 'http://example.com')
617 application_url = kwargs.pop('application_url', 'http://example.com')
617 host = kwargs.pop('host', 'example.com:80')
618 host = kwargs.pop('host', 'example.com:80')
618 domain = kwargs.pop('domain', 'example.com')
619 domain = kwargs.pop('domain', 'example.com')
619
620
620 class TestDummySession(pyramid.testing.DummySession):
621 class TestDummySession(pyramid.testing.DummySession):
621 def save(*arg, **kw):
622 def save(*arg, **kw):
622 pass
623 pass
623
624
624 request = TestRequest(**kwargs)
625 request = TestRequest(**kwargs)
625 request.session = TestDummySession()
626 request.session = TestDummySession()
626
627
627 config = pyramid.testing.setUp(request=request)
628 config = pyramid.testing.setUp(request=request)
628 add_events_routes(config)
629 add_events_routes(config)
629 return request
630 return request
630
631
@@ -1,540 +1,599 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import logging
28 import logging
28 import importlib
29 import importlib
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
34 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
36
36 import rhodecode
37 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.authentication.base import (
39 authenticate, get_perms_cache_manager, VCS_TYPE)
40 from rhodecode.lib import caches
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
41 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
43 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
41 from rhodecode.lib.exceptions import (
44 from rhodecode.lib.exceptions import (
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
45 HTTPLockedRC, HTTPRequirementError, UserCreationError,
43 NotAllowedToCreateUserError)
46 NotAllowedToCreateUserError)
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
47 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 from rhodecode.lib.middleware import appenlight
48 from rhodecode.lib.middleware import appenlight
46 from rhodecode.lib.middleware.utils import scm_app_http
49 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
50 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
48 is_valid_repo, get_rhodecode_base_path, SLUG_RE)
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
51 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.backends import base
53 from rhodecode.lib.vcs.backends import base
52 from rhodecode.model import meta
54 from rhodecode.model import meta
53 from rhodecode.model.db import User, Repository, PullRequest
55 from rhodecode.model.db import User, Repository, PullRequest
54 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.pull_request import PullRequestModel
57 from rhodecode.model.pull_request import PullRequestModel
56 from rhodecode.model.settings import SettingsModel
58 from rhodecode.model.settings import SettingsModel
57
59
58 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
59
61
60
62
61 def initialize_generator(factory):
63 def initialize_generator(factory):
62 """
64 """
63 Initializes the returned generator by draining its first element.
65 Initializes the returned generator by draining its first element.
64
66
65 This can be used to give a generator an initializer, which is the code
67 This can be used to give a generator an initializer, which is the code
66 up to the first yield statement. This decorator enforces that the first
68 up to the first yield statement. This decorator enforces that the first
67 produced element has the value ``"__init__"`` to make its special
69 produced element has the value ``"__init__"`` to make its special
68 purpose very explicit in the using code.
70 purpose very explicit in the using code.
69 """
71 """
70
72
71 @wraps(factory)
73 @wraps(factory)
72 def wrapper(*args, **kwargs):
74 def wrapper(*args, **kwargs):
73 gen = factory(*args, **kwargs)
75 gen = factory(*args, **kwargs)
74 try:
76 try:
75 init = gen.next()
77 init = gen.next()
76 except StopIteration:
78 except StopIteration:
77 raise ValueError('Generator must yield at least one element.')
79 raise ValueError('Generator must yield at least one element.')
78 if init != "__init__":
80 if init != "__init__":
79 raise ValueError('First yielded element must be "__init__".')
81 raise ValueError('First yielded element must be "__init__".')
80 return gen
82 return gen
81 return wrapper
83 return wrapper
82
84
83
85
84 class SimpleVCS(object):
86 class SimpleVCS(object):
85 """Common functionality for SCM HTTP handlers."""
87 """Common functionality for SCM HTTP handlers."""
86
88
87 SCM = 'unknown'
89 SCM = 'unknown'
88
90
89 acl_repo_name = None
91 acl_repo_name = None
90 url_repo_name = None
92 url_repo_name = None
91 vcs_repo_name = None
93 vcs_repo_name = None
92
94
93 # We have to handle requests to shadow repositories different than requests
95 # We have to handle requests to shadow repositories different than requests
94 # to normal repositories. Therefore we have to distinguish them. To do this
96 # to normal repositories. Therefore we have to distinguish them. To do this
95 # we use this regex which will match only on URLs pointing to shadow
97 # we use this regex which will match only on URLs pointing to shadow
96 # repositories.
98 # repositories.
97 shadow_repo_re = re.compile(
99 shadow_repo_re = re.compile(
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
100 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
99 '(?P<target>{slug_pat})/' # target repo
101 '(?P<target>{slug_pat})/' # target repo
100 'pull-request/(?P<pr_id>\d+)/' # pull request
102 'pull-request/(?P<pr_id>\d+)/' # pull request
101 'repository$' # shadow repo
103 'repository$' # shadow repo
102 .format(slug_pat=SLUG_RE.pattern))
104 .format(slug_pat=SLUG_RE.pattern))
103
105
104 def __init__(self, application, config, registry):
106 def __init__(self, application, config, registry):
105 self.registry = registry
107 self.registry = registry
106 self.application = application
108 self.application = application
107 self.config = config
109 self.config = config
108 # re-populated by specialized middleware
110 # re-populated by specialized middleware
109 self.repo_vcs_config = base.Config()
111 self.repo_vcs_config = base.Config()
110 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
111 self.basepath = rhodecode.CONFIG['base_path']
113 self.basepath = rhodecode.CONFIG['base_path']
112 registry.rhodecode_settings = self.rhodecode_settings
114 registry.rhodecode_settings = self.rhodecode_settings
113 # authenticate this VCS request using authfunc
115 # authenticate this VCS request using authfunc
114 auth_ret_code_detection = \
116 auth_ret_code_detection = \
115 str2bool(self.config.get('auth_ret_code_detection', False))
117 str2bool(self.config.get('auth_ret_code_detection', False))
116 self.authenticate = BasicAuth(
118 self.authenticate = BasicAuth(
117 '', authenticate, registry, config.get('auth_ret_code'),
119 '', authenticate, registry, config.get('auth_ret_code'),
118 auth_ret_code_detection)
120 auth_ret_code_detection)
119 self.ip_addr = '0.0.0.0'
121 self.ip_addr = '0.0.0.0'
120
122
121 def set_repo_names(self, environ):
123 def set_repo_names(self, environ):
122 """
124 """
123 This will populate the attributes acl_repo_name, url_repo_name,
125 This will populate the attributes acl_repo_name, url_repo_name,
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
126 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
125 shadow) repositories all names are equal. In case of requests to a
127 shadow) repositories all names are equal. In case of requests to a
126 shadow repository the acl-name points to the target repo of the pull
128 shadow repository the acl-name points to the target repo of the pull
127 request and the vcs-name points to the shadow repo file system path.
129 request and the vcs-name points to the shadow repo file system path.
128 The url-name is always the URL used by the vcs client program.
130 The url-name is always the URL used by the vcs client program.
129
131
130 Example in case of a shadow repo:
132 Example in case of a shadow repo:
131 acl_repo_name = RepoGroup/MyRepo
133 acl_repo_name = RepoGroup/MyRepo
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
134 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
135 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
134 """
136 """
135 # First we set the repo name from URL for all attributes. This is the
137 # First we set the repo name from URL for all attributes. This is the
136 # default if handling normal (non shadow) repo requests.
138 # default if handling normal (non shadow) repo requests.
137 self.url_repo_name = self._get_repository_name(environ)
139 self.url_repo_name = self._get_repository_name(environ)
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
140 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
139 self.is_shadow_repo = False
141 self.is_shadow_repo = False
140
142
141 # Check if this is a request to a shadow repository.
143 # Check if this is a request to a shadow repository.
142 match = self.shadow_repo_re.match(self.url_repo_name)
144 match = self.shadow_repo_re.match(self.url_repo_name)
143 if match:
145 if match:
144 match_dict = match.groupdict()
146 match_dict = match.groupdict()
145
147
146 # Build acl repo name from regex match.
148 # Build acl repo name from regex match.
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
149 acl_repo_name = safe_unicode('{groups}{target}'.format(
148 groups=match_dict['groups'] or '',
150 groups=match_dict['groups'] or '',
149 target=match_dict['target']))
151 target=match_dict['target']))
150
152
151 # Retrieve pull request instance by ID from regex match.
153 # Retrieve pull request instance by ID from regex match.
152 pull_request = PullRequest.get(match_dict['pr_id'])
154 pull_request = PullRequest.get(match_dict['pr_id'])
153
155
154 # Only proceed if we got a pull request and if acl repo name from
156 # Only proceed if we got a pull request and if acl repo name from
155 # URL equals the target repo name of the pull request.
157 # URL equals the target repo name of the pull request.
156 if pull_request and (acl_repo_name ==
158 if pull_request and (acl_repo_name ==
157 pull_request.target_repo.repo_name):
159 pull_request.target_repo.repo_name):
158 # Get file system path to shadow repository.
160 # Get file system path to shadow repository.
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
161 workspace_id = PullRequestModel()._workspace_id(pull_request)
160 target_vcs = pull_request.target_repo.scm_instance()
162 target_vcs = pull_request.target_repo.scm_instance()
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
163 vcs_repo_name = target_vcs._get_shadow_repository_path(
162 workspace_id)
164 workspace_id)
163
165
164 # Store names for later usage.
166 # Store names for later usage.
165 self.vcs_repo_name = vcs_repo_name
167 self.vcs_repo_name = vcs_repo_name
166 self.acl_repo_name = acl_repo_name
168 self.acl_repo_name = acl_repo_name
167 self.is_shadow_repo = True
169 self.is_shadow_repo = True
168
170
169 log.debug('Setting all VCS repository names: %s', {
171 log.debug('Setting all VCS repository names: %s', {
170 'acl_repo_name': self.acl_repo_name,
172 'acl_repo_name': self.acl_repo_name,
171 'url_repo_name': self.url_repo_name,
173 'url_repo_name': self.url_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
174 'vcs_repo_name': self.vcs_repo_name,
173 })
175 })
174
176
175 @property
177 @property
176 def scm_app(self):
178 def scm_app(self):
177 custom_implementation = self.config['vcs.scm_app_implementation']
179 custom_implementation = self.config['vcs.scm_app_implementation']
178 if custom_implementation == 'http':
180 if custom_implementation == 'http':
179 log.info('Using HTTP implementation of scm app.')
181 log.info('Using HTTP implementation of scm app.')
180 scm_app_impl = scm_app_http
182 scm_app_impl = scm_app_http
181 else:
183 else:
182 log.info('Using custom implementation of scm_app: "{}"'.format(
184 log.info('Using custom implementation of scm_app: "{}"'.format(
183 custom_implementation))
185 custom_implementation))
184 scm_app_impl = importlib.import_module(custom_implementation)
186 scm_app_impl = importlib.import_module(custom_implementation)
185 return scm_app_impl
187 return scm_app_impl
186
188
187 def _get_by_id(self, repo_name):
189 def _get_by_id(self, repo_name):
188 """
190 """
189 Gets a special pattern _<ID> from clone url and tries to replace it
191 Gets a special pattern _<ID> from clone url and tries to replace it
190 with a repository_name for support of _<ID> non changeable urls
192 with a repository_name for support of _<ID> non changeable urls
191 """
193 """
192
194
193 data = repo_name.split('/')
195 data = repo_name.split('/')
194 if len(data) >= 2:
196 if len(data) >= 2:
195 from rhodecode.model.repo import RepoModel
197 from rhodecode.model.repo import RepoModel
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
198 by_id_match = RepoModel().get_repo_by_id(repo_name)
197 if by_id_match:
199 if by_id_match:
198 data[1] = by_id_match.repo_name
200 data[1] = by_id_match.repo_name
199
201
200 return safe_str('/'.join(data))
202 return safe_str('/'.join(data))
201
203
202 def _invalidate_cache(self, repo_name):
204 def _invalidate_cache(self, repo_name):
203 """
205 """
204 Set's cache for this repository for invalidation on next access
206 Set's cache for this repository for invalidation on next access
205
207
206 :param repo_name: full repo name, also a cache key
208 :param repo_name: full repo name, also a cache key
207 """
209 """
208 ScmModel().mark_for_invalidation(repo_name)
210 ScmModel().mark_for_invalidation(repo_name)
209
211
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
212 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
211 db_repo = Repository.get_by_repo_name(repo_name)
213 db_repo = Repository.get_by_repo_name(repo_name)
212 if not db_repo:
214 if not db_repo:
213 log.debug('Repository `%s` not found inside the database.',
215 log.debug('Repository `%s` not found inside the database.',
214 repo_name)
216 repo_name)
215 return False
217 return False
216
218
217 if db_repo.repo_type != scm_type:
219 if db_repo.repo_type != scm_type:
218 log.warning(
220 log.warning(
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
221 'Repository `%s` have incorrect scm_type, expected %s got %s',
220 repo_name, db_repo.repo_type, scm_type)
222 repo_name, db_repo.repo_type, scm_type)
221 return False
223 return False
222
224
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
225 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
224
226
225 def valid_and_active_user(self, user):
227 def valid_and_active_user(self, user):
226 """
228 """
227 Checks if that user is not empty, and if it's actually object it checks
229 Checks if that user is not empty, and if it's actually object it checks
228 if he's active.
230 if he's active.
229
231
230 :param user: user object or None
232 :param user: user object or None
231 :return: boolean
233 :return: boolean
232 """
234 """
233 if user is None:
235 if user is None:
234 return False
236 return False
235
237
236 elif user.active:
238 elif user.active:
237 return True
239 return True
238
240
239 return False
241 return False
240
242
241 @property
243 @property
242 def is_shadow_repo_dir(self):
244 def is_shadow_repo_dir(self):
243 return os.path.isdir(self.vcs_repo_name)
245 return os.path.isdir(self.vcs_repo_name)
244
246
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
247 def _check_permission(self, action, user, repo_name, ip_addr=None,
248 plugin_id='', plugin_cache_active=False, cache_ttl=0):
246 """
249 """
247 Checks permissions using action (push/pull) user and repository
250 Checks permissions using action (push/pull) user and repository
248 name
251 name. If plugin_cache and ttl is set it will use the plugin which
252 authenticated the user to store the cached permissions result for N
253 amount of seconds as in cache_ttl
249
254
250 :param action: push or pull action
255 :param action: push or pull action
251 :param user: user instance
256 :param user: user instance
252 :param repo_name: repository name
257 :param repo_name: repository name
253 """
258 """
259
260 # get instance of cache manager configured for a namespace
261 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
262 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
263 plugin_id, plugin_cache_active, cache_ttl)
264
265 # for environ based password can be empty, but then the validation is
266 # on the server that fills in the env data needed for authentication
267 _perm_calc_hash = caches.compute_key_from_params(
268 plugin_id, action, user.user_id, repo_name, ip_addr)
269
270 # _authenticate is a wrapper for .auth() method of plugin.
271 # it checks if .auth() sends proper data.
272 # For RhodeCodeExternalAuthPlugin it also maps users to
273 # Database and maps the attributes returned from .auth()
274 # to RhodeCode database. If this function returns data
275 # then auth is correct.
276 start = time.time()
277 log.debug('Running plugin `%s` permissions check', plugin_id)
278
279 def perm_func():
280 """
281 This function is used internally in Cache of Beaker to calculate
282 Results
283 """
284 log.debug('auth: calculating permission access now...')
254 # check IP
285 # check IP
255 inherit = user.inherit_default_permissions
286 inherit = user.inherit_default_permissions
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
287 ip_allowed = AuthUser.check_ip_allowed(
257 inherit_from_default=inherit)
288 user.user_id, ip_addr, inherit_from_default=inherit)
258 if ip_allowed:
289 if ip_allowed:
259 log.info('Access for IP:%s allowed', ip_addr)
290 log.info('Access for IP:%s allowed', ip_addr)
260 else:
291 else:
261 return False
292 return False
262
293
263 if action == 'push':
294 if action == 'push':
264 if not HasPermissionAnyMiddleware('repository.write',
295 perms = ('repository.write', 'repository.admin')
265 'repository.admin')(user,
296 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
266 repo_name):
267 return False
297 return False
268
298
269 else:
299 else:
270 # any other action need at least read permission
300 # any other action need at least read permission
271 if not HasPermissionAnyMiddleware('repository.read',
301 perms = (
272 'repository.write',
302 'repository.read', 'repository.write', 'repository.admin')
273 'repository.admin')(user,
303 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
274 repo_name):
275 return False
304 return False
276
305
277 return True
306 return True
278
307
308 if plugin_cache_active:
309 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
310 perm_result = cache_manager.get(
311 _perm_calc_hash, createfunc=perm_func)
312 else:
313 perm_result = perm_func()
314
315 auth_time = time.time() - start
316 log.debug('Permissions for plugin `%s` completed in %.3fs, '
317 'expiration time of fetched cache %.1fs.',
318 plugin_id, auth_time, cache_ttl)
319
320 return perm_result
321
279 def _check_ssl(self, environ, start_response):
322 def _check_ssl(self, environ, start_response):
280 """
323 """
281 Checks the SSL check flag and returns False if SSL is not present
324 Checks the SSL check flag and returns False if SSL is not present
282 and required True otherwise
325 and required True otherwise
283 """
326 """
284 org_proto = environ['wsgi._org_proto']
327 org_proto = environ['wsgi._org_proto']
285 # check if we have SSL required ! if not it's a bad request !
328 # check if we have SSL required ! if not it's a bad request !
286 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
329 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
287 if require_ssl and org_proto == 'http':
330 if require_ssl and org_proto == 'http':
288 log.debug('proto is %s and SSL is required BAD REQUEST !',
331 log.debug('proto is %s and SSL is required BAD REQUEST !',
289 org_proto)
332 org_proto)
290 return False
333 return False
291 return True
334 return True
292
335
293 def __call__(self, environ, start_response):
336 def __call__(self, environ, start_response):
294 try:
337 try:
295 return self._handle_request(environ, start_response)
338 return self._handle_request(environ, start_response)
296 except Exception:
339 except Exception:
297 log.exception("Exception while handling request")
340 log.exception("Exception while handling request")
298 appenlight.track_exception(environ)
341 appenlight.track_exception(environ)
299 return HTTPInternalServerError()(environ, start_response)
342 return HTTPInternalServerError()(environ, start_response)
300 finally:
343 finally:
301 meta.Session.remove()
344 meta.Session.remove()
302
345
303 def _handle_request(self, environ, start_response):
346 def _handle_request(self, environ, start_response):
304
347
305 if not self._check_ssl(environ, start_response):
348 if not self._check_ssl(environ, start_response):
306 reason = ('SSL required, while RhodeCode was unable '
349 reason = ('SSL required, while RhodeCode was unable '
307 'to detect this as SSL request')
350 'to detect this as SSL request')
308 log.debug('User not allowed to proceed, %s', reason)
351 log.debug('User not allowed to proceed, %s', reason)
309 return HTTPNotAcceptable(reason)(environ, start_response)
352 return HTTPNotAcceptable(reason)(environ, start_response)
310
353
311 if not self.url_repo_name:
354 if not self.url_repo_name:
312 log.warning('Repository name is empty: %s', self.url_repo_name)
355 log.warning('Repository name is empty: %s', self.url_repo_name)
313 # failed to get repo name, we fail now
356 # failed to get repo name, we fail now
314 return HTTPNotFound()(environ, start_response)
357 return HTTPNotFound()(environ, start_response)
315 log.debug('Extracted repo name is %s', self.url_repo_name)
358 log.debug('Extracted repo name is %s', self.url_repo_name)
316
359
317 ip_addr = get_ip_addr(environ)
360 ip_addr = get_ip_addr(environ)
318 user_agent = get_user_agent(environ)
361 user_agent = get_user_agent(environ)
319 username = None
362 username = None
320
363
321 # skip passing error to error controller
364 # skip passing error to error controller
322 environ['pylons.status_code_redirect'] = True
365 environ['pylons.status_code_redirect'] = True
323
366
324 # ======================================================================
367 # ======================================================================
325 # GET ACTION PULL or PUSH
368 # GET ACTION PULL or PUSH
326 # ======================================================================
369 # ======================================================================
327 action = self._get_action(environ)
370 action = self._get_action(environ)
328
371
329 # ======================================================================
372 # ======================================================================
330 # Check if this is a request to a shadow repository of a pull request.
373 # Check if this is a request to a shadow repository of a pull request.
331 # In this case only pull action is allowed.
374 # In this case only pull action is allowed.
332 # ======================================================================
375 # ======================================================================
333 if self.is_shadow_repo and action != 'pull':
376 if self.is_shadow_repo and action != 'pull':
334 reason = 'Only pull action is allowed for shadow repositories.'
377 reason = 'Only pull action is allowed for shadow repositories.'
335 log.debug('User not allowed to proceed, %s', reason)
378 log.debug('User not allowed to proceed, %s', reason)
336 return HTTPNotAcceptable(reason)(environ, start_response)
379 return HTTPNotAcceptable(reason)(environ, start_response)
337
380
338 # Check if the shadow repo actually exists, in case someone refers
381 # Check if the shadow repo actually exists, in case someone refers
339 # to it, and it has been deleted because of successful merge.
382 # to it, and it has been deleted because of successful merge.
340 if self.is_shadow_repo and not self.is_shadow_repo_dir:
383 if self.is_shadow_repo and not self.is_shadow_repo_dir:
341 return HTTPNotFound()(environ, start_response)
384 return HTTPNotFound()(environ, start_response)
342
385
343 # ======================================================================
386 # ======================================================================
344 # CHECK ANONYMOUS PERMISSION
387 # CHECK ANONYMOUS PERMISSION
345 # ======================================================================
388 # ======================================================================
346 if action in ['pull', 'push']:
389 if action in ['pull', 'push']:
347 anonymous_user = User.get_default_user()
390 anonymous_user = User.get_default_user()
348 username = anonymous_user.username
391 username = anonymous_user.username
349 if anonymous_user.active:
392 if anonymous_user.active:
350 # ONLY check permissions if the user is activated
393 # ONLY check permissions if the user is activated
351 anonymous_perm = self._check_permission(
394 anonymous_perm = self._check_permission(
352 action, anonymous_user, self.acl_repo_name, ip_addr)
395 action, anonymous_user, self.acl_repo_name, ip_addr)
353 else:
396 else:
354 anonymous_perm = False
397 anonymous_perm = False
355
398
356 if not anonymous_user.active or not anonymous_perm:
399 if not anonymous_user.active or not anonymous_perm:
357 if not anonymous_user.active:
400 if not anonymous_user.active:
358 log.debug('Anonymous access is disabled, running '
401 log.debug('Anonymous access is disabled, running '
359 'authentication')
402 'authentication')
360
403
361 if not anonymous_perm:
404 if not anonymous_perm:
362 log.debug('Not enough credentials to access this '
405 log.debug('Not enough credentials to access this '
363 'repository as anonymous user')
406 'repository as anonymous user')
364
407
365 username = None
408 username = None
366 # ==============================================================
409 # ==============================================================
367 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
410 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
368 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
411 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
369 # ==============================================================
412 # ==============================================================
370
413
371 # try to auth based on environ, container auth methods
414 # try to auth based on environ, container auth methods
372 log.debug('Running PRE-AUTH for container based authentication')
415 log.debug('Running PRE-AUTH for container based authentication')
373 pre_auth = authenticate(
416 pre_auth = authenticate(
374 '', '', environ, VCS_TYPE, registry=self.registry,
417 '', '', environ, VCS_TYPE, registry=self.registry,
375 acl_repo_name=self.acl_repo_name)
418 acl_repo_name=self.acl_repo_name)
376 if pre_auth and pre_auth.get('username'):
419 if pre_auth and pre_auth.get('username'):
377 username = pre_auth['username']
420 username = pre_auth['username']
378 log.debug('PRE-AUTH got %s as username', username)
421 log.debug('PRE-AUTH got %s as username', username)
422 if pre_auth:
423 log.debug('PRE-AUTH successful from %s',
424 pre_auth.get('auth_data', {}).get('_plugin'))
379
425
380 # If not authenticated by the container, running basic auth
426 # If not authenticated by the container, running basic auth
381 # before inject the calling repo_name for special scope checks
427 # before inject the calling repo_name for special scope checks
382 self.authenticate.acl_repo_name = self.acl_repo_name
428 self.authenticate.acl_repo_name = self.acl_repo_name
429
430 plugin_cache_active, cache_ttl = False, 0
431 plugin = None
383 if not username:
432 if not username:
384 self.authenticate.realm = self.authenticate.get_rc_realm()
433 self.authenticate.realm = self.authenticate.get_rc_realm()
385
434
386 try:
435 try:
387 result = self.authenticate(environ)
436 auth_result = self.authenticate(environ)
388 except (UserCreationError, NotAllowedToCreateUserError) as e:
437 except (UserCreationError, NotAllowedToCreateUserError) as e:
389 log.error(e)
438 log.error(e)
390 reason = safe_str(e)
439 reason = safe_str(e)
391 return HTTPNotAcceptable(reason)(environ, start_response)
440 return HTTPNotAcceptable(reason)(environ, start_response)
392
441
393 if isinstance(result, str):
442 if isinstance(auth_result, dict):
394 AUTH_TYPE.update(environ, 'basic')
443 AUTH_TYPE.update(environ, 'basic')
395 REMOTE_USER.update(environ, result)
444 REMOTE_USER.update(environ, auth_result['username'])
396 username = result
445 username = auth_result['username']
446 plugin = auth_result.get('auth_data', {}).get('_plugin')
447 log.info(
448 'MAIN-AUTH successful for user `%s` from %s plugin',
449 username, plugin)
450
451 plugin_cache_active, cache_ttl = auth_result.get(
452 'auth_data', {}).get('_ttl_cache') or (False, 0)
397 else:
453 else:
398 return result.wsgi_application(environ, start_response)
454 return auth_result.wsgi_application(
455 environ, start_response)
456
399
457
400 # ==============================================================
458 # ==============================================================
401 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
459 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
402 # ==============================================================
460 # ==============================================================
403 user = User.get_by_username(username)
461 user = User.get_by_username(username)
404 if not self.valid_and_active_user(user):
462 if not self.valid_and_active_user(user):
405 return HTTPForbidden()(environ, start_response)
463 return HTTPForbidden()(environ, start_response)
406 username = user.username
464 username = user.username
407 user.update_lastactivity()
465 user.update_lastactivity()
408 meta.Session().commit()
466 meta.Session().commit()
409
467
410 # check user attributes for password change flag
468 # check user attributes for password change flag
411 user_obj = user
469 user_obj = user
412 if user_obj and user_obj.username != User.DEFAULT_USER and \
470 if user_obj and user_obj.username != User.DEFAULT_USER and \
413 user_obj.user_data.get('force_password_change'):
471 user_obj.user_data.get('force_password_change'):
414 reason = 'password change required'
472 reason = 'password change required'
415 log.debug('User not allowed to authenticate, %s', reason)
473 log.debug('User not allowed to authenticate, %s', reason)
416 return HTTPNotAcceptable(reason)(environ, start_response)
474 return HTTPNotAcceptable(reason)(environ, start_response)
417
475
418 # check permissions for this repository
476 # check permissions for this repository
419 perm = self._check_permission(
477 perm = self._check_permission(
420 action, user, self.acl_repo_name, ip_addr)
478 action, user, self.acl_repo_name, ip_addr,
479 plugin, plugin_cache_active, cache_ttl)
421 if not perm:
480 if not perm:
422 return HTTPForbidden()(environ, start_response)
481 return HTTPForbidden()(environ, start_response)
423
482
424 # extras are injected into UI object and later available
483 # extras are injected into UI object and later available
425 # in hooks executed by rhodecode
484 # in hooks executed by RhodeCode
426 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
485 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
427 extras = vcs_operation_context(
486 extras = vcs_operation_context(
428 environ, repo_name=self.acl_repo_name, username=username,
487 environ, repo_name=self.acl_repo_name, username=username,
429 action=action, scm=self.SCM, check_locking=check_locking,
488 action=action, scm=self.SCM, check_locking=check_locking,
430 is_shadow_repo=self.is_shadow_repo
489 is_shadow_repo=self.is_shadow_repo
431 )
490 )
432
491
433 # ======================================================================
492 # ======================================================================
434 # REQUEST HANDLING
493 # REQUEST HANDLING
435 # ======================================================================
494 # ======================================================================
436 repo_path = os.path.join(
495 repo_path = os.path.join(
437 safe_str(self.basepath), safe_str(self.vcs_repo_name))
496 safe_str(self.basepath), safe_str(self.vcs_repo_name))
438 log.debug('Repository path is %s', repo_path)
497 log.debug('Repository path is %s', repo_path)
439
498
440 fix_PATH()
499 fix_PATH()
441
500
442 log.info(
501 log.info(
443 '%s action on %s repo "%s" by "%s" from %s %s',
502 '%s action on %s repo "%s" by "%s" from %s %s',
444 action, self.SCM, safe_str(self.url_repo_name),
503 action, self.SCM, safe_str(self.url_repo_name),
445 safe_str(username), ip_addr, user_agent)
504 safe_str(username), ip_addr, user_agent)
446
505
447 return self._generate_vcs_response(
506 return self._generate_vcs_response(
448 environ, start_response, repo_path, extras, action)
507 environ, start_response, repo_path, extras, action)
449
508
450 @initialize_generator
509 @initialize_generator
451 def _generate_vcs_response(
510 def _generate_vcs_response(
452 self, environ, start_response, repo_path, extras, action):
511 self, environ, start_response, repo_path, extras, action):
453 """
512 """
454 Returns a generator for the response content.
513 Returns a generator for the response content.
455
514
456 This method is implemented as a generator, so that it can trigger
515 This method is implemented as a generator, so that it can trigger
457 the cache validation after all content sent back to the client. It
516 the cache validation after all content sent back to the client. It
458 also handles the locking exceptions which will be triggered when
517 also handles the locking exceptions which will be triggered when
459 the first chunk is produced by the underlying WSGI application.
518 the first chunk is produced by the underlying WSGI application.
460 """
519 """
461 callback_daemon, extras = self._prepare_callback_daemon(extras)
520 callback_daemon, extras = self._prepare_callback_daemon(extras)
462 config = self._create_config(extras, self.acl_repo_name)
521 config = self._create_config(extras, self.acl_repo_name)
463 log.debug('HOOKS extras is %s', extras)
522 log.debug('HOOKS extras is %s', extras)
464 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
523 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
465
524
466 try:
525 try:
467 with callback_daemon:
526 with callback_daemon:
468 try:
527 try:
469 response = app(environ, start_response)
528 response = app(environ, start_response)
470 finally:
529 finally:
471 # This statement works together with the decorator
530 # This statement works together with the decorator
472 # "initialize_generator" above. The decorator ensures that
531 # "initialize_generator" above. The decorator ensures that
473 # we hit the first yield statement before the generator is
532 # we hit the first yield statement before the generator is
474 # returned back to the WSGI server. This is needed to
533 # returned back to the WSGI server. This is needed to
475 # ensure that the call to "app" above triggers the
534 # ensure that the call to "app" above triggers the
476 # needed callback to "start_response" before the
535 # needed callback to "start_response" before the
477 # generator is actually used.
536 # generator is actually used.
478 yield "__init__"
537 yield "__init__"
479
538
480 for chunk in response:
539 for chunk in response:
481 yield chunk
540 yield chunk
482 except Exception as exc:
541 except Exception as exc:
483 # TODO: martinb: Exceptions are only raised in case of the Pyro4
542 # TODO: martinb: Exceptions are only raised in case of the Pyro4
484 # backend. Refactor this except block after dropping Pyro4 support.
543 # backend. Refactor this except block after dropping Pyro4 support.
485 # TODO: johbo: Improve "translating" back the exception.
544 # TODO: johbo: Improve "translating" back the exception.
486 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
545 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
487 exc = HTTPLockedRC(*exc.args)
546 exc = HTTPLockedRC(*exc.args)
488 _code = rhodecode.CONFIG.get('lock_ret_code')
547 _code = rhodecode.CONFIG.get('lock_ret_code')
489 log.debug('Repository LOCKED ret code %s!', (_code,))
548 log.debug('Repository LOCKED ret code %s!', (_code,))
490 elif getattr(exc, '_vcs_kind', None) == 'requirement':
549 elif getattr(exc, '_vcs_kind', None) == 'requirement':
491 log.debug(
550 log.debug(
492 'Repository requires features unknown to this Mercurial')
551 'Repository requires features unknown to this Mercurial')
493 exc = HTTPRequirementError(*exc.args)
552 exc = HTTPRequirementError(*exc.args)
494 else:
553 else:
495 raise
554 raise
496
555
497 for chunk in exc(environ, start_response):
556 for chunk in exc(environ, start_response):
498 yield chunk
557 yield chunk
499 finally:
558 finally:
500 # invalidate cache on push
559 # invalidate cache on push
501 try:
560 try:
502 if action == 'push':
561 if action == 'push':
503 self._invalidate_cache(self.url_repo_name)
562 self._invalidate_cache(self.url_repo_name)
504 finally:
563 finally:
505 meta.Session.remove()
564 meta.Session.remove()
506
565
507 def _get_repository_name(self, environ):
566 def _get_repository_name(self, environ):
508 """Get repository name out of the environmnent
567 """Get repository name out of the environmnent
509
568
510 :param environ: WSGI environment
569 :param environ: WSGI environment
511 """
570 """
512 raise NotImplementedError()
571 raise NotImplementedError()
513
572
514 def _get_action(self, environ):
573 def _get_action(self, environ):
515 """Map request commands into a pull or push command.
574 """Map request commands into a pull or push command.
516
575
517 :param environ: WSGI environment
576 :param environ: WSGI environment
518 """
577 """
519 raise NotImplementedError()
578 raise NotImplementedError()
520
579
521 def _create_wsgi_app(self, repo_path, repo_name, config):
580 def _create_wsgi_app(self, repo_path, repo_name, config):
522 """Return the WSGI app that will finally handle the request."""
581 """Return the WSGI app that will finally handle the request."""
523 raise NotImplementedError()
582 raise NotImplementedError()
524
583
525 def _create_config(self, extras, repo_name):
584 def _create_config(self, extras, repo_name):
526 """Create a safe config representation."""
585 """Create a safe config representation."""
527 raise NotImplementedError()
586 raise NotImplementedError()
528
587
529 def _prepare_callback_daemon(self, extras):
588 def _prepare_callback_daemon(self, extras):
530 return prepare_callback_daemon(
589 return prepare_callback_daemon(
531 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
590 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
532 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
591 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
533
592
534
593
535 def _should_check_locking(query_string):
594 def _should_check_locking(query_string):
536 # this is kind of hacky, but due to how mercurial handles client-server
595 # this is kind of hacky, but due to how mercurial handles client-server
537 # server see all operation on commit; bookmarks, phases and
596 # server see all operation on commit; bookmarks, phases and
538 # obsolescence marker in different transaction, we don't want to check
597 # obsolescence marker in different transaction, we don't want to check
539 # locking on those
598 # locking on those
540 return query_string not in ['cmd=listkeys']
599 return query_string not in ['cmd=listkeys']
@@ -1,4227 +1,4227 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from sqlalchemy.sql.functions import coalesce, count # noqa
44 from sqlalchemy.sql.functions import coalesce, count # noqa
45 from sqlalchemy.exc import IntegrityError # noqa
45 from sqlalchemy.exc import IntegrityError # noqa
46 from sqlalchemy.dialects.mysql import LONGTEXT
46 from sqlalchemy.dialects.mysql import LONGTEXT
47 from beaker.cache import cache_region
47 from beaker.cache import cache_region
48 from zope.cachedescriptors.property import Lazy as LazyProperty
48 from zope.cachedescriptors.property import Lazy as LazyProperty
49
49
50 from pyramid.threadlocal import get_current_request
50 from pyramid.threadlocal import get_current_request
51
51
52 from rhodecode.translation import _
52 from rhodecode.translation import _
53 from rhodecode.lib.vcs import get_vcs_instance
53 from rhodecode.lib.vcs import get_vcs_instance
54 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
54 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
56 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
57 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
57 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
58 glob2re, StrictAttributeDict, cleaned_uri)
58 glob2re, StrictAttributeDict, cleaned_uri)
59 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
59 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
60 from rhodecode.lib.ext_json import json
60 from rhodecode.lib.ext_json import json
61 from rhodecode.lib.caching_query import FromCache
61 from rhodecode.lib.caching_query import FromCache
62 from rhodecode.lib.encrypt import AESCipher
62 from rhodecode.lib.encrypt import AESCipher
63
63
64 from rhodecode.model.meta import Base, Session
64 from rhodecode.model.meta import Base, Session
65
65
66 URL_SEP = '/'
66 URL_SEP = '/'
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69 # =============================================================================
69 # =============================================================================
70 # BASE CLASSES
70 # BASE CLASSES
71 # =============================================================================
71 # =============================================================================
72
72
73 # this is propagated from .ini file rhodecode.encrypted_values.secret or
73 # this is propagated from .ini file rhodecode.encrypted_values.secret or
74 # beaker.session.secret if first is not set.
74 # beaker.session.secret if first is not set.
75 # and initialized at environment.py
75 # and initialized at environment.py
76 ENCRYPTION_KEY = None
76 ENCRYPTION_KEY = None
77
77
78 # used to sort permissions by types, '#' used here is not allowed to be in
78 # used to sort permissions by types, '#' used here is not allowed to be in
79 # usernames, and it's very early in sorted string.printable table.
79 # usernames, and it's very early in sorted string.printable table.
80 PERMISSION_TYPE_SORT = {
80 PERMISSION_TYPE_SORT = {
81 'admin': '####',
81 'admin': '####',
82 'write': '###',
82 'write': '###',
83 'read': '##',
83 'read': '##',
84 'none': '#',
84 'none': '#',
85 }
85 }
86
86
87
87
88 def display_user_sort(obj):
88 def display_user_sort(obj):
89 """
89 """
90 Sort function used to sort permissions in .permissions() function of
90 Sort function used to sort permissions in .permissions() function of
91 Repository, RepoGroup, UserGroup. Also it put the default user in front
91 Repository, RepoGroup, UserGroup. Also it put the default user in front
92 of all other resources
92 of all other resources
93 """
93 """
94
94
95 if obj.username == User.DEFAULT_USER:
95 if obj.username == User.DEFAULT_USER:
96 return '#####'
96 return '#####'
97 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
97 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
98 return prefix + obj.username
98 return prefix + obj.username
99
99
100
100
101 def display_user_group_sort(obj):
101 def display_user_group_sort(obj):
102 """
102 """
103 Sort function used to sort permissions in .permissions() function of
103 Sort function used to sort permissions in .permissions() function of
104 Repository, RepoGroup, UserGroup. Also it put the default user in front
104 Repository, RepoGroup, UserGroup. Also it put the default user in front
105 of all other resources
105 of all other resources
106 """
106 """
107
107
108 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
109 return prefix + obj.users_group_name
109 return prefix + obj.users_group_name
110
110
111
111
112 def _hash_key(k):
112 def _hash_key(k):
113 return md5_safe(k)
113 return md5_safe(k)
114
114
115
115
116 def in_filter_generator(qry, items, limit=500):
116 def in_filter_generator(qry, items, limit=500):
117 """
117 """
118 Splits IN() into multiple with OR
118 Splits IN() into multiple with OR
119 e.g.::
119 e.g.::
120 cnt = Repository.query().filter(
120 cnt = Repository.query().filter(
121 or_(
121 or_(
122 *in_filter_generator(Repository.repo_id, range(100000))
122 *in_filter_generator(Repository.repo_id, range(100000))
123 )).count()
123 )).count()
124 """
124 """
125 parts = []
125 parts = []
126 for chunk in xrange(0, len(items), limit):
126 for chunk in xrange(0, len(items), limit):
127 parts.append(
127 parts.append(
128 qry.in_(items[chunk: chunk + limit])
128 qry.in_(items[chunk: chunk + limit])
129 )
129 )
130
130
131 return parts
131 return parts
132
132
133
133
134 class EncryptedTextValue(TypeDecorator):
134 class EncryptedTextValue(TypeDecorator):
135 """
135 """
136 Special column for encrypted long text data, use like::
136 Special column for encrypted long text data, use like::
137
137
138 value = Column("encrypted_value", EncryptedValue(), nullable=False)
138 value = Column("encrypted_value", EncryptedValue(), nullable=False)
139
139
140 This column is intelligent so if value is in unencrypted form it return
140 This column is intelligent so if value is in unencrypted form it return
141 unencrypted form, but on save it always encrypts
141 unencrypted form, but on save it always encrypts
142 """
142 """
143 impl = Text
143 impl = Text
144
144
145 def process_bind_param(self, value, dialect):
145 def process_bind_param(self, value, dialect):
146 if not value:
146 if not value:
147 return value
147 return value
148 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
148 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
149 # protect against double encrypting if someone manually starts
149 # protect against double encrypting if someone manually starts
150 # doing
150 # doing
151 raise ValueError('value needs to be in unencrypted format, ie. '
151 raise ValueError('value needs to be in unencrypted format, ie. '
152 'not starting with enc$aes')
152 'not starting with enc$aes')
153 return 'enc$aes_hmac$%s' % AESCipher(
153 return 'enc$aes_hmac$%s' % AESCipher(
154 ENCRYPTION_KEY, hmac=True).encrypt(value)
154 ENCRYPTION_KEY, hmac=True).encrypt(value)
155
155
156 def process_result_value(self, value, dialect):
156 def process_result_value(self, value, dialect):
157 import rhodecode
157 import rhodecode
158
158
159 if not value:
159 if not value:
160 return value
160 return value
161
161
162 parts = value.split('$', 3)
162 parts = value.split('$', 3)
163 if not len(parts) == 3:
163 if not len(parts) == 3:
164 # probably not encrypted values
164 # probably not encrypted values
165 return value
165 return value
166 else:
166 else:
167 if parts[0] != 'enc':
167 if parts[0] != 'enc':
168 # parts ok but without our header ?
168 # parts ok but without our header ?
169 return value
169 return value
170 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
170 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
171 'rhodecode.encrypted_values.strict') or True)
171 'rhodecode.encrypted_values.strict') or True)
172 # at that stage we know it's our encryption
172 # at that stage we know it's our encryption
173 if parts[1] == 'aes':
173 if parts[1] == 'aes':
174 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
174 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
175 elif parts[1] == 'aes_hmac':
175 elif parts[1] == 'aes_hmac':
176 decrypted_data = AESCipher(
176 decrypted_data = AESCipher(
177 ENCRYPTION_KEY, hmac=True,
177 ENCRYPTION_KEY, hmac=True,
178 strict_verification=enc_strict_mode).decrypt(parts[2])
178 strict_verification=enc_strict_mode).decrypt(parts[2])
179 else:
179 else:
180 raise ValueError(
180 raise ValueError(
181 'Encryption type part is wrong, must be `aes` '
181 'Encryption type part is wrong, must be `aes` '
182 'or `aes_hmac`, got `%s` instead' % (parts[1]))
182 'or `aes_hmac`, got `%s` instead' % (parts[1]))
183 return decrypted_data
183 return decrypted_data
184
184
185
185
186 class BaseModel(object):
186 class BaseModel(object):
187 """
187 """
188 Base Model for all classes
188 Base Model for all classes
189 """
189 """
190
190
191 @classmethod
191 @classmethod
192 def _get_keys(cls):
192 def _get_keys(cls):
193 """return column names for this model """
193 """return column names for this model """
194 return class_mapper(cls).c.keys()
194 return class_mapper(cls).c.keys()
195
195
196 def get_dict(self):
196 def get_dict(self):
197 """
197 """
198 return dict with keys and values corresponding
198 return dict with keys and values corresponding
199 to this model data """
199 to this model data """
200
200
201 d = {}
201 d = {}
202 for k in self._get_keys():
202 for k in self._get_keys():
203 d[k] = getattr(self, k)
203 d[k] = getattr(self, k)
204
204
205 # also use __json__() if present to get additional fields
205 # also use __json__() if present to get additional fields
206 _json_attr = getattr(self, '__json__', None)
206 _json_attr = getattr(self, '__json__', None)
207 if _json_attr:
207 if _json_attr:
208 # update with attributes from __json__
208 # update with attributes from __json__
209 if callable(_json_attr):
209 if callable(_json_attr):
210 _json_attr = _json_attr()
210 _json_attr = _json_attr()
211 for k, val in _json_attr.iteritems():
211 for k, val in _json_attr.iteritems():
212 d[k] = val
212 d[k] = val
213 return d
213 return d
214
214
215 def get_appstruct(self):
215 def get_appstruct(self):
216 """return list with keys and values tuples corresponding
216 """return list with keys and values tuples corresponding
217 to this model data """
217 to this model data """
218
218
219 l = []
219 l = []
220 for k in self._get_keys():
220 for k in self._get_keys():
221 l.append((k, getattr(self, k),))
221 l.append((k, getattr(self, k),))
222 return l
222 return l
223
223
224 def populate_obj(self, populate_dict):
224 def populate_obj(self, populate_dict):
225 """populate model with data from given populate_dict"""
225 """populate model with data from given populate_dict"""
226
226
227 for k in self._get_keys():
227 for k in self._get_keys():
228 if k in populate_dict:
228 if k in populate_dict:
229 setattr(self, k, populate_dict[k])
229 setattr(self, k, populate_dict[k])
230
230
231 @classmethod
231 @classmethod
232 def query(cls):
232 def query(cls):
233 return Session().query(cls)
233 return Session().query(cls)
234
234
235 @classmethod
235 @classmethod
236 def get(cls, id_):
236 def get(cls, id_):
237 if id_:
237 if id_:
238 return cls.query().get(id_)
238 return cls.query().get(id_)
239
239
240 @classmethod
240 @classmethod
241 def get_or_404(cls, id_):
241 def get_or_404(cls, id_):
242 from pyramid.httpexceptions import HTTPNotFound
242 from pyramid.httpexceptions import HTTPNotFound
243
243
244 try:
244 try:
245 id_ = int(id_)
245 id_ = int(id_)
246 except (TypeError, ValueError):
246 except (TypeError, ValueError):
247 raise HTTPNotFound()
247 raise HTTPNotFound()
248
248
249 res = cls.query().get(id_)
249 res = cls.query().get(id_)
250 if not res:
250 if not res:
251 raise HTTPNotFound()
251 raise HTTPNotFound()
252 return res
252 return res
253
253
254 @classmethod
254 @classmethod
255 def getAll(cls):
255 def getAll(cls):
256 # deprecated and left for backward compatibility
256 # deprecated and left for backward compatibility
257 return cls.get_all()
257 return cls.get_all()
258
258
259 @classmethod
259 @classmethod
260 def get_all(cls):
260 def get_all(cls):
261 return cls.query().all()
261 return cls.query().all()
262
262
263 @classmethod
263 @classmethod
264 def delete(cls, id_):
264 def delete(cls, id_):
265 obj = cls.query().get(id_)
265 obj = cls.query().get(id_)
266 Session().delete(obj)
266 Session().delete(obj)
267
267
268 @classmethod
268 @classmethod
269 def identity_cache(cls, session, attr_name, value):
269 def identity_cache(cls, session, attr_name, value):
270 exist_in_session = []
270 exist_in_session = []
271 for (item_cls, pkey), instance in session.identity_map.items():
271 for (item_cls, pkey), instance in session.identity_map.items():
272 if cls == item_cls and getattr(instance, attr_name) == value:
272 if cls == item_cls and getattr(instance, attr_name) == value:
273 exist_in_session.append(instance)
273 exist_in_session.append(instance)
274 if exist_in_session:
274 if exist_in_session:
275 if len(exist_in_session) == 1:
275 if len(exist_in_session) == 1:
276 return exist_in_session[0]
276 return exist_in_session[0]
277 log.exception(
277 log.exception(
278 'multiple objects with attr %s and '
278 'multiple objects with attr %s and '
279 'value %s found with same name: %r',
279 'value %s found with same name: %r',
280 attr_name, value, exist_in_session)
280 attr_name, value, exist_in_session)
281
281
282 def __repr__(self):
282 def __repr__(self):
283 if hasattr(self, '__unicode__'):
283 if hasattr(self, '__unicode__'):
284 # python repr needs to return str
284 # python repr needs to return str
285 try:
285 try:
286 return safe_str(self.__unicode__())
286 return safe_str(self.__unicode__())
287 except UnicodeDecodeError:
287 except UnicodeDecodeError:
288 pass
288 pass
289 return '<DB:%s>' % (self.__class__.__name__)
289 return '<DB:%s>' % (self.__class__.__name__)
290
290
291
291
292 class RhodeCodeSetting(Base, BaseModel):
292 class RhodeCodeSetting(Base, BaseModel):
293 __tablename__ = 'rhodecode_settings'
293 __tablename__ = 'rhodecode_settings'
294 __table_args__ = (
294 __table_args__ = (
295 UniqueConstraint('app_settings_name'),
295 UniqueConstraint('app_settings_name'),
296 {'extend_existing': True, 'mysql_engine': 'InnoDB',
296 {'extend_existing': True, 'mysql_engine': 'InnoDB',
297 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
297 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
298 )
298 )
299
299
300 SETTINGS_TYPES = {
300 SETTINGS_TYPES = {
301 'str': safe_str,
301 'str': safe_str,
302 'int': safe_int,
302 'int': safe_int,
303 'unicode': safe_unicode,
303 'unicode': safe_unicode,
304 'bool': str2bool,
304 'bool': str2bool,
305 'list': functools.partial(aslist, sep=',')
305 'list': functools.partial(aslist, sep=',')
306 }
306 }
307 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
307 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
308 GLOBAL_CONF_KEY = 'app_settings'
308 GLOBAL_CONF_KEY = 'app_settings'
309
309
310 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
310 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
311 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
311 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
312 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
312 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
313 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
313 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
314
314
315 def __init__(self, key='', val='', type='unicode'):
315 def __init__(self, key='', val='', type='unicode'):
316 self.app_settings_name = key
316 self.app_settings_name = key
317 self.app_settings_type = type
317 self.app_settings_type = type
318 self.app_settings_value = val
318 self.app_settings_value = val
319
319
320 @validates('_app_settings_value')
320 @validates('_app_settings_value')
321 def validate_settings_value(self, key, val):
321 def validate_settings_value(self, key, val):
322 assert type(val) == unicode
322 assert type(val) == unicode
323 return val
323 return val
324
324
325 @hybrid_property
325 @hybrid_property
326 def app_settings_value(self):
326 def app_settings_value(self):
327 v = self._app_settings_value
327 v = self._app_settings_value
328 _type = self.app_settings_type
328 _type = self.app_settings_type
329 if _type:
329 if _type:
330 _type = self.app_settings_type.split('.')[0]
330 _type = self.app_settings_type.split('.')[0]
331 # decode the encrypted value
331 # decode the encrypted value
332 if 'encrypted' in self.app_settings_type:
332 if 'encrypted' in self.app_settings_type:
333 cipher = EncryptedTextValue()
333 cipher = EncryptedTextValue()
334 v = safe_unicode(cipher.process_result_value(v, None))
334 v = safe_unicode(cipher.process_result_value(v, None))
335
335
336 converter = self.SETTINGS_TYPES.get(_type) or \
336 converter = self.SETTINGS_TYPES.get(_type) or \
337 self.SETTINGS_TYPES['unicode']
337 self.SETTINGS_TYPES['unicode']
338 return converter(v)
338 return converter(v)
339
339
340 @app_settings_value.setter
340 @app_settings_value.setter
341 def app_settings_value(self, val):
341 def app_settings_value(self, val):
342 """
342 """
343 Setter that will always make sure we use unicode in app_settings_value
343 Setter that will always make sure we use unicode in app_settings_value
344
344
345 :param val:
345 :param val:
346 """
346 """
347 val = safe_unicode(val)
347 val = safe_unicode(val)
348 # encode the encrypted value
348 # encode the encrypted value
349 if 'encrypted' in self.app_settings_type:
349 if 'encrypted' in self.app_settings_type:
350 cipher = EncryptedTextValue()
350 cipher = EncryptedTextValue()
351 val = safe_unicode(cipher.process_bind_param(val, None))
351 val = safe_unicode(cipher.process_bind_param(val, None))
352 self._app_settings_value = val
352 self._app_settings_value = val
353
353
354 @hybrid_property
354 @hybrid_property
355 def app_settings_type(self):
355 def app_settings_type(self):
356 return self._app_settings_type
356 return self._app_settings_type
357
357
358 @app_settings_type.setter
358 @app_settings_type.setter
359 def app_settings_type(self, val):
359 def app_settings_type(self, val):
360 if val.split('.')[0] not in self.SETTINGS_TYPES:
360 if val.split('.')[0] not in self.SETTINGS_TYPES:
361 raise Exception('type must be one of %s got %s'
361 raise Exception('type must be one of %s got %s'
362 % (self.SETTINGS_TYPES.keys(), val))
362 % (self.SETTINGS_TYPES.keys(), val))
363 self._app_settings_type = val
363 self._app_settings_type = val
364
364
365 def __unicode__(self):
365 def __unicode__(self):
366 return u"<%s('%s:%s[%s]')>" % (
366 return u"<%s('%s:%s[%s]')>" % (
367 self.__class__.__name__,
367 self.__class__.__name__,
368 self.app_settings_name, self.app_settings_value,
368 self.app_settings_name, self.app_settings_value,
369 self.app_settings_type
369 self.app_settings_type
370 )
370 )
371
371
372
372
373 class RhodeCodeUi(Base, BaseModel):
373 class RhodeCodeUi(Base, BaseModel):
374 __tablename__ = 'rhodecode_ui'
374 __tablename__ = 'rhodecode_ui'
375 __table_args__ = (
375 __table_args__ = (
376 UniqueConstraint('ui_key'),
376 UniqueConstraint('ui_key'),
377 {'extend_existing': True, 'mysql_engine': 'InnoDB',
377 {'extend_existing': True, 'mysql_engine': 'InnoDB',
378 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
378 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
379 )
379 )
380
380
381 HOOK_REPO_SIZE = 'changegroup.repo_size'
381 HOOK_REPO_SIZE = 'changegroup.repo_size'
382 # HG
382 # HG
383 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
383 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
384 HOOK_PULL = 'outgoing.pull_logger'
384 HOOK_PULL = 'outgoing.pull_logger'
385 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
385 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
386 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
386 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
387 HOOK_PUSH = 'changegroup.push_logger'
387 HOOK_PUSH = 'changegroup.push_logger'
388 HOOK_PUSH_KEY = 'pushkey.key_push'
388 HOOK_PUSH_KEY = 'pushkey.key_push'
389
389
390 # TODO: johbo: Unify way how hooks are configured for git and hg,
390 # TODO: johbo: Unify way how hooks are configured for git and hg,
391 # git part is currently hardcoded.
391 # git part is currently hardcoded.
392
392
393 # SVN PATTERNS
393 # SVN PATTERNS
394 SVN_BRANCH_ID = 'vcs_svn_branch'
394 SVN_BRANCH_ID = 'vcs_svn_branch'
395 SVN_TAG_ID = 'vcs_svn_tag'
395 SVN_TAG_ID = 'vcs_svn_tag'
396
396
397 ui_id = Column(
397 ui_id = Column(
398 "ui_id", Integer(), nullable=False, unique=True, default=None,
398 "ui_id", Integer(), nullable=False, unique=True, default=None,
399 primary_key=True)
399 primary_key=True)
400 ui_section = Column(
400 ui_section = Column(
401 "ui_section", String(255), nullable=True, unique=None, default=None)
401 "ui_section", String(255), nullable=True, unique=None, default=None)
402 ui_key = Column(
402 ui_key = Column(
403 "ui_key", String(255), nullable=True, unique=None, default=None)
403 "ui_key", String(255), nullable=True, unique=None, default=None)
404 ui_value = Column(
404 ui_value = Column(
405 "ui_value", String(255), nullable=True, unique=None, default=None)
405 "ui_value", String(255), nullable=True, unique=None, default=None)
406 ui_active = Column(
406 ui_active = Column(
407 "ui_active", Boolean(), nullable=True, unique=None, default=True)
407 "ui_active", Boolean(), nullable=True, unique=None, default=True)
408
408
409 def __repr__(self):
409 def __repr__(self):
410 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
410 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
411 self.ui_key, self.ui_value)
411 self.ui_key, self.ui_value)
412
412
413
413
414 class RepoRhodeCodeSetting(Base, BaseModel):
414 class RepoRhodeCodeSetting(Base, BaseModel):
415 __tablename__ = 'repo_rhodecode_settings'
415 __tablename__ = 'repo_rhodecode_settings'
416 __table_args__ = (
416 __table_args__ = (
417 UniqueConstraint(
417 UniqueConstraint(
418 'app_settings_name', 'repository_id',
418 'app_settings_name', 'repository_id',
419 name='uq_repo_rhodecode_setting_name_repo_id'),
419 name='uq_repo_rhodecode_setting_name_repo_id'),
420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
421 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
421 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
422 )
422 )
423
423
424 repository_id = Column(
424 repository_id = Column(
425 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
425 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
426 nullable=False)
426 nullable=False)
427 app_settings_id = Column(
427 app_settings_id = Column(
428 "app_settings_id", Integer(), nullable=False, unique=True,
428 "app_settings_id", Integer(), nullable=False, unique=True,
429 default=None, primary_key=True)
429 default=None, primary_key=True)
430 app_settings_name = Column(
430 app_settings_name = Column(
431 "app_settings_name", String(255), nullable=True, unique=None,
431 "app_settings_name", String(255), nullable=True, unique=None,
432 default=None)
432 default=None)
433 _app_settings_value = Column(
433 _app_settings_value = Column(
434 "app_settings_value", String(4096), nullable=True, unique=None,
434 "app_settings_value", String(4096), nullable=True, unique=None,
435 default=None)
435 default=None)
436 _app_settings_type = Column(
436 _app_settings_type = Column(
437 "app_settings_type", String(255), nullable=True, unique=None,
437 "app_settings_type", String(255), nullable=True, unique=None,
438 default=None)
438 default=None)
439
439
440 repository = relationship('Repository')
440 repository = relationship('Repository')
441
441
442 def __init__(self, repository_id, key='', val='', type='unicode'):
442 def __init__(self, repository_id, key='', val='', type='unicode'):
443 self.repository_id = repository_id
443 self.repository_id = repository_id
444 self.app_settings_name = key
444 self.app_settings_name = key
445 self.app_settings_type = type
445 self.app_settings_type = type
446 self.app_settings_value = val
446 self.app_settings_value = val
447
447
448 @validates('_app_settings_value')
448 @validates('_app_settings_value')
449 def validate_settings_value(self, key, val):
449 def validate_settings_value(self, key, val):
450 assert type(val) == unicode
450 assert type(val) == unicode
451 return val
451 return val
452
452
453 @hybrid_property
453 @hybrid_property
454 def app_settings_value(self):
454 def app_settings_value(self):
455 v = self._app_settings_value
455 v = self._app_settings_value
456 type_ = self.app_settings_type
456 type_ = self.app_settings_type
457 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
457 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
458 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
458 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
459 return converter(v)
459 return converter(v)
460
460
461 @app_settings_value.setter
461 @app_settings_value.setter
462 def app_settings_value(self, val):
462 def app_settings_value(self, val):
463 """
463 """
464 Setter that will always make sure we use unicode in app_settings_value
464 Setter that will always make sure we use unicode in app_settings_value
465
465
466 :param val:
466 :param val:
467 """
467 """
468 self._app_settings_value = safe_unicode(val)
468 self._app_settings_value = safe_unicode(val)
469
469
470 @hybrid_property
470 @hybrid_property
471 def app_settings_type(self):
471 def app_settings_type(self):
472 return self._app_settings_type
472 return self._app_settings_type
473
473
474 @app_settings_type.setter
474 @app_settings_type.setter
475 def app_settings_type(self, val):
475 def app_settings_type(self, val):
476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
477 if val not in SETTINGS_TYPES:
477 if val not in SETTINGS_TYPES:
478 raise Exception('type must be one of %s got %s'
478 raise Exception('type must be one of %s got %s'
479 % (SETTINGS_TYPES.keys(), val))
479 % (SETTINGS_TYPES.keys(), val))
480 self._app_settings_type = val
480 self._app_settings_type = val
481
481
482 def __unicode__(self):
482 def __unicode__(self):
483 return u"<%s('%s:%s:%s[%s]')>" % (
483 return u"<%s('%s:%s:%s[%s]')>" % (
484 self.__class__.__name__, self.repository.repo_name,
484 self.__class__.__name__, self.repository.repo_name,
485 self.app_settings_name, self.app_settings_value,
485 self.app_settings_name, self.app_settings_value,
486 self.app_settings_type
486 self.app_settings_type
487 )
487 )
488
488
489
489
490 class RepoRhodeCodeUi(Base, BaseModel):
490 class RepoRhodeCodeUi(Base, BaseModel):
491 __tablename__ = 'repo_rhodecode_ui'
491 __tablename__ = 'repo_rhodecode_ui'
492 __table_args__ = (
492 __table_args__ = (
493 UniqueConstraint(
493 UniqueConstraint(
494 'repository_id', 'ui_section', 'ui_key',
494 'repository_id', 'ui_section', 'ui_key',
495 name='uq_repo_rhodecode_ui_repository_id_section_key'),
495 name='uq_repo_rhodecode_ui_repository_id_section_key'),
496 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 {'extend_existing': True, 'mysql_engine': 'InnoDB',
497 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
498 )
498 )
499
499
500 repository_id = Column(
500 repository_id = Column(
501 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
501 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
502 nullable=False)
502 nullable=False)
503 ui_id = Column(
503 ui_id = Column(
504 "ui_id", Integer(), nullable=False, unique=True, default=None,
504 "ui_id", Integer(), nullable=False, unique=True, default=None,
505 primary_key=True)
505 primary_key=True)
506 ui_section = Column(
506 ui_section = Column(
507 "ui_section", String(255), nullable=True, unique=None, default=None)
507 "ui_section", String(255), nullable=True, unique=None, default=None)
508 ui_key = Column(
508 ui_key = Column(
509 "ui_key", String(255), nullable=True, unique=None, default=None)
509 "ui_key", String(255), nullable=True, unique=None, default=None)
510 ui_value = Column(
510 ui_value = Column(
511 "ui_value", String(255), nullable=True, unique=None, default=None)
511 "ui_value", String(255), nullable=True, unique=None, default=None)
512 ui_active = Column(
512 ui_active = Column(
513 "ui_active", Boolean(), nullable=True, unique=None, default=True)
513 "ui_active", Boolean(), nullable=True, unique=None, default=True)
514
514
515 repository = relationship('Repository')
515 repository = relationship('Repository')
516
516
517 def __repr__(self):
517 def __repr__(self):
518 return '<%s[%s:%s]%s=>%s]>' % (
518 return '<%s[%s:%s]%s=>%s]>' % (
519 self.__class__.__name__, self.repository.repo_name,
519 self.__class__.__name__, self.repository.repo_name,
520 self.ui_section, self.ui_key, self.ui_value)
520 self.ui_section, self.ui_key, self.ui_value)
521
521
522
522
523 class User(Base, BaseModel):
523 class User(Base, BaseModel):
524 __tablename__ = 'users'
524 __tablename__ = 'users'
525 __table_args__ = (
525 __table_args__ = (
526 UniqueConstraint('username'), UniqueConstraint('email'),
526 UniqueConstraint('username'), UniqueConstraint('email'),
527 Index('u_username_idx', 'username'),
527 Index('u_username_idx', 'username'),
528 Index('u_email_idx', 'email'),
528 Index('u_email_idx', 'email'),
529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
530 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
530 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
531 )
531 )
532 DEFAULT_USER = 'default'
532 DEFAULT_USER = 'default'
533 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
533 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
534 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
534 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
535
535
536 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
536 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
537 username = Column("username", String(255), nullable=True, unique=None, default=None)
537 username = Column("username", String(255), nullable=True, unique=None, default=None)
538 password = Column("password", String(255), nullable=True, unique=None, default=None)
538 password = Column("password", String(255), nullable=True, unique=None, default=None)
539 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
539 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
540 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
540 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
541 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
541 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
542 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
542 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
543 _email = Column("email", String(255), nullable=True, unique=None, default=None)
543 _email = Column("email", String(255), nullable=True, unique=None, default=None)
544 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
544 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
545 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
545 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
546
546
547 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
547 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
548 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
548 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
549 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
549 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
550 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
550 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
551 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
551 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
552 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
552 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
553
553
554 user_log = relationship('UserLog')
554 user_log = relationship('UserLog')
555 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
555 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
556
556
557 repositories = relationship('Repository')
557 repositories = relationship('Repository')
558 repository_groups = relationship('RepoGroup')
558 repository_groups = relationship('RepoGroup')
559 user_groups = relationship('UserGroup')
559 user_groups = relationship('UserGroup')
560
560
561 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
561 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
562 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
562 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
563
563
564 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
564 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
565 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
565 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
566 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
566 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
567
567
568 group_member = relationship('UserGroupMember', cascade='all')
568 group_member = relationship('UserGroupMember', cascade='all')
569
569
570 notifications = relationship('UserNotification', cascade='all')
570 notifications = relationship('UserNotification', cascade='all')
571 # notifications assigned to this user
571 # notifications assigned to this user
572 user_created_notifications = relationship('Notification', cascade='all')
572 user_created_notifications = relationship('Notification', cascade='all')
573 # comments created by this user
573 # comments created by this user
574 user_comments = relationship('ChangesetComment', cascade='all')
574 user_comments = relationship('ChangesetComment', cascade='all')
575 # user profile extra info
575 # user profile extra info
576 user_emails = relationship('UserEmailMap', cascade='all')
576 user_emails = relationship('UserEmailMap', cascade='all')
577 user_ip_map = relationship('UserIpMap', cascade='all')
577 user_ip_map = relationship('UserIpMap', cascade='all')
578 user_auth_tokens = relationship('UserApiKeys', cascade='all')
578 user_auth_tokens = relationship('UserApiKeys', cascade='all')
579 user_ssh_keys = relationship('UserSshKeys', cascade='all')
579 user_ssh_keys = relationship('UserSshKeys', cascade='all')
580
580
581 # gists
581 # gists
582 user_gists = relationship('Gist', cascade='all')
582 user_gists = relationship('Gist', cascade='all')
583 # user pull requests
583 # user pull requests
584 user_pull_requests = relationship('PullRequest', cascade='all')
584 user_pull_requests = relationship('PullRequest', cascade='all')
585 # external identities
585 # external identities
586 extenal_identities = relationship(
586 extenal_identities = relationship(
587 'ExternalIdentity',
587 'ExternalIdentity',
588 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
588 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
589 cascade='all')
589 cascade='all')
590 # review rules
590 # review rules
591 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
591 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
592
592
593 def __unicode__(self):
593 def __unicode__(self):
594 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
594 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
595 self.user_id, self.username)
595 self.user_id, self.username)
596
596
597 @hybrid_property
597 @hybrid_property
598 def email(self):
598 def email(self):
599 return self._email
599 return self._email
600
600
601 @email.setter
601 @email.setter
602 def email(self, val):
602 def email(self, val):
603 self._email = val.lower() if val else None
603 self._email = val.lower() if val else None
604
604
605 @hybrid_property
605 @hybrid_property
606 def first_name(self):
606 def first_name(self):
607 from rhodecode.lib import helpers as h
607 from rhodecode.lib import helpers as h
608 if self.name:
608 if self.name:
609 return h.escape(self.name)
609 return h.escape(self.name)
610 return self.name
610 return self.name
611
611
612 @hybrid_property
612 @hybrid_property
613 def last_name(self):
613 def last_name(self):
614 from rhodecode.lib import helpers as h
614 from rhodecode.lib import helpers as h
615 if self.lastname:
615 if self.lastname:
616 return h.escape(self.lastname)
616 return h.escape(self.lastname)
617 return self.lastname
617 return self.lastname
618
618
619 @hybrid_property
619 @hybrid_property
620 def api_key(self):
620 def api_key(self):
621 """
621 """
622 Fetch if exist an auth-token with role ALL connected to this user
622 Fetch if exist an auth-token with role ALL connected to this user
623 """
623 """
624 user_auth_token = UserApiKeys.query()\
624 user_auth_token = UserApiKeys.query()\
625 .filter(UserApiKeys.user_id == self.user_id)\
625 .filter(UserApiKeys.user_id == self.user_id)\
626 .filter(or_(UserApiKeys.expires == -1,
626 .filter(or_(UserApiKeys.expires == -1,
627 UserApiKeys.expires >= time.time()))\
627 UserApiKeys.expires >= time.time()))\
628 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
628 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
629 if user_auth_token:
629 if user_auth_token:
630 user_auth_token = user_auth_token.api_key
630 user_auth_token = user_auth_token.api_key
631
631
632 return user_auth_token
632 return user_auth_token
633
633
634 @api_key.setter
634 @api_key.setter
635 def api_key(self, val):
635 def api_key(self, val):
636 # don't allow to set API key this is deprecated for now
636 # don't allow to set API key this is deprecated for now
637 self._api_key = None
637 self._api_key = None
638
638
639 @property
639 @property
640 def reviewer_pull_requests(self):
640 def reviewer_pull_requests(self):
641 return PullRequestReviewers.query() \
641 return PullRequestReviewers.query() \
642 .options(joinedload(PullRequestReviewers.pull_request)) \
642 .options(joinedload(PullRequestReviewers.pull_request)) \
643 .filter(PullRequestReviewers.user_id == self.user_id) \
643 .filter(PullRequestReviewers.user_id == self.user_id) \
644 .all()
644 .all()
645
645
646 @property
646 @property
647 def firstname(self):
647 def firstname(self):
648 # alias for future
648 # alias for future
649 return self.name
649 return self.name
650
650
651 @property
651 @property
652 def emails(self):
652 def emails(self):
653 other = UserEmailMap.query()\
653 other = UserEmailMap.query()\
654 .filter(UserEmailMap.user == self) \
654 .filter(UserEmailMap.user == self) \
655 .order_by(UserEmailMap.email_id.asc()) \
655 .order_by(UserEmailMap.email_id.asc()) \
656 .all()
656 .all()
657 return [self.email] + [x.email for x in other]
657 return [self.email] + [x.email for x in other]
658
658
659 @property
659 @property
660 def auth_tokens(self):
660 def auth_tokens(self):
661 auth_tokens = self.get_auth_tokens()
661 auth_tokens = self.get_auth_tokens()
662 return [x.api_key for x in auth_tokens]
662 return [x.api_key for x in auth_tokens]
663
663
664 def get_auth_tokens(self):
664 def get_auth_tokens(self):
665 return UserApiKeys.query()\
665 return UserApiKeys.query()\
666 .filter(UserApiKeys.user == self)\
666 .filter(UserApiKeys.user == self)\
667 .order_by(UserApiKeys.user_api_key_id.asc())\
667 .order_by(UserApiKeys.user_api_key_id.asc())\
668 .all()
668 .all()
669
669
670 @property
670 @property
671 def feed_token(self):
671 def feed_token(self):
672 return self.get_feed_token()
672 return self.get_feed_token()
673
673
674 def get_feed_token(self):
674 def get_feed_token(self):
675 feed_tokens = UserApiKeys.query()\
675 feed_tokens = UserApiKeys.query()\
676 .filter(UserApiKeys.user == self)\
676 .filter(UserApiKeys.user == self)\
677 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
677 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
678 .all()
678 .all()
679 if feed_tokens:
679 if feed_tokens:
680 return feed_tokens[0].api_key
680 return feed_tokens[0].api_key
681 return 'NO_FEED_TOKEN_AVAILABLE'
681 return 'NO_FEED_TOKEN_AVAILABLE'
682
682
683 @classmethod
683 @classmethod
684 def get(cls, user_id, cache=False):
684 def get(cls, user_id, cache=False):
685 if not user_id:
685 if not user_id:
686 return
686 return
687
687
688 user = cls.query()
688 user = cls.query()
689 if cache:
689 if cache:
690 user = user.options(
690 user = user.options(
691 FromCache("sql_cache_short", "get_users_%s" % user_id))
691 FromCache("sql_cache_short", "get_users_%s" % user_id))
692 return user.get(user_id)
692 return user.get(user_id)
693
693
694 @classmethod
694 @classmethod
695 def extra_valid_auth_tokens(cls, user, role=None):
695 def extra_valid_auth_tokens(cls, user, role=None):
696 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
696 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
697 .filter(or_(UserApiKeys.expires == -1,
697 .filter(or_(UserApiKeys.expires == -1,
698 UserApiKeys.expires >= time.time()))
698 UserApiKeys.expires >= time.time()))
699 if role:
699 if role:
700 tokens = tokens.filter(or_(UserApiKeys.role == role,
700 tokens = tokens.filter(or_(UserApiKeys.role == role,
701 UserApiKeys.role == UserApiKeys.ROLE_ALL))
701 UserApiKeys.role == UserApiKeys.ROLE_ALL))
702 return tokens.all()
702 return tokens.all()
703
703
704 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
704 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
705 from rhodecode.lib import auth
705 from rhodecode.lib import auth
706
706
707 log.debug('Trying to authenticate user: %s via auth-token, '
707 log.debug('Trying to authenticate user: %s via auth-token, '
708 'and roles: %s', self, roles)
708 'and roles: %s', self, roles)
709
709
710 if not auth_token:
710 if not auth_token:
711 return False
711 return False
712
712
713 crypto_backend = auth.crypto_backend()
713 crypto_backend = auth.crypto_backend()
714
714
715 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
715 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
716 tokens_q = UserApiKeys.query()\
716 tokens_q = UserApiKeys.query()\
717 .filter(UserApiKeys.user_id == self.user_id)\
717 .filter(UserApiKeys.user_id == self.user_id)\
718 .filter(or_(UserApiKeys.expires == -1,
718 .filter(or_(UserApiKeys.expires == -1,
719 UserApiKeys.expires >= time.time()))
719 UserApiKeys.expires >= time.time()))
720
720
721 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
721 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
722
722
723 plain_tokens = []
723 plain_tokens = []
724 hash_tokens = []
724 hash_tokens = []
725
725
726 for token in tokens_q.all():
726 for token in tokens_q.all():
727 # verify scope first
727 # verify scope first
728 if token.repo_id:
728 if token.repo_id:
729 # token has a scope, we need to verify it
729 # token has a scope, we need to verify it
730 if scope_repo_id != token.repo_id:
730 if scope_repo_id != token.repo_id:
731 log.debug(
731 log.debug(
732 'Scope mismatch: token has a set repo scope: %s, '
732 'Scope mismatch: token has a set repo scope: %s, '
733 'and calling scope is:%s, skipping further checks',
733 'and calling scope is:%s, skipping further checks',
734 token.repo, scope_repo_id)
734 token.repo, scope_repo_id)
735 # token has a scope, and it doesn't match, skip token
735 # token has a scope, and it doesn't match, skip token
736 continue
736 continue
737
737
738 if token.api_key.startswith(crypto_backend.ENC_PREF):
738 if token.api_key.startswith(crypto_backend.ENC_PREF):
739 hash_tokens.append(token.api_key)
739 hash_tokens.append(token.api_key)
740 else:
740 else:
741 plain_tokens.append(token.api_key)
741 plain_tokens.append(token.api_key)
742
742
743 is_plain_match = auth_token in plain_tokens
743 is_plain_match = auth_token in plain_tokens
744 if is_plain_match:
744 if is_plain_match:
745 return True
745 return True
746
746
747 for hashed in hash_tokens:
747 for hashed in hash_tokens:
748 # TODO(marcink): this is expensive to calculate, but most secure
748 # TODO(marcink): this is expensive to calculate, but most secure
749 match = crypto_backend.hash_check(auth_token, hashed)
749 match = crypto_backend.hash_check(auth_token, hashed)
750 if match:
750 if match:
751 return True
751 return True
752
752
753 return False
753 return False
754
754
755 @property
755 @property
756 def ip_addresses(self):
756 def ip_addresses(self):
757 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
757 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
758 return [x.ip_addr for x in ret]
758 return [x.ip_addr for x in ret]
759
759
760 @property
760 @property
761 def username_and_name(self):
761 def username_and_name(self):
762 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
762 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
763
763
764 @property
764 @property
765 def username_or_name_or_email(self):
765 def username_or_name_or_email(self):
766 full_name = self.full_name if self.full_name is not ' ' else None
766 full_name = self.full_name if self.full_name is not ' ' else None
767 return self.username or full_name or self.email
767 return self.username or full_name or self.email
768
768
769 @property
769 @property
770 def full_name(self):
770 def full_name(self):
771 return '%s %s' % (self.first_name, self.last_name)
771 return '%s %s' % (self.first_name, self.last_name)
772
772
773 @property
773 @property
774 def full_name_or_username(self):
774 def full_name_or_username(self):
775 return ('%s %s' % (self.first_name, self.last_name)
775 return ('%s %s' % (self.first_name, self.last_name)
776 if (self.first_name and self.last_name) else self.username)
776 if (self.first_name and self.last_name) else self.username)
777
777
778 @property
778 @property
779 def full_contact(self):
779 def full_contact(self):
780 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
780 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
781
781
782 @property
782 @property
783 def short_contact(self):
783 def short_contact(self):
784 return '%s %s' % (self.first_name, self.last_name)
784 return '%s %s' % (self.first_name, self.last_name)
785
785
786 @property
786 @property
787 def is_admin(self):
787 def is_admin(self):
788 return self.admin
788 return self.admin
789
789
790 def AuthUser(self, **kwargs):
790 def AuthUser(self, **kwargs):
791 """
791 """
792 Returns instance of AuthUser for this user
792 Returns instance of AuthUser for this user
793 """
793 """
794 from rhodecode.lib.auth import AuthUser
794 from rhodecode.lib.auth import AuthUser
795 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
795 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
796
796
797 @hybrid_property
797 @hybrid_property
798 def user_data(self):
798 def user_data(self):
799 if not self._user_data:
799 if not self._user_data:
800 return {}
800 return {}
801
801
802 try:
802 try:
803 return json.loads(self._user_data)
803 return json.loads(self._user_data)
804 except TypeError:
804 except TypeError:
805 return {}
805 return {}
806
806
807 @user_data.setter
807 @user_data.setter
808 def user_data(self, val):
808 def user_data(self, val):
809 if not isinstance(val, dict):
809 if not isinstance(val, dict):
810 raise Exception('user_data must be dict, got %s' % type(val))
810 raise Exception('user_data must be dict, got %s' % type(val))
811 try:
811 try:
812 self._user_data = json.dumps(val)
812 self._user_data = json.dumps(val)
813 except Exception:
813 except Exception:
814 log.error(traceback.format_exc())
814 log.error(traceback.format_exc())
815
815
816 @classmethod
816 @classmethod
817 def get_by_username(cls, username, case_insensitive=False,
817 def get_by_username(cls, username, case_insensitive=False,
818 cache=False, identity_cache=False):
818 cache=False, identity_cache=False):
819 session = Session()
819 session = Session()
820
820
821 if case_insensitive:
821 if case_insensitive:
822 q = cls.query().filter(
822 q = cls.query().filter(
823 func.lower(cls.username) == func.lower(username))
823 func.lower(cls.username) == func.lower(username))
824 else:
824 else:
825 q = cls.query().filter(cls.username == username)
825 q = cls.query().filter(cls.username == username)
826
826
827 if cache:
827 if cache:
828 if identity_cache:
828 if identity_cache:
829 val = cls.identity_cache(session, 'username', username)
829 val = cls.identity_cache(session, 'username', username)
830 if val:
830 if val:
831 return val
831 return val
832 else:
832 else:
833 cache_key = "get_user_by_name_%s" % _hash_key(username)
833 cache_key = "get_user_by_name_%s" % _hash_key(username)
834 q = q.options(
834 q = q.options(
835 FromCache("sql_cache_short", cache_key))
835 FromCache("sql_cache_short", cache_key))
836
836
837 return q.scalar()
837 return q.scalar()
838
838
839 @classmethod
839 @classmethod
840 def get_by_auth_token(cls, auth_token, cache=False):
840 def get_by_auth_token(cls, auth_token, cache=False):
841 q = UserApiKeys.query()\
841 q = UserApiKeys.query()\
842 .filter(UserApiKeys.api_key == auth_token)\
842 .filter(UserApiKeys.api_key == auth_token)\
843 .filter(or_(UserApiKeys.expires == -1,
843 .filter(or_(UserApiKeys.expires == -1,
844 UserApiKeys.expires >= time.time()))
844 UserApiKeys.expires >= time.time()))
845 if cache:
845 if cache:
846 q = q.options(
846 q = q.options(
847 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
847 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
848
848
849 match = q.first()
849 match = q.first()
850 if match:
850 if match:
851 return match.user
851 return match.user
852
852
853 @classmethod
853 @classmethod
854 def get_by_email(cls, email, case_insensitive=False, cache=False):
854 def get_by_email(cls, email, case_insensitive=False, cache=False):
855
855
856 if case_insensitive:
856 if case_insensitive:
857 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
857 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
858
858
859 else:
859 else:
860 q = cls.query().filter(cls.email == email)
860 q = cls.query().filter(cls.email == email)
861
861
862 email_key = _hash_key(email)
862 email_key = _hash_key(email)
863 if cache:
863 if cache:
864 q = q.options(
864 q = q.options(
865 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
865 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
866
866
867 ret = q.scalar()
867 ret = q.scalar()
868 if ret is None:
868 if ret is None:
869 q = UserEmailMap.query()
869 q = UserEmailMap.query()
870 # try fetching in alternate email map
870 # try fetching in alternate email map
871 if case_insensitive:
871 if case_insensitive:
872 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
872 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
873 else:
873 else:
874 q = q.filter(UserEmailMap.email == email)
874 q = q.filter(UserEmailMap.email == email)
875 q = q.options(joinedload(UserEmailMap.user))
875 q = q.options(joinedload(UserEmailMap.user))
876 if cache:
876 if cache:
877 q = q.options(
877 q = q.options(
878 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
878 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
879 ret = getattr(q.scalar(), 'user', None)
879 ret = getattr(q.scalar(), 'user', None)
880
880
881 return ret
881 return ret
882
882
883 @classmethod
883 @classmethod
884 def get_from_cs_author(cls, author):
884 def get_from_cs_author(cls, author):
885 """
885 """
886 Tries to get User objects out of commit author string
886 Tries to get User objects out of commit author string
887
887
888 :param author:
888 :param author:
889 """
889 """
890 from rhodecode.lib.helpers import email, author_name
890 from rhodecode.lib.helpers import email, author_name
891 # Valid email in the attribute passed, see if they're in the system
891 # Valid email in the attribute passed, see if they're in the system
892 _email = email(author)
892 _email = email(author)
893 if _email:
893 if _email:
894 user = cls.get_by_email(_email, case_insensitive=True)
894 user = cls.get_by_email(_email, case_insensitive=True)
895 if user:
895 if user:
896 return user
896 return user
897 # Maybe we can match by username?
897 # Maybe we can match by username?
898 _author = author_name(author)
898 _author = author_name(author)
899 user = cls.get_by_username(_author, case_insensitive=True)
899 user = cls.get_by_username(_author, case_insensitive=True)
900 if user:
900 if user:
901 return user
901 return user
902
902
903 def update_userdata(self, **kwargs):
903 def update_userdata(self, **kwargs):
904 usr = self
904 usr = self
905 old = usr.user_data
905 old = usr.user_data
906 old.update(**kwargs)
906 old.update(**kwargs)
907 usr.user_data = old
907 usr.user_data = old
908 Session().add(usr)
908 Session().add(usr)
909 log.debug('updated userdata with ', kwargs)
909 log.debug('updated userdata with ', kwargs)
910
910
911 def update_lastlogin(self):
911 def update_lastlogin(self):
912 """Update user lastlogin"""
912 """Update user lastlogin"""
913 self.last_login = datetime.datetime.now()
913 self.last_login = datetime.datetime.now()
914 Session().add(self)
914 Session().add(self)
915 log.debug('updated user %s lastlogin', self.username)
915 log.debug('updated user %s lastlogin', self.username)
916
916
917 def update_lastactivity(self):
917 def update_lastactivity(self):
918 """Update user lastactivity"""
918 """Update user lastactivity"""
919 self.last_activity = datetime.datetime.now()
919 self.last_activity = datetime.datetime.now()
920 Session().add(self)
920 Session().add(self)
921 log.debug('updated user %s lastactivity', self.username)
921 log.debug('updated user `%s` last activity', self.username)
922
922
923 def update_password(self, new_password):
923 def update_password(self, new_password):
924 from rhodecode.lib.auth import get_crypt_password
924 from rhodecode.lib.auth import get_crypt_password
925
925
926 self.password = get_crypt_password(new_password)
926 self.password = get_crypt_password(new_password)
927 Session().add(self)
927 Session().add(self)
928
928
929 @classmethod
929 @classmethod
930 def get_first_super_admin(cls):
930 def get_first_super_admin(cls):
931 user = User.query().filter(User.admin == true()).first()
931 user = User.query().filter(User.admin == true()).first()
932 if user is None:
932 if user is None:
933 raise Exception('FATAL: Missing administrative account!')
933 raise Exception('FATAL: Missing administrative account!')
934 return user
934 return user
935
935
936 @classmethod
936 @classmethod
937 def get_all_super_admins(cls):
937 def get_all_super_admins(cls):
938 """
938 """
939 Returns all admin accounts sorted by username
939 Returns all admin accounts sorted by username
940 """
940 """
941 return User.query().filter(User.admin == true())\
941 return User.query().filter(User.admin == true())\
942 .order_by(User.username.asc()).all()
942 .order_by(User.username.asc()).all()
943
943
944 @classmethod
944 @classmethod
945 def get_default_user(cls, cache=False, refresh=False):
945 def get_default_user(cls, cache=False, refresh=False):
946 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
946 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
947 if user is None:
947 if user is None:
948 raise Exception('FATAL: Missing default account!')
948 raise Exception('FATAL: Missing default account!')
949 if refresh:
949 if refresh:
950 # The default user might be based on outdated state which
950 # The default user might be based on outdated state which
951 # has been loaded from the cache.
951 # has been loaded from the cache.
952 # A call to refresh() ensures that the
952 # A call to refresh() ensures that the
953 # latest state from the database is used.
953 # latest state from the database is used.
954 Session().refresh(user)
954 Session().refresh(user)
955 return user
955 return user
956
956
957 def _get_default_perms(self, user, suffix=''):
957 def _get_default_perms(self, user, suffix=''):
958 from rhodecode.model.permission import PermissionModel
958 from rhodecode.model.permission import PermissionModel
959 return PermissionModel().get_default_perms(user.user_perms, suffix)
959 return PermissionModel().get_default_perms(user.user_perms, suffix)
960
960
961 def get_default_perms(self, suffix=''):
961 def get_default_perms(self, suffix=''):
962 return self._get_default_perms(self, suffix)
962 return self._get_default_perms(self, suffix)
963
963
964 def get_api_data(self, include_secrets=False, details='full'):
964 def get_api_data(self, include_secrets=False, details='full'):
965 """
965 """
966 Common function for generating user related data for API
966 Common function for generating user related data for API
967
967
968 :param include_secrets: By default secrets in the API data will be replaced
968 :param include_secrets: By default secrets in the API data will be replaced
969 by a placeholder value to prevent exposing this data by accident. In case
969 by a placeholder value to prevent exposing this data by accident. In case
970 this data shall be exposed, set this flag to ``True``.
970 this data shall be exposed, set this flag to ``True``.
971
971
972 :param details: details can be 'basic|full' basic gives only a subset of
972 :param details: details can be 'basic|full' basic gives only a subset of
973 the available user information that includes user_id, name and emails.
973 the available user information that includes user_id, name and emails.
974 """
974 """
975 user = self
975 user = self
976 user_data = self.user_data
976 user_data = self.user_data
977 data = {
977 data = {
978 'user_id': user.user_id,
978 'user_id': user.user_id,
979 'username': user.username,
979 'username': user.username,
980 'firstname': user.name,
980 'firstname': user.name,
981 'lastname': user.lastname,
981 'lastname': user.lastname,
982 'email': user.email,
982 'email': user.email,
983 'emails': user.emails,
983 'emails': user.emails,
984 }
984 }
985 if details == 'basic':
985 if details == 'basic':
986 return data
986 return data
987
987
988 auth_token_length = 40
988 auth_token_length = 40
989 auth_token_replacement = '*' * auth_token_length
989 auth_token_replacement = '*' * auth_token_length
990
990
991 extras = {
991 extras = {
992 'auth_tokens': [auth_token_replacement],
992 'auth_tokens': [auth_token_replacement],
993 'active': user.active,
993 'active': user.active,
994 'admin': user.admin,
994 'admin': user.admin,
995 'extern_type': user.extern_type,
995 'extern_type': user.extern_type,
996 'extern_name': user.extern_name,
996 'extern_name': user.extern_name,
997 'last_login': user.last_login,
997 'last_login': user.last_login,
998 'last_activity': user.last_activity,
998 'last_activity': user.last_activity,
999 'ip_addresses': user.ip_addresses,
999 'ip_addresses': user.ip_addresses,
1000 'language': user_data.get('language')
1000 'language': user_data.get('language')
1001 }
1001 }
1002 data.update(extras)
1002 data.update(extras)
1003
1003
1004 if include_secrets:
1004 if include_secrets:
1005 data['auth_tokens'] = user.auth_tokens
1005 data['auth_tokens'] = user.auth_tokens
1006 return data
1006 return data
1007
1007
1008 def __json__(self):
1008 def __json__(self):
1009 data = {
1009 data = {
1010 'full_name': self.full_name,
1010 'full_name': self.full_name,
1011 'full_name_or_username': self.full_name_or_username,
1011 'full_name_or_username': self.full_name_or_username,
1012 'short_contact': self.short_contact,
1012 'short_contact': self.short_contact,
1013 'full_contact': self.full_contact,
1013 'full_contact': self.full_contact,
1014 }
1014 }
1015 data.update(self.get_api_data())
1015 data.update(self.get_api_data())
1016 return data
1016 return data
1017
1017
1018
1018
1019 class UserApiKeys(Base, BaseModel):
1019 class UserApiKeys(Base, BaseModel):
1020 __tablename__ = 'user_api_keys'
1020 __tablename__ = 'user_api_keys'
1021 __table_args__ = (
1021 __table_args__ = (
1022 Index('uak_api_key_idx', 'api_key', unique=True),
1022 Index('uak_api_key_idx', 'api_key', unique=True),
1023 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1023 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1026 )
1026 )
1027 __mapper_args__ = {}
1027 __mapper_args__ = {}
1028
1028
1029 # ApiKey role
1029 # ApiKey role
1030 ROLE_ALL = 'token_role_all'
1030 ROLE_ALL = 'token_role_all'
1031 ROLE_HTTP = 'token_role_http'
1031 ROLE_HTTP = 'token_role_http'
1032 ROLE_VCS = 'token_role_vcs'
1032 ROLE_VCS = 'token_role_vcs'
1033 ROLE_API = 'token_role_api'
1033 ROLE_API = 'token_role_api'
1034 ROLE_FEED = 'token_role_feed'
1034 ROLE_FEED = 'token_role_feed'
1035 ROLE_PASSWORD_RESET = 'token_password_reset'
1035 ROLE_PASSWORD_RESET = 'token_password_reset'
1036
1036
1037 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1037 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1038
1038
1039 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1039 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1040 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1040 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1041 api_key = Column("api_key", String(255), nullable=False, unique=True)
1041 api_key = Column("api_key", String(255), nullable=False, unique=True)
1042 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1042 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1043 expires = Column('expires', Float(53), nullable=False)
1043 expires = Column('expires', Float(53), nullable=False)
1044 role = Column('role', String(255), nullable=True)
1044 role = Column('role', String(255), nullable=True)
1045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1046
1046
1047 # scope columns
1047 # scope columns
1048 repo_id = Column(
1048 repo_id = Column(
1049 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1049 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1050 nullable=True, unique=None, default=None)
1050 nullable=True, unique=None, default=None)
1051 repo = relationship('Repository', lazy='joined')
1051 repo = relationship('Repository', lazy='joined')
1052
1052
1053 repo_group_id = Column(
1053 repo_group_id = Column(
1054 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1054 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1055 nullable=True, unique=None, default=None)
1055 nullable=True, unique=None, default=None)
1056 repo_group = relationship('RepoGroup', lazy='joined')
1056 repo_group = relationship('RepoGroup', lazy='joined')
1057
1057
1058 user = relationship('User', lazy='joined')
1058 user = relationship('User', lazy='joined')
1059
1059
1060 def __unicode__(self):
1060 def __unicode__(self):
1061 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1061 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1062
1062
1063 def __json__(self):
1063 def __json__(self):
1064 data = {
1064 data = {
1065 'auth_token': self.api_key,
1065 'auth_token': self.api_key,
1066 'role': self.role,
1066 'role': self.role,
1067 'scope': self.scope_humanized,
1067 'scope': self.scope_humanized,
1068 'expired': self.expired
1068 'expired': self.expired
1069 }
1069 }
1070 return data
1070 return data
1071
1071
1072 def get_api_data(self, include_secrets=False):
1072 def get_api_data(self, include_secrets=False):
1073 data = self.__json__()
1073 data = self.__json__()
1074 if include_secrets:
1074 if include_secrets:
1075 return data
1075 return data
1076 else:
1076 else:
1077 data['auth_token'] = self.token_obfuscated
1077 data['auth_token'] = self.token_obfuscated
1078 return data
1078 return data
1079
1079
1080 @hybrid_property
1080 @hybrid_property
1081 def description_safe(self):
1081 def description_safe(self):
1082 from rhodecode.lib import helpers as h
1082 from rhodecode.lib import helpers as h
1083 return h.escape(self.description)
1083 return h.escape(self.description)
1084
1084
1085 @property
1085 @property
1086 def expired(self):
1086 def expired(self):
1087 if self.expires == -1:
1087 if self.expires == -1:
1088 return False
1088 return False
1089 return time.time() > self.expires
1089 return time.time() > self.expires
1090
1090
1091 @classmethod
1091 @classmethod
1092 def _get_role_name(cls, role):
1092 def _get_role_name(cls, role):
1093 return {
1093 return {
1094 cls.ROLE_ALL: _('all'),
1094 cls.ROLE_ALL: _('all'),
1095 cls.ROLE_HTTP: _('http/web interface'),
1095 cls.ROLE_HTTP: _('http/web interface'),
1096 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1096 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1097 cls.ROLE_API: _('api calls'),
1097 cls.ROLE_API: _('api calls'),
1098 cls.ROLE_FEED: _('feed access'),
1098 cls.ROLE_FEED: _('feed access'),
1099 }.get(role, role)
1099 }.get(role, role)
1100
1100
1101 @property
1101 @property
1102 def role_humanized(self):
1102 def role_humanized(self):
1103 return self._get_role_name(self.role)
1103 return self._get_role_name(self.role)
1104
1104
1105 def _get_scope(self):
1105 def _get_scope(self):
1106 if self.repo:
1106 if self.repo:
1107 return repr(self.repo)
1107 return repr(self.repo)
1108 if self.repo_group:
1108 if self.repo_group:
1109 return repr(self.repo_group) + ' (recursive)'
1109 return repr(self.repo_group) + ' (recursive)'
1110 return 'global'
1110 return 'global'
1111
1111
1112 @property
1112 @property
1113 def scope_humanized(self):
1113 def scope_humanized(self):
1114 return self._get_scope()
1114 return self._get_scope()
1115
1115
1116 @property
1116 @property
1117 def token_obfuscated(self):
1117 def token_obfuscated(self):
1118 if self.api_key:
1118 if self.api_key:
1119 return self.api_key[:4] + "****"
1119 return self.api_key[:4] + "****"
1120
1120
1121
1121
1122 class UserEmailMap(Base, BaseModel):
1122 class UserEmailMap(Base, BaseModel):
1123 __tablename__ = 'user_email_map'
1123 __tablename__ = 'user_email_map'
1124 __table_args__ = (
1124 __table_args__ = (
1125 Index('uem_email_idx', 'email'),
1125 Index('uem_email_idx', 'email'),
1126 UniqueConstraint('email'),
1126 UniqueConstraint('email'),
1127 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1127 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1128 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1128 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1129 )
1129 )
1130 __mapper_args__ = {}
1130 __mapper_args__ = {}
1131
1131
1132 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1132 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1133 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1133 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1134 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1134 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1135 user = relationship('User', lazy='joined')
1135 user = relationship('User', lazy='joined')
1136
1136
1137 @validates('_email')
1137 @validates('_email')
1138 def validate_email(self, key, email):
1138 def validate_email(self, key, email):
1139 # check if this email is not main one
1139 # check if this email is not main one
1140 main_email = Session().query(User).filter(User.email == email).scalar()
1140 main_email = Session().query(User).filter(User.email == email).scalar()
1141 if main_email is not None:
1141 if main_email is not None:
1142 raise AttributeError('email %s is present is user table' % email)
1142 raise AttributeError('email %s is present is user table' % email)
1143 return email
1143 return email
1144
1144
1145 @hybrid_property
1145 @hybrid_property
1146 def email(self):
1146 def email(self):
1147 return self._email
1147 return self._email
1148
1148
1149 @email.setter
1149 @email.setter
1150 def email(self, val):
1150 def email(self, val):
1151 self._email = val.lower() if val else None
1151 self._email = val.lower() if val else None
1152
1152
1153
1153
1154 class UserIpMap(Base, BaseModel):
1154 class UserIpMap(Base, BaseModel):
1155 __tablename__ = 'user_ip_map'
1155 __tablename__ = 'user_ip_map'
1156 __table_args__ = (
1156 __table_args__ = (
1157 UniqueConstraint('user_id', 'ip_addr'),
1157 UniqueConstraint('user_id', 'ip_addr'),
1158 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1158 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1159 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1159 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1160 )
1160 )
1161 __mapper_args__ = {}
1161 __mapper_args__ = {}
1162
1162
1163 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1163 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1165 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1165 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1166 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1166 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1167 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1167 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1168 user = relationship('User', lazy='joined')
1168 user = relationship('User', lazy='joined')
1169
1169
1170 @hybrid_property
1170 @hybrid_property
1171 def description_safe(self):
1171 def description_safe(self):
1172 from rhodecode.lib import helpers as h
1172 from rhodecode.lib import helpers as h
1173 return h.escape(self.description)
1173 return h.escape(self.description)
1174
1174
1175 @classmethod
1175 @classmethod
1176 def _get_ip_range(cls, ip_addr):
1176 def _get_ip_range(cls, ip_addr):
1177 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1177 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1178 return [str(net.network_address), str(net.broadcast_address)]
1178 return [str(net.network_address), str(net.broadcast_address)]
1179
1179
1180 def __json__(self):
1180 def __json__(self):
1181 return {
1181 return {
1182 'ip_addr': self.ip_addr,
1182 'ip_addr': self.ip_addr,
1183 'ip_range': self._get_ip_range(self.ip_addr),
1183 'ip_range': self._get_ip_range(self.ip_addr),
1184 }
1184 }
1185
1185
1186 def __unicode__(self):
1186 def __unicode__(self):
1187 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1187 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1188 self.user_id, self.ip_addr)
1188 self.user_id, self.ip_addr)
1189
1189
1190
1190
1191 class UserSshKeys(Base, BaseModel):
1191 class UserSshKeys(Base, BaseModel):
1192 __tablename__ = 'user_ssh_keys'
1192 __tablename__ = 'user_ssh_keys'
1193 __table_args__ = (
1193 __table_args__ = (
1194 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1194 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1195
1195
1196 UniqueConstraint('ssh_key_fingerprint'),
1196 UniqueConstraint('ssh_key_fingerprint'),
1197
1197
1198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1200 )
1200 )
1201 __mapper_args__ = {}
1201 __mapper_args__ = {}
1202
1202
1203 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1203 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1204 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1204 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1205 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1205 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1206
1206
1207 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1207 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1208
1208
1209 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1209 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1210 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1210 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1211 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1211 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1212
1212
1213 user = relationship('User', lazy='joined')
1213 user = relationship('User', lazy='joined')
1214
1214
1215 def __json__(self):
1215 def __json__(self):
1216 data = {
1216 data = {
1217 'ssh_fingerprint': self.ssh_key_fingerprint,
1217 'ssh_fingerprint': self.ssh_key_fingerprint,
1218 'description': self.description,
1218 'description': self.description,
1219 'created_on': self.created_on
1219 'created_on': self.created_on
1220 }
1220 }
1221 return data
1221 return data
1222
1222
1223 def get_api_data(self):
1223 def get_api_data(self):
1224 data = self.__json__()
1224 data = self.__json__()
1225 return data
1225 return data
1226
1226
1227
1227
1228 class UserLog(Base, BaseModel):
1228 class UserLog(Base, BaseModel):
1229 __tablename__ = 'user_logs'
1229 __tablename__ = 'user_logs'
1230 __table_args__ = (
1230 __table_args__ = (
1231 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1231 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1232 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1232 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1233 )
1233 )
1234 VERSION_1 = 'v1'
1234 VERSION_1 = 'v1'
1235 VERSION_2 = 'v2'
1235 VERSION_2 = 'v2'
1236 VERSIONS = [VERSION_1, VERSION_2]
1236 VERSIONS = [VERSION_1, VERSION_2]
1237
1237
1238 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1238 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1239 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1239 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1240 username = Column("username", String(255), nullable=True, unique=None, default=None)
1240 username = Column("username", String(255), nullable=True, unique=None, default=None)
1241 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1241 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1242 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1242 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1243 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1243 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1244 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1244 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1245 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1245 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1246
1246
1247 version = Column("version", String(255), nullable=True, default=VERSION_1)
1247 version = Column("version", String(255), nullable=True, default=VERSION_1)
1248 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1248 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1249 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1249 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1250
1250
1251 def __unicode__(self):
1251 def __unicode__(self):
1252 return u"<%s('id:%s:%s')>" % (
1252 return u"<%s('id:%s:%s')>" % (
1253 self.__class__.__name__, self.repository_name, self.action)
1253 self.__class__.__name__, self.repository_name, self.action)
1254
1254
1255 def __json__(self):
1255 def __json__(self):
1256 return {
1256 return {
1257 'user_id': self.user_id,
1257 'user_id': self.user_id,
1258 'username': self.username,
1258 'username': self.username,
1259 'repository_id': self.repository_id,
1259 'repository_id': self.repository_id,
1260 'repository_name': self.repository_name,
1260 'repository_name': self.repository_name,
1261 'user_ip': self.user_ip,
1261 'user_ip': self.user_ip,
1262 'action_date': self.action_date,
1262 'action_date': self.action_date,
1263 'action': self.action,
1263 'action': self.action,
1264 }
1264 }
1265
1265
1266 @hybrid_property
1266 @hybrid_property
1267 def entry_id(self):
1267 def entry_id(self):
1268 return self.user_log_id
1268 return self.user_log_id
1269
1269
1270 @property
1270 @property
1271 def action_as_day(self):
1271 def action_as_day(self):
1272 return datetime.date(*self.action_date.timetuple()[:3])
1272 return datetime.date(*self.action_date.timetuple()[:3])
1273
1273
1274 user = relationship('User')
1274 user = relationship('User')
1275 repository = relationship('Repository', cascade='')
1275 repository = relationship('Repository', cascade='')
1276
1276
1277
1277
1278 class UserGroup(Base, BaseModel):
1278 class UserGroup(Base, BaseModel):
1279 __tablename__ = 'users_groups'
1279 __tablename__ = 'users_groups'
1280 __table_args__ = (
1280 __table_args__ = (
1281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1283 )
1283 )
1284
1284
1285 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1285 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1286 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1286 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1287 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1287 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1288 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1288 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1289 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1289 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1290 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1290 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1291 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1291 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1292 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1292 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1293
1293
1294 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1294 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1295 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1295 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1296 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1296 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1297 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1297 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1298 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1298 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1299 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1299 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1300
1300
1301 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1301 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1302 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1302 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1303
1303
1304 @classmethod
1304 @classmethod
1305 def _load_group_data(cls, column):
1305 def _load_group_data(cls, column):
1306 if not column:
1306 if not column:
1307 return {}
1307 return {}
1308
1308
1309 try:
1309 try:
1310 return json.loads(column) or {}
1310 return json.loads(column) or {}
1311 except TypeError:
1311 except TypeError:
1312 return {}
1312 return {}
1313
1313
1314 @hybrid_property
1314 @hybrid_property
1315 def description_safe(self):
1315 def description_safe(self):
1316 from rhodecode.lib import helpers as h
1316 from rhodecode.lib import helpers as h
1317 return h.escape(self.description)
1317 return h.escape(self.description)
1318
1318
1319 @hybrid_property
1319 @hybrid_property
1320 def group_data(self):
1320 def group_data(self):
1321 return self._load_group_data(self._group_data)
1321 return self._load_group_data(self._group_data)
1322
1322
1323 @group_data.expression
1323 @group_data.expression
1324 def group_data(self, **kwargs):
1324 def group_data(self, **kwargs):
1325 return self._group_data
1325 return self._group_data
1326
1326
1327 @group_data.setter
1327 @group_data.setter
1328 def group_data(self, val):
1328 def group_data(self, val):
1329 try:
1329 try:
1330 self._group_data = json.dumps(val)
1330 self._group_data = json.dumps(val)
1331 except Exception:
1331 except Exception:
1332 log.error(traceback.format_exc())
1332 log.error(traceback.format_exc())
1333
1333
1334 def __unicode__(self):
1334 def __unicode__(self):
1335 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1335 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1336 self.users_group_id,
1336 self.users_group_id,
1337 self.users_group_name)
1337 self.users_group_name)
1338
1338
1339 @classmethod
1339 @classmethod
1340 def get_by_group_name(cls, group_name, cache=False,
1340 def get_by_group_name(cls, group_name, cache=False,
1341 case_insensitive=False):
1341 case_insensitive=False):
1342 if case_insensitive:
1342 if case_insensitive:
1343 q = cls.query().filter(func.lower(cls.users_group_name) ==
1343 q = cls.query().filter(func.lower(cls.users_group_name) ==
1344 func.lower(group_name))
1344 func.lower(group_name))
1345
1345
1346 else:
1346 else:
1347 q = cls.query().filter(cls.users_group_name == group_name)
1347 q = cls.query().filter(cls.users_group_name == group_name)
1348 if cache:
1348 if cache:
1349 q = q.options(
1349 q = q.options(
1350 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1350 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1351 return q.scalar()
1351 return q.scalar()
1352
1352
1353 @classmethod
1353 @classmethod
1354 def get(cls, user_group_id, cache=False):
1354 def get(cls, user_group_id, cache=False):
1355 if not user_group_id:
1355 if not user_group_id:
1356 return
1356 return
1357
1357
1358 user_group = cls.query()
1358 user_group = cls.query()
1359 if cache:
1359 if cache:
1360 user_group = user_group.options(
1360 user_group = user_group.options(
1361 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1361 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1362 return user_group.get(user_group_id)
1362 return user_group.get(user_group_id)
1363
1363
1364 def permissions(self, with_admins=True, with_owner=True):
1364 def permissions(self, with_admins=True, with_owner=True):
1365 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1365 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1366 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1366 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1367 joinedload(UserUserGroupToPerm.user),
1367 joinedload(UserUserGroupToPerm.user),
1368 joinedload(UserUserGroupToPerm.permission),)
1368 joinedload(UserUserGroupToPerm.permission),)
1369
1369
1370 # get owners and admins and permissions. We do a trick of re-writing
1370 # get owners and admins and permissions. We do a trick of re-writing
1371 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1371 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1372 # has a global reference and changing one object propagates to all
1372 # has a global reference and changing one object propagates to all
1373 # others. This means if admin is also an owner admin_row that change
1373 # others. This means if admin is also an owner admin_row that change
1374 # would propagate to both objects
1374 # would propagate to both objects
1375 perm_rows = []
1375 perm_rows = []
1376 for _usr in q.all():
1376 for _usr in q.all():
1377 usr = AttributeDict(_usr.user.get_dict())
1377 usr = AttributeDict(_usr.user.get_dict())
1378 usr.permission = _usr.permission.permission_name
1378 usr.permission = _usr.permission.permission_name
1379 perm_rows.append(usr)
1379 perm_rows.append(usr)
1380
1380
1381 # filter the perm rows by 'default' first and then sort them by
1381 # filter the perm rows by 'default' first and then sort them by
1382 # admin,write,read,none permissions sorted again alphabetically in
1382 # admin,write,read,none permissions sorted again alphabetically in
1383 # each group
1383 # each group
1384 perm_rows = sorted(perm_rows, key=display_user_sort)
1384 perm_rows = sorted(perm_rows, key=display_user_sort)
1385
1385
1386 _admin_perm = 'usergroup.admin'
1386 _admin_perm = 'usergroup.admin'
1387 owner_row = []
1387 owner_row = []
1388 if with_owner:
1388 if with_owner:
1389 usr = AttributeDict(self.user.get_dict())
1389 usr = AttributeDict(self.user.get_dict())
1390 usr.owner_row = True
1390 usr.owner_row = True
1391 usr.permission = _admin_perm
1391 usr.permission = _admin_perm
1392 owner_row.append(usr)
1392 owner_row.append(usr)
1393
1393
1394 super_admin_rows = []
1394 super_admin_rows = []
1395 if with_admins:
1395 if with_admins:
1396 for usr in User.get_all_super_admins():
1396 for usr in User.get_all_super_admins():
1397 # if this admin is also owner, don't double the record
1397 # if this admin is also owner, don't double the record
1398 if usr.user_id == owner_row[0].user_id:
1398 if usr.user_id == owner_row[0].user_id:
1399 owner_row[0].admin_row = True
1399 owner_row[0].admin_row = True
1400 else:
1400 else:
1401 usr = AttributeDict(usr.get_dict())
1401 usr = AttributeDict(usr.get_dict())
1402 usr.admin_row = True
1402 usr.admin_row = True
1403 usr.permission = _admin_perm
1403 usr.permission = _admin_perm
1404 super_admin_rows.append(usr)
1404 super_admin_rows.append(usr)
1405
1405
1406 return super_admin_rows + owner_row + perm_rows
1406 return super_admin_rows + owner_row + perm_rows
1407
1407
1408 def permission_user_groups(self):
1408 def permission_user_groups(self):
1409 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1409 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1410 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1410 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1411 joinedload(UserGroupUserGroupToPerm.target_user_group),
1411 joinedload(UserGroupUserGroupToPerm.target_user_group),
1412 joinedload(UserGroupUserGroupToPerm.permission),)
1412 joinedload(UserGroupUserGroupToPerm.permission),)
1413
1413
1414 perm_rows = []
1414 perm_rows = []
1415 for _user_group in q.all():
1415 for _user_group in q.all():
1416 usr = AttributeDict(_user_group.user_group.get_dict())
1416 usr = AttributeDict(_user_group.user_group.get_dict())
1417 usr.permission = _user_group.permission.permission_name
1417 usr.permission = _user_group.permission.permission_name
1418 perm_rows.append(usr)
1418 perm_rows.append(usr)
1419
1419
1420 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1420 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1421 return perm_rows
1421 return perm_rows
1422
1422
1423 def _get_default_perms(self, user_group, suffix=''):
1423 def _get_default_perms(self, user_group, suffix=''):
1424 from rhodecode.model.permission import PermissionModel
1424 from rhodecode.model.permission import PermissionModel
1425 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1425 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1426
1426
1427 def get_default_perms(self, suffix=''):
1427 def get_default_perms(self, suffix=''):
1428 return self._get_default_perms(self, suffix)
1428 return self._get_default_perms(self, suffix)
1429
1429
1430 def get_api_data(self, with_group_members=True, include_secrets=False):
1430 def get_api_data(self, with_group_members=True, include_secrets=False):
1431 """
1431 """
1432 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1432 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1433 basically forwarded.
1433 basically forwarded.
1434
1434
1435 """
1435 """
1436 user_group = self
1436 user_group = self
1437 data = {
1437 data = {
1438 'users_group_id': user_group.users_group_id,
1438 'users_group_id': user_group.users_group_id,
1439 'group_name': user_group.users_group_name,
1439 'group_name': user_group.users_group_name,
1440 'group_description': user_group.user_group_description,
1440 'group_description': user_group.user_group_description,
1441 'active': user_group.users_group_active,
1441 'active': user_group.users_group_active,
1442 'owner': user_group.user.username,
1442 'owner': user_group.user.username,
1443 'owner_email': user_group.user.email,
1443 'owner_email': user_group.user.email,
1444 }
1444 }
1445
1445
1446 if with_group_members:
1446 if with_group_members:
1447 users = []
1447 users = []
1448 for user in user_group.members:
1448 for user in user_group.members:
1449 user = user.user
1449 user = user.user
1450 users.append(user.get_api_data(include_secrets=include_secrets))
1450 users.append(user.get_api_data(include_secrets=include_secrets))
1451 data['users'] = users
1451 data['users'] = users
1452
1452
1453 return data
1453 return data
1454
1454
1455
1455
1456 class UserGroupMember(Base, BaseModel):
1456 class UserGroupMember(Base, BaseModel):
1457 __tablename__ = 'users_groups_members'
1457 __tablename__ = 'users_groups_members'
1458 __table_args__ = (
1458 __table_args__ = (
1459 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1459 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1460 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1460 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1461 )
1461 )
1462
1462
1463 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1463 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1464 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1464 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1465 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1465 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1466
1466
1467 user = relationship('User', lazy='joined')
1467 user = relationship('User', lazy='joined')
1468 users_group = relationship('UserGroup')
1468 users_group = relationship('UserGroup')
1469
1469
1470 def __init__(self, gr_id='', u_id=''):
1470 def __init__(self, gr_id='', u_id=''):
1471 self.users_group_id = gr_id
1471 self.users_group_id = gr_id
1472 self.user_id = u_id
1472 self.user_id = u_id
1473
1473
1474
1474
1475 class RepositoryField(Base, BaseModel):
1475 class RepositoryField(Base, BaseModel):
1476 __tablename__ = 'repositories_fields'
1476 __tablename__ = 'repositories_fields'
1477 __table_args__ = (
1477 __table_args__ = (
1478 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1478 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1481 )
1481 )
1482 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1482 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1483
1483
1484 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1484 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1485 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1486 field_key = Column("field_key", String(250))
1486 field_key = Column("field_key", String(250))
1487 field_label = Column("field_label", String(1024), nullable=False)
1487 field_label = Column("field_label", String(1024), nullable=False)
1488 field_value = Column("field_value", String(10000), nullable=False)
1488 field_value = Column("field_value", String(10000), nullable=False)
1489 field_desc = Column("field_desc", String(1024), nullable=False)
1489 field_desc = Column("field_desc", String(1024), nullable=False)
1490 field_type = Column("field_type", String(255), nullable=False, unique=None)
1490 field_type = Column("field_type", String(255), nullable=False, unique=None)
1491 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1491 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1492
1492
1493 repository = relationship('Repository')
1493 repository = relationship('Repository')
1494
1494
1495 @property
1495 @property
1496 def field_key_prefixed(self):
1496 def field_key_prefixed(self):
1497 return 'ex_%s' % self.field_key
1497 return 'ex_%s' % self.field_key
1498
1498
1499 @classmethod
1499 @classmethod
1500 def un_prefix_key(cls, key):
1500 def un_prefix_key(cls, key):
1501 if key.startswith(cls.PREFIX):
1501 if key.startswith(cls.PREFIX):
1502 return key[len(cls.PREFIX):]
1502 return key[len(cls.PREFIX):]
1503 return key
1503 return key
1504
1504
1505 @classmethod
1505 @classmethod
1506 def get_by_key_name(cls, key, repo):
1506 def get_by_key_name(cls, key, repo):
1507 row = cls.query()\
1507 row = cls.query()\
1508 .filter(cls.repository == repo)\
1508 .filter(cls.repository == repo)\
1509 .filter(cls.field_key == key).scalar()
1509 .filter(cls.field_key == key).scalar()
1510 return row
1510 return row
1511
1511
1512
1512
1513 class Repository(Base, BaseModel):
1513 class Repository(Base, BaseModel):
1514 __tablename__ = 'repositories'
1514 __tablename__ = 'repositories'
1515 __table_args__ = (
1515 __table_args__ = (
1516 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1516 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1519 )
1519 )
1520 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1520 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1521 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1521 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1522
1522
1523 STATE_CREATED = 'repo_state_created'
1523 STATE_CREATED = 'repo_state_created'
1524 STATE_PENDING = 'repo_state_pending'
1524 STATE_PENDING = 'repo_state_pending'
1525 STATE_ERROR = 'repo_state_error'
1525 STATE_ERROR = 'repo_state_error'
1526
1526
1527 LOCK_AUTOMATIC = 'lock_auto'
1527 LOCK_AUTOMATIC = 'lock_auto'
1528 LOCK_API = 'lock_api'
1528 LOCK_API = 'lock_api'
1529 LOCK_WEB = 'lock_web'
1529 LOCK_WEB = 'lock_web'
1530 LOCK_PULL = 'lock_pull'
1530 LOCK_PULL = 'lock_pull'
1531
1531
1532 NAME_SEP = URL_SEP
1532 NAME_SEP = URL_SEP
1533
1533
1534 repo_id = Column(
1534 repo_id = Column(
1535 "repo_id", Integer(), nullable=False, unique=True, default=None,
1535 "repo_id", Integer(), nullable=False, unique=True, default=None,
1536 primary_key=True)
1536 primary_key=True)
1537 _repo_name = Column(
1537 _repo_name = Column(
1538 "repo_name", Text(), nullable=False, default=None)
1538 "repo_name", Text(), nullable=False, default=None)
1539 _repo_name_hash = Column(
1539 _repo_name_hash = Column(
1540 "repo_name_hash", String(255), nullable=False, unique=True)
1540 "repo_name_hash", String(255), nullable=False, unique=True)
1541 repo_state = Column("repo_state", String(255), nullable=True)
1541 repo_state = Column("repo_state", String(255), nullable=True)
1542
1542
1543 clone_uri = Column(
1543 clone_uri = Column(
1544 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1544 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1545 default=None)
1545 default=None)
1546 repo_type = Column(
1546 repo_type = Column(
1547 "repo_type", String(255), nullable=False, unique=False, default=None)
1547 "repo_type", String(255), nullable=False, unique=False, default=None)
1548 user_id = Column(
1548 user_id = Column(
1549 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1549 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1550 unique=False, default=None)
1550 unique=False, default=None)
1551 private = Column(
1551 private = Column(
1552 "private", Boolean(), nullable=True, unique=None, default=None)
1552 "private", Boolean(), nullable=True, unique=None, default=None)
1553 enable_statistics = Column(
1553 enable_statistics = Column(
1554 "statistics", Boolean(), nullable=True, unique=None, default=True)
1554 "statistics", Boolean(), nullable=True, unique=None, default=True)
1555 enable_downloads = Column(
1555 enable_downloads = Column(
1556 "downloads", Boolean(), nullable=True, unique=None, default=True)
1556 "downloads", Boolean(), nullable=True, unique=None, default=True)
1557 description = Column(
1557 description = Column(
1558 "description", String(10000), nullable=True, unique=None, default=None)
1558 "description", String(10000), nullable=True, unique=None, default=None)
1559 created_on = Column(
1559 created_on = Column(
1560 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1560 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1561 default=datetime.datetime.now)
1561 default=datetime.datetime.now)
1562 updated_on = Column(
1562 updated_on = Column(
1563 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1563 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1564 default=datetime.datetime.now)
1564 default=datetime.datetime.now)
1565 _landing_revision = Column(
1565 _landing_revision = Column(
1566 "landing_revision", String(255), nullable=False, unique=False,
1566 "landing_revision", String(255), nullable=False, unique=False,
1567 default=None)
1567 default=None)
1568 enable_locking = Column(
1568 enable_locking = Column(
1569 "enable_locking", Boolean(), nullable=False, unique=None,
1569 "enable_locking", Boolean(), nullable=False, unique=None,
1570 default=False)
1570 default=False)
1571 _locked = Column(
1571 _locked = Column(
1572 "locked", String(255), nullable=True, unique=False, default=None)
1572 "locked", String(255), nullable=True, unique=False, default=None)
1573 _changeset_cache = Column(
1573 _changeset_cache = Column(
1574 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1574 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1575
1575
1576 fork_id = Column(
1576 fork_id = Column(
1577 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1577 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1578 nullable=True, unique=False, default=None)
1578 nullable=True, unique=False, default=None)
1579 group_id = Column(
1579 group_id = Column(
1580 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1580 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1581 unique=False, default=None)
1581 unique=False, default=None)
1582
1582
1583 user = relationship('User', lazy='joined')
1583 user = relationship('User', lazy='joined')
1584 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1584 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1585 group = relationship('RepoGroup', lazy='joined')
1585 group = relationship('RepoGroup', lazy='joined')
1586 repo_to_perm = relationship(
1586 repo_to_perm = relationship(
1587 'UserRepoToPerm', cascade='all',
1587 'UserRepoToPerm', cascade='all',
1588 order_by='UserRepoToPerm.repo_to_perm_id')
1588 order_by='UserRepoToPerm.repo_to_perm_id')
1589 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1589 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1590 stats = relationship('Statistics', cascade='all', uselist=False)
1590 stats = relationship('Statistics', cascade='all', uselist=False)
1591
1591
1592 followers = relationship(
1592 followers = relationship(
1593 'UserFollowing',
1593 'UserFollowing',
1594 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1594 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1595 cascade='all')
1595 cascade='all')
1596 extra_fields = relationship(
1596 extra_fields = relationship(
1597 'RepositoryField', cascade="all, delete, delete-orphan")
1597 'RepositoryField', cascade="all, delete, delete-orphan")
1598 logs = relationship('UserLog')
1598 logs = relationship('UserLog')
1599 comments = relationship(
1599 comments = relationship(
1600 'ChangesetComment', cascade="all, delete, delete-orphan")
1600 'ChangesetComment', cascade="all, delete, delete-orphan")
1601 pull_requests_source = relationship(
1601 pull_requests_source = relationship(
1602 'PullRequest',
1602 'PullRequest',
1603 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1603 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1604 cascade="all, delete, delete-orphan")
1604 cascade="all, delete, delete-orphan")
1605 pull_requests_target = relationship(
1605 pull_requests_target = relationship(
1606 'PullRequest',
1606 'PullRequest',
1607 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1607 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1608 cascade="all, delete, delete-orphan")
1608 cascade="all, delete, delete-orphan")
1609 ui = relationship('RepoRhodeCodeUi', cascade="all")
1609 ui = relationship('RepoRhodeCodeUi', cascade="all")
1610 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1610 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1611 integrations = relationship('Integration',
1611 integrations = relationship('Integration',
1612 cascade="all, delete, delete-orphan")
1612 cascade="all, delete, delete-orphan")
1613
1613
1614 def __unicode__(self):
1614 def __unicode__(self):
1615 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1615 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1616 safe_unicode(self.repo_name))
1616 safe_unicode(self.repo_name))
1617
1617
1618 @hybrid_property
1618 @hybrid_property
1619 def description_safe(self):
1619 def description_safe(self):
1620 from rhodecode.lib import helpers as h
1620 from rhodecode.lib import helpers as h
1621 return h.escape(self.description)
1621 return h.escape(self.description)
1622
1622
1623 @hybrid_property
1623 @hybrid_property
1624 def landing_rev(self):
1624 def landing_rev(self):
1625 # always should return [rev_type, rev]
1625 # always should return [rev_type, rev]
1626 if self._landing_revision:
1626 if self._landing_revision:
1627 _rev_info = self._landing_revision.split(':')
1627 _rev_info = self._landing_revision.split(':')
1628 if len(_rev_info) < 2:
1628 if len(_rev_info) < 2:
1629 _rev_info.insert(0, 'rev')
1629 _rev_info.insert(0, 'rev')
1630 return [_rev_info[0], _rev_info[1]]
1630 return [_rev_info[0], _rev_info[1]]
1631 return [None, None]
1631 return [None, None]
1632
1632
1633 @landing_rev.setter
1633 @landing_rev.setter
1634 def landing_rev(self, val):
1634 def landing_rev(self, val):
1635 if ':' not in val:
1635 if ':' not in val:
1636 raise ValueError('value must be delimited with `:` and consist '
1636 raise ValueError('value must be delimited with `:` and consist '
1637 'of <rev_type>:<rev>, got %s instead' % val)
1637 'of <rev_type>:<rev>, got %s instead' % val)
1638 self._landing_revision = val
1638 self._landing_revision = val
1639
1639
1640 @hybrid_property
1640 @hybrid_property
1641 def locked(self):
1641 def locked(self):
1642 if self._locked:
1642 if self._locked:
1643 user_id, timelocked, reason = self._locked.split(':')
1643 user_id, timelocked, reason = self._locked.split(':')
1644 lock_values = int(user_id), timelocked, reason
1644 lock_values = int(user_id), timelocked, reason
1645 else:
1645 else:
1646 lock_values = [None, None, None]
1646 lock_values = [None, None, None]
1647 return lock_values
1647 return lock_values
1648
1648
1649 @locked.setter
1649 @locked.setter
1650 def locked(self, val):
1650 def locked(self, val):
1651 if val and isinstance(val, (list, tuple)):
1651 if val and isinstance(val, (list, tuple)):
1652 self._locked = ':'.join(map(str, val))
1652 self._locked = ':'.join(map(str, val))
1653 else:
1653 else:
1654 self._locked = None
1654 self._locked = None
1655
1655
1656 @hybrid_property
1656 @hybrid_property
1657 def changeset_cache(self):
1657 def changeset_cache(self):
1658 from rhodecode.lib.vcs.backends.base import EmptyCommit
1658 from rhodecode.lib.vcs.backends.base import EmptyCommit
1659 dummy = EmptyCommit().__json__()
1659 dummy = EmptyCommit().__json__()
1660 if not self._changeset_cache:
1660 if not self._changeset_cache:
1661 return dummy
1661 return dummy
1662 try:
1662 try:
1663 return json.loads(self._changeset_cache)
1663 return json.loads(self._changeset_cache)
1664 except TypeError:
1664 except TypeError:
1665 return dummy
1665 return dummy
1666 except Exception:
1666 except Exception:
1667 log.error(traceback.format_exc())
1667 log.error(traceback.format_exc())
1668 return dummy
1668 return dummy
1669
1669
1670 @changeset_cache.setter
1670 @changeset_cache.setter
1671 def changeset_cache(self, val):
1671 def changeset_cache(self, val):
1672 try:
1672 try:
1673 self._changeset_cache = json.dumps(val)
1673 self._changeset_cache = json.dumps(val)
1674 except Exception:
1674 except Exception:
1675 log.error(traceback.format_exc())
1675 log.error(traceback.format_exc())
1676
1676
1677 @hybrid_property
1677 @hybrid_property
1678 def repo_name(self):
1678 def repo_name(self):
1679 return self._repo_name
1679 return self._repo_name
1680
1680
1681 @repo_name.setter
1681 @repo_name.setter
1682 def repo_name(self, value):
1682 def repo_name(self, value):
1683 self._repo_name = value
1683 self._repo_name = value
1684 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1684 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1685
1685
1686 @classmethod
1686 @classmethod
1687 def normalize_repo_name(cls, repo_name):
1687 def normalize_repo_name(cls, repo_name):
1688 """
1688 """
1689 Normalizes os specific repo_name to the format internally stored inside
1689 Normalizes os specific repo_name to the format internally stored inside
1690 database using URL_SEP
1690 database using URL_SEP
1691
1691
1692 :param cls:
1692 :param cls:
1693 :param repo_name:
1693 :param repo_name:
1694 """
1694 """
1695 return cls.NAME_SEP.join(repo_name.split(os.sep))
1695 return cls.NAME_SEP.join(repo_name.split(os.sep))
1696
1696
1697 @classmethod
1697 @classmethod
1698 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1698 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1699 session = Session()
1699 session = Session()
1700 q = session.query(cls).filter(cls.repo_name == repo_name)
1700 q = session.query(cls).filter(cls.repo_name == repo_name)
1701
1701
1702 if cache:
1702 if cache:
1703 if identity_cache:
1703 if identity_cache:
1704 val = cls.identity_cache(session, 'repo_name', repo_name)
1704 val = cls.identity_cache(session, 'repo_name', repo_name)
1705 if val:
1705 if val:
1706 return val
1706 return val
1707 else:
1707 else:
1708 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1708 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1709 q = q.options(
1709 q = q.options(
1710 FromCache("sql_cache_short", cache_key))
1710 FromCache("sql_cache_short", cache_key))
1711
1711
1712 return q.scalar()
1712 return q.scalar()
1713
1713
1714 @classmethod
1714 @classmethod
1715 def get_by_full_path(cls, repo_full_path):
1715 def get_by_full_path(cls, repo_full_path):
1716 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1716 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1717 repo_name = cls.normalize_repo_name(repo_name)
1717 repo_name = cls.normalize_repo_name(repo_name)
1718 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1718 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1719
1719
1720 @classmethod
1720 @classmethod
1721 def get_repo_forks(cls, repo_id):
1721 def get_repo_forks(cls, repo_id):
1722 return cls.query().filter(Repository.fork_id == repo_id)
1722 return cls.query().filter(Repository.fork_id == repo_id)
1723
1723
1724 @classmethod
1724 @classmethod
1725 def base_path(cls):
1725 def base_path(cls):
1726 """
1726 """
1727 Returns base path when all repos are stored
1727 Returns base path when all repos are stored
1728
1728
1729 :param cls:
1729 :param cls:
1730 """
1730 """
1731 q = Session().query(RhodeCodeUi)\
1731 q = Session().query(RhodeCodeUi)\
1732 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1732 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1733 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1733 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1734 return q.one().ui_value
1734 return q.one().ui_value
1735
1735
1736 @classmethod
1736 @classmethod
1737 def is_valid(cls, repo_name):
1737 def is_valid(cls, repo_name):
1738 """
1738 """
1739 returns True if given repo name is a valid filesystem repository
1739 returns True if given repo name is a valid filesystem repository
1740
1740
1741 :param cls:
1741 :param cls:
1742 :param repo_name:
1742 :param repo_name:
1743 """
1743 """
1744 from rhodecode.lib.utils import is_valid_repo
1744 from rhodecode.lib.utils import is_valid_repo
1745
1745
1746 return is_valid_repo(repo_name, cls.base_path())
1746 return is_valid_repo(repo_name, cls.base_path())
1747
1747
1748 @classmethod
1748 @classmethod
1749 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1749 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1750 case_insensitive=True):
1750 case_insensitive=True):
1751 q = Repository.query()
1751 q = Repository.query()
1752
1752
1753 if not isinstance(user_id, Optional):
1753 if not isinstance(user_id, Optional):
1754 q = q.filter(Repository.user_id == user_id)
1754 q = q.filter(Repository.user_id == user_id)
1755
1755
1756 if not isinstance(group_id, Optional):
1756 if not isinstance(group_id, Optional):
1757 q = q.filter(Repository.group_id == group_id)
1757 q = q.filter(Repository.group_id == group_id)
1758
1758
1759 if case_insensitive:
1759 if case_insensitive:
1760 q = q.order_by(func.lower(Repository.repo_name))
1760 q = q.order_by(func.lower(Repository.repo_name))
1761 else:
1761 else:
1762 q = q.order_by(Repository.repo_name)
1762 q = q.order_by(Repository.repo_name)
1763 return q.all()
1763 return q.all()
1764
1764
1765 @property
1765 @property
1766 def forks(self):
1766 def forks(self):
1767 """
1767 """
1768 Return forks of this repo
1768 Return forks of this repo
1769 """
1769 """
1770 return Repository.get_repo_forks(self.repo_id)
1770 return Repository.get_repo_forks(self.repo_id)
1771
1771
1772 @property
1772 @property
1773 def parent(self):
1773 def parent(self):
1774 """
1774 """
1775 Returns fork parent
1775 Returns fork parent
1776 """
1776 """
1777 return self.fork
1777 return self.fork
1778
1778
1779 @property
1779 @property
1780 def just_name(self):
1780 def just_name(self):
1781 return self.repo_name.split(self.NAME_SEP)[-1]
1781 return self.repo_name.split(self.NAME_SEP)[-1]
1782
1782
1783 @property
1783 @property
1784 def groups_with_parents(self):
1784 def groups_with_parents(self):
1785 groups = []
1785 groups = []
1786 if self.group is None:
1786 if self.group is None:
1787 return groups
1787 return groups
1788
1788
1789 cur_gr = self.group
1789 cur_gr = self.group
1790 groups.insert(0, cur_gr)
1790 groups.insert(0, cur_gr)
1791 while 1:
1791 while 1:
1792 gr = getattr(cur_gr, 'parent_group', None)
1792 gr = getattr(cur_gr, 'parent_group', None)
1793 cur_gr = cur_gr.parent_group
1793 cur_gr = cur_gr.parent_group
1794 if gr is None:
1794 if gr is None:
1795 break
1795 break
1796 groups.insert(0, gr)
1796 groups.insert(0, gr)
1797
1797
1798 return groups
1798 return groups
1799
1799
1800 @property
1800 @property
1801 def groups_and_repo(self):
1801 def groups_and_repo(self):
1802 return self.groups_with_parents, self
1802 return self.groups_with_parents, self
1803
1803
1804 @LazyProperty
1804 @LazyProperty
1805 def repo_path(self):
1805 def repo_path(self):
1806 """
1806 """
1807 Returns base full path for that repository means where it actually
1807 Returns base full path for that repository means where it actually
1808 exists on a filesystem
1808 exists on a filesystem
1809 """
1809 """
1810 q = Session().query(RhodeCodeUi).filter(
1810 q = Session().query(RhodeCodeUi).filter(
1811 RhodeCodeUi.ui_key == self.NAME_SEP)
1811 RhodeCodeUi.ui_key == self.NAME_SEP)
1812 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1812 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1813 return q.one().ui_value
1813 return q.one().ui_value
1814
1814
1815 @property
1815 @property
1816 def repo_full_path(self):
1816 def repo_full_path(self):
1817 p = [self.repo_path]
1817 p = [self.repo_path]
1818 # we need to split the name by / since this is how we store the
1818 # we need to split the name by / since this is how we store the
1819 # names in the database, but that eventually needs to be converted
1819 # names in the database, but that eventually needs to be converted
1820 # into a valid system path
1820 # into a valid system path
1821 p += self.repo_name.split(self.NAME_SEP)
1821 p += self.repo_name.split(self.NAME_SEP)
1822 return os.path.join(*map(safe_unicode, p))
1822 return os.path.join(*map(safe_unicode, p))
1823
1823
1824 @property
1824 @property
1825 def cache_keys(self):
1825 def cache_keys(self):
1826 """
1826 """
1827 Returns associated cache keys for that repo
1827 Returns associated cache keys for that repo
1828 """
1828 """
1829 return CacheKey.query()\
1829 return CacheKey.query()\
1830 .filter(CacheKey.cache_args == self.repo_name)\
1830 .filter(CacheKey.cache_args == self.repo_name)\
1831 .order_by(CacheKey.cache_key)\
1831 .order_by(CacheKey.cache_key)\
1832 .all()
1832 .all()
1833
1833
1834 def get_new_name(self, repo_name):
1834 def get_new_name(self, repo_name):
1835 """
1835 """
1836 returns new full repository name based on assigned group and new new
1836 returns new full repository name based on assigned group and new new
1837
1837
1838 :param group_name:
1838 :param group_name:
1839 """
1839 """
1840 path_prefix = self.group.full_path_splitted if self.group else []
1840 path_prefix = self.group.full_path_splitted if self.group else []
1841 return self.NAME_SEP.join(path_prefix + [repo_name])
1841 return self.NAME_SEP.join(path_prefix + [repo_name])
1842
1842
1843 @property
1843 @property
1844 def _config(self):
1844 def _config(self):
1845 """
1845 """
1846 Returns db based config object.
1846 Returns db based config object.
1847 """
1847 """
1848 from rhodecode.lib.utils import make_db_config
1848 from rhodecode.lib.utils import make_db_config
1849 return make_db_config(clear_session=False, repo=self)
1849 return make_db_config(clear_session=False, repo=self)
1850
1850
1851 def permissions(self, with_admins=True, with_owner=True):
1851 def permissions(self, with_admins=True, with_owner=True):
1852 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1852 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1853 q = q.options(joinedload(UserRepoToPerm.repository),
1853 q = q.options(joinedload(UserRepoToPerm.repository),
1854 joinedload(UserRepoToPerm.user),
1854 joinedload(UserRepoToPerm.user),
1855 joinedload(UserRepoToPerm.permission),)
1855 joinedload(UserRepoToPerm.permission),)
1856
1856
1857 # get owners and admins and permissions. We do a trick of re-writing
1857 # get owners and admins and permissions. We do a trick of re-writing
1858 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1858 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1859 # has a global reference and changing one object propagates to all
1859 # has a global reference and changing one object propagates to all
1860 # others. This means if admin is also an owner admin_row that change
1860 # others. This means if admin is also an owner admin_row that change
1861 # would propagate to both objects
1861 # would propagate to both objects
1862 perm_rows = []
1862 perm_rows = []
1863 for _usr in q.all():
1863 for _usr in q.all():
1864 usr = AttributeDict(_usr.user.get_dict())
1864 usr = AttributeDict(_usr.user.get_dict())
1865 usr.permission = _usr.permission.permission_name
1865 usr.permission = _usr.permission.permission_name
1866 perm_rows.append(usr)
1866 perm_rows.append(usr)
1867
1867
1868 # filter the perm rows by 'default' first and then sort them by
1868 # filter the perm rows by 'default' first and then sort them by
1869 # admin,write,read,none permissions sorted again alphabetically in
1869 # admin,write,read,none permissions sorted again alphabetically in
1870 # each group
1870 # each group
1871 perm_rows = sorted(perm_rows, key=display_user_sort)
1871 perm_rows = sorted(perm_rows, key=display_user_sort)
1872
1872
1873 _admin_perm = 'repository.admin'
1873 _admin_perm = 'repository.admin'
1874 owner_row = []
1874 owner_row = []
1875 if with_owner:
1875 if with_owner:
1876 usr = AttributeDict(self.user.get_dict())
1876 usr = AttributeDict(self.user.get_dict())
1877 usr.owner_row = True
1877 usr.owner_row = True
1878 usr.permission = _admin_perm
1878 usr.permission = _admin_perm
1879 owner_row.append(usr)
1879 owner_row.append(usr)
1880
1880
1881 super_admin_rows = []
1881 super_admin_rows = []
1882 if with_admins:
1882 if with_admins:
1883 for usr in User.get_all_super_admins():
1883 for usr in User.get_all_super_admins():
1884 # if this admin is also owner, don't double the record
1884 # if this admin is also owner, don't double the record
1885 if usr.user_id == owner_row[0].user_id:
1885 if usr.user_id == owner_row[0].user_id:
1886 owner_row[0].admin_row = True
1886 owner_row[0].admin_row = True
1887 else:
1887 else:
1888 usr = AttributeDict(usr.get_dict())
1888 usr = AttributeDict(usr.get_dict())
1889 usr.admin_row = True
1889 usr.admin_row = True
1890 usr.permission = _admin_perm
1890 usr.permission = _admin_perm
1891 super_admin_rows.append(usr)
1891 super_admin_rows.append(usr)
1892
1892
1893 return super_admin_rows + owner_row + perm_rows
1893 return super_admin_rows + owner_row + perm_rows
1894
1894
1895 def permission_user_groups(self):
1895 def permission_user_groups(self):
1896 q = UserGroupRepoToPerm.query().filter(
1896 q = UserGroupRepoToPerm.query().filter(
1897 UserGroupRepoToPerm.repository == self)
1897 UserGroupRepoToPerm.repository == self)
1898 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1898 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1899 joinedload(UserGroupRepoToPerm.users_group),
1899 joinedload(UserGroupRepoToPerm.users_group),
1900 joinedload(UserGroupRepoToPerm.permission),)
1900 joinedload(UserGroupRepoToPerm.permission),)
1901
1901
1902 perm_rows = []
1902 perm_rows = []
1903 for _user_group in q.all():
1903 for _user_group in q.all():
1904 usr = AttributeDict(_user_group.users_group.get_dict())
1904 usr = AttributeDict(_user_group.users_group.get_dict())
1905 usr.permission = _user_group.permission.permission_name
1905 usr.permission = _user_group.permission.permission_name
1906 perm_rows.append(usr)
1906 perm_rows.append(usr)
1907
1907
1908 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1908 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1909 return perm_rows
1909 return perm_rows
1910
1910
1911 def get_api_data(self, include_secrets=False):
1911 def get_api_data(self, include_secrets=False):
1912 """
1912 """
1913 Common function for generating repo api data
1913 Common function for generating repo api data
1914
1914
1915 :param include_secrets: See :meth:`User.get_api_data`.
1915 :param include_secrets: See :meth:`User.get_api_data`.
1916
1916
1917 """
1917 """
1918 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1918 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1919 # move this methods on models level.
1919 # move this methods on models level.
1920 from rhodecode.model.settings import SettingsModel
1920 from rhodecode.model.settings import SettingsModel
1921 from rhodecode.model.repo import RepoModel
1921 from rhodecode.model.repo import RepoModel
1922
1922
1923 repo = self
1923 repo = self
1924 _user_id, _time, _reason = self.locked
1924 _user_id, _time, _reason = self.locked
1925
1925
1926 data = {
1926 data = {
1927 'repo_id': repo.repo_id,
1927 'repo_id': repo.repo_id,
1928 'repo_name': repo.repo_name,
1928 'repo_name': repo.repo_name,
1929 'repo_type': repo.repo_type,
1929 'repo_type': repo.repo_type,
1930 'clone_uri': repo.clone_uri or '',
1930 'clone_uri': repo.clone_uri or '',
1931 'url': RepoModel().get_url(self),
1931 'url': RepoModel().get_url(self),
1932 'private': repo.private,
1932 'private': repo.private,
1933 'created_on': repo.created_on,
1933 'created_on': repo.created_on,
1934 'description': repo.description_safe,
1934 'description': repo.description_safe,
1935 'landing_rev': repo.landing_rev,
1935 'landing_rev': repo.landing_rev,
1936 'owner': repo.user.username,
1936 'owner': repo.user.username,
1937 'fork_of': repo.fork.repo_name if repo.fork else None,
1937 'fork_of': repo.fork.repo_name if repo.fork else None,
1938 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1938 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1939 'enable_statistics': repo.enable_statistics,
1939 'enable_statistics': repo.enable_statistics,
1940 'enable_locking': repo.enable_locking,
1940 'enable_locking': repo.enable_locking,
1941 'enable_downloads': repo.enable_downloads,
1941 'enable_downloads': repo.enable_downloads,
1942 'last_changeset': repo.changeset_cache,
1942 'last_changeset': repo.changeset_cache,
1943 'locked_by': User.get(_user_id).get_api_data(
1943 'locked_by': User.get(_user_id).get_api_data(
1944 include_secrets=include_secrets) if _user_id else None,
1944 include_secrets=include_secrets) if _user_id else None,
1945 'locked_date': time_to_datetime(_time) if _time else None,
1945 'locked_date': time_to_datetime(_time) if _time else None,
1946 'lock_reason': _reason if _reason else None,
1946 'lock_reason': _reason if _reason else None,
1947 }
1947 }
1948
1948
1949 # TODO: mikhail: should be per-repo settings here
1949 # TODO: mikhail: should be per-repo settings here
1950 rc_config = SettingsModel().get_all_settings()
1950 rc_config = SettingsModel().get_all_settings()
1951 repository_fields = str2bool(
1951 repository_fields = str2bool(
1952 rc_config.get('rhodecode_repository_fields'))
1952 rc_config.get('rhodecode_repository_fields'))
1953 if repository_fields:
1953 if repository_fields:
1954 for f in self.extra_fields:
1954 for f in self.extra_fields:
1955 data[f.field_key_prefixed] = f.field_value
1955 data[f.field_key_prefixed] = f.field_value
1956
1956
1957 return data
1957 return data
1958
1958
1959 @classmethod
1959 @classmethod
1960 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1960 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1961 if not lock_time:
1961 if not lock_time:
1962 lock_time = time.time()
1962 lock_time = time.time()
1963 if not lock_reason:
1963 if not lock_reason:
1964 lock_reason = cls.LOCK_AUTOMATIC
1964 lock_reason = cls.LOCK_AUTOMATIC
1965 repo.locked = [user_id, lock_time, lock_reason]
1965 repo.locked = [user_id, lock_time, lock_reason]
1966 Session().add(repo)
1966 Session().add(repo)
1967 Session().commit()
1967 Session().commit()
1968
1968
1969 @classmethod
1969 @classmethod
1970 def unlock(cls, repo):
1970 def unlock(cls, repo):
1971 repo.locked = None
1971 repo.locked = None
1972 Session().add(repo)
1972 Session().add(repo)
1973 Session().commit()
1973 Session().commit()
1974
1974
1975 @classmethod
1975 @classmethod
1976 def getlock(cls, repo):
1976 def getlock(cls, repo):
1977 return repo.locked
1977 return repo.locked
1978
1978
1979 def is_user_lock(self, user_id):
1979 def is_user_lock(self, user_id):
1980 if self.lock[0]:
1980 if self.lock[0]:
1981 lock_user_id = safe_int(self.lock[0])
1981 lock_user_id = safe_int(self.lock[0])
1982 user_id = safe_int(user_id)
1982 user_id = safe_int(user_id)
1983 # both are ints, and they are equal
1983 # both are ints, and they are equal
1984 return all([lock_user_id, user_id]) and lock_user_id == user_id
1984 return all([lock_user_id, user_id]) and lock_user_id == user_id
1985
1985
1986 return False
1986 return False
1987
1987
1988 def get_locking_state(self, action, user_id, only_when_enabled=True):
1988 def get_locking_state(self, action, user_id, only_when_enabled=True):
1989 """
1989 """
1990 Checks locking on this repository, if locking is enabled and lock is
1990 Checks locking on this repository, if locking is enabled and lock is
1991 present returns a tuple of make_lock, locked, locked_by.
1991 present returns a tuple of make_lock, locked, locked_by.
1992 make_lock can have 3 states None (do nothing) True, make lock
1992 make_lock can have 3 states None (do nothing) True, make lock
1993 False release lock, This value is later propagated to hooks, which
1993 False release lock, This value is later propagated to hooks, which
1994 do the locking. Think about this as signals passed to hooks what to do.
1994 do the locking. Think about this as signals passed to hooks what to do.
1995
1995
1996 """
1996 """
1997 # TODO: johbo: This is part of the business logic and should be moved
1997 # TODO: johbo: This is part of the business logic and should be moved
1998 # into the RepositoryModel.
1998 # into the RepositoryModel.
1999
1999
2000 if action not in ('push', 'pull'):
2000 if action not in ('push', 'pull'):
2001 raise ValueError("Invalid action value: %s" % repr(action))
2001 raise ValueError("Invalid action value: %s" % repr(action))
2002
2002
2003 # defines if locked error should be thrown to user
2003 # defines if locked error should be thrown to user
2004 currently_locked = False
2004 currently_locked = False
2005 # defines if new lock should be made, tri-state
2005 # defines if new lock should be made, tri-state
2006 make_lock = None
2006 make_lock = None
2007 repo = self
2007 repo = self
2008 user = User.get(user_id)
2008 user = User.get(user_id)
2009
2009
2010 lock_info = repo.locked
2010 lock_info = repo.locked
2011
2011
2012 if repo and (repo.enable_locking or not only_when_enabled):
2012 if repo and (repo.enable_locking or not only_when_enabled):
2013 if action == 'push':
2013 if action == 'push':
2014 # check if it's already locked !, if it is compare users
2014 # check if it's already locked !, if it is compare users
2015 locked_by_user_id = lock_info[0]
2015 locked_by_user_id = lock_info[0]
2016 if user.user_id == locked_by_user_id:
2016 if user.user_id == locked_by_user_id:
2017 log.debug(
2017 log.debug(
2018 'Got `push` action from user %s, now unlocking', user)
2018 'Got `push` action from user %s, now unlocking', user)
2019 # unlock if we have push from user who locked
2019 # unlock if we have push from user who locked
2020 make_lock = False
2020 make_lock = False
2021 else:
2021 else:
2022 # we're not the same user who locked, ban with
2022 # we're not the same user who locked, ban with
2023 # code defined in settings (default is 423 HTTP Locked) !
2023 # code defined in settings (default is 423 HTTP Locked) !
2024 log.debug('Repo %s is currently locked by %s', repo, user)
2024 log.debug('Repo %s is currently locked by %s', repo, user)
2025 currently_locked = True
2025 currently_locked = True
2026 elif action == 'pull':
2026 elif action == 'pull':
2027 # [0] user [1] date
2027 # [0] user [1] date
2028 if lock_info[0] and lock_info[1]:
2028 if lock_info[0] and lock_info[1]:
2029 log.debug('Repo %s is currently locked by %s', repo, user)
2029 log.debug('Repo %s is currently locked by %s', repo, user)
2030 currently_locked = True
2030 currently_locked = True
2031 else:
2031 else:
2032 log.debug('Setting lock on repo %s by %s', repo, user)
2032 log.debug('Setting lock on repo %s by %s', repo, user)
2033 make_lock = True
2033 make_lock = True
2034
2034
2035 else:
2035 else:
2036 log.debug('Repository %s do not have locking enabled', repo)
2036 log.debug('Repository %s do not have locking enabled', repo)
2037
2037
2038 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2038 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2039 make_lock, currently_locked, lock_info)
2039 make_lock, currently_locked, lock_info)
2040
2040
2041 from rhodecode.lib.auth import HasRepoPermissionAny
2041 from rhodecode.lib.auth import HasRepoPermissionAny
2042 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2042 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2043 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2043 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2044 # if we don't have at least write permission we cannot make a lock
2044 # if we don't have at least write permission we cannot make a lock
2045 log.debug('lock state reset back to FALSE due to lack '
2045 log.debug('lock state reset back to FALSE due to lack '
2046 'of at least read permission')
2046 'of at least read permission')
2047 make_lock = False
2047 make_lock = False
2048
2048
2049 return make_lock, currently_locked, lock_info
2049 return make_lock, currently_locked, lock_info
2050
2050
2051 @property
2051 @property
2052 def last_db_change(self):
2052 def last_db_change(self):
2053 return self.updated_on
2053 return self.updated_on
2054
2054
2055 @property
2055 @property
2056 def clone_uri_hidden(self):
2056 def clone_uri_hidden(self):
2057 clone_uri = self.clone_uri
2057 clone_uri = self.clone_uri
2058 if clone_uri:
2058 if clone_uri:
2059 import urlobject
2059 import urlobject
2060 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2060 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2061 if url_obj.password:
2061 if url_obj.password:
2062 clone_uri = url_obj.with_password('*****')
2062 clone_uri = url_obj.with_password('*****')
2063 return clone_uri
2063 return clone_uri
2064
2064
2065 def clone_url(self, **override):
2065 def clone_url(self, **override):
2066 from rhodecode.model.settings import SettingsModel
2066 from rhodecode.model.settings import SettingsModel
2067
2067
2068 uri_tmpl = None
2068 uri_tmpl = None
2069 if 'with_id' in override:
2069 if 'with_id' in override:
2070 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2070 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2071 del override['with_id']
2071 del override['with_id']
2072
2072
2073 if 'uri_tmpl' in override:
2073 if 'uri_tmpl' in override:
2074 uri_tmpl = override['uri_tmpl']
2074 uri_tmpl = override['uri_tmpl']
2075 del override['uri_tmpl']
2075 del override['uri_tmpl']
2076
2076
2077 # we didn't override our tmpl from **overrides
2077 # we didn't override our tmpl from **overrides
2078 if not uri_tmpl:
2078 if not uri_tmpl:
2079 rc_config = SettingsModel().get_all_settings(cache=True)
2079 rc_config = SettingsModel().get_all_settings(cache=True)
2080 uri_tmpl = rc_config.get(
2080 uri_tmpl = rc_config.get(
2081 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2081 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2082
2082
2083 request = get_current_request()
2083 request = get_current_request()
2084 return get_clone_url(request=request,
2084 return get_clone_url(request=request,
2085 uri_tmpl=uri_tmpl,
2085 uri_tmpl=uri_tmpl,
2086 repo_name=self.repo_name,
2086 repo_name=self.repo_name,
2087 repo_id=self.repo_id, **override)
2087 repo_id=self.repo_id, **override)
2088
2088
2089 def set_state(self, state):
2089 def set_state(self, state):
2090 self.repo_state = state
2090 self.repo_state = state
2091 Session().add(self)
2091 Session().add(self)
2092 #==========================================================================
2092 #==========================================================================
2093 # SCM PROPERTIES
2093 # SCM PROPERTIES
2094 #==========================================================================
2094 #==========================================================================
2095
2095
2096 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2096 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2097 return get_commit_safe(
2097 return get_commit_safe(
2098 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2098 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2099
2099
2100 def get_changeset(self, rev=None, pre_load=None):
2100 def get_changeset(self, rev=None, pre_load=None):
2101 warnings.warn("Use get_commit", DeprecationWarning)
2101 warnings.warn("Use get_commit", DeprecationWarning)
2102 commit_id = None
2102 commit_id = None
2103 commit_idx = None
2103 commit_idx = None
2104 if isinstance(rev, basestring):
2104 if isinstance(rev, basestring):
2105 commit_id = rev
2105 commit_id = rev
2106 else:
2106 else:
2107 commit_idx = rev
2107 commit_idx = rev
2108 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2108 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2109 pre_load=pre_load)
2109 pre_load=pre_load)
2110
2110
2111 def get_landing_commit(self):
2111 def get_landing_commit(self):
2112 """
2112 """
2113 Returns landing commit, or if that doesn't exist returns the tip
2113 Returns landing commit, or if that doesn't exist returns the tip
2114 """
2114 """
2115 _rev_type, _rev = self.landing_rev
2115 _rev_type, _rev = self.landing_rev
2116 commit = self.get_commit(_rev)
2116 commit = self.get_commit(_rev)
2117 if isinstance(commit, EmptyCommit):
2117 if isinstance(commit, EmptyCommit):
2118 return self.get_commit()
2118 return self.get_commit()
2119 return commit
2119 return commit
2120
2120
2121 def update_commit_cache(self, cs_cache=None, config=None):
2121 def update_commit_cache(self, cs_cache=None, config=None):
2122 """
2122 """
2123 Update cache of last changeset for repository, keys should be::
2123 Update cache of last changeset for repository, keys should be::
2124
2124
2125 short_id
2125 short_id
2126 raw_id
2126 raw_id
2127 revision
2127 revision
2128 parents
2128 parents
2129 message
2129 message
2130 date
2130 date
2131 author
2131 author
2132
2132
2133 :param cs_cache:
2133 :param cs_cache:
2134 """
2134 """
2135 from rhodecode.lib.vcs.backends.base import BaseChangeset
2135 from rhodecode.lib.vcs.backends.base import BaseChangeset
2136 if cs_cache is None:
2136 if cs_cache is None:
2137 # use no-cache version here
2137 # use no-cache version here
2138 scm_repo = self.scm_instance(cache=False, config=config)
2138 scm_repo = self.scm_instance(cache=False, config=config)
2139 if scm_repo:
2139 if scm_repo:
2140 cs_cache = scm_repo.get_commit(
2140 cs_cache = scm_repo.get_commit(
2141 pre_load=["author", "date", "message", "parents"])
2141 pre_load=["author", "date", "message", "parents"])
2142 else:
2142 else:
2143 cs_cache = EmptyCommit()
2143 cs_cache = EmptyCommit()
2144
2144
2145 if isinstance(cs_cache, BaseChangeset):
2145 if isinstance(cs_cache, BaseChangeset):
2146 cs_cache = cs_cache.__json__()
2146 cs_cache = cs_cache.__json__()
2147
2147
2148 def is_outdated(new_cs_cache):
2148 def is_outdated(new_cs_cache):
2149 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2149 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2150 new_cs_cache['revision'] != self.changeset_cache['revision']):
2150 new_cs_cache['revision'] != self.changeset_cache['revision']):
2151 return True
2151 return True
2152 return False
2152 return False
2153
2153
2154 # check if we have maybe already latest cached revision
2154 # check if we have maybe already latest cached revision
2155 if is_outdated(cs_cache) or not self.changeset_cache:
2155 if is_outdated(cs_cache) or not self.changeset_cache:
2156 _default = datetime.datetime.fromtimestamp(0)
2156 _default = datetime.datetime.fromtimestamp(0)
2157 last_change = cs_cache.get('date') or _default
2157 last_change = cs_cache.get('date') or _default
2158 log.debug('updated repo %s with new cs cache %s',
2158 log.debug('updated repo %s with new cs cache %s',
2159 self.repo_name, cs_cache)
2159 self.repo_name, cs_cache)
2160 self.updated_on = last_change
2160 self.updated_on = last_change
2161 self.changeset_cache = cs_cache
2161 self.changeset_cache = cs_cache
2162 Session().add(self)
2162 Session().add(self)
2163 Session().commit()
2163 Session().commit()
2164 else:
2164 else:
2165 log.debug('Skipping update_commit_cache for repo:`%s` '
2165 log.debug('Skipping update_commit_cache for repo:`%s` '
2166 'commit already with latest changes', self.repo_name)
2166 'commit already with latest changes', self.repo_name)
2167
2167
2168 @property
2168 @property
2169 def tip(self):
2169 def tip(self):
2170 return self.get_commit('tip')
2170 return self.get_commit('tip')
2171
2171
2172 @property
2172 @property
2173 def author(self):
2173 def author(self):
2174 return self.tip.author
2174 return self.tip.author
2175
2175
2176 @property
2176 @property
2177 def last_change(self):
2177 def last_change(self):
2178 return self.scm_instance().last_change
2178 return self.scm_instance().last_change
2179
2179
2180 def get_comments(self, revisions=None):
2180 def get_comments(self, revisions=None):
2181 """
2181 """
2182 Returns comments for this repository grouped by revisions
2182 Returns comments for this repository grouped by revisions
2183
2183
2184 :param revisions: filter query by revisions only
2184 :param revisions: filter query by revisions only
2185 """
2185 """
2186 cmts = ChangesetComment.query()\
2186 cmts = ChangesetComment.query()\
2187 .filter(ChangesetComment.repo == self)
2187 .filter(ChangesetComment.repo == self)
2188 if revisions:
2188 if revisions:
2189 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2189 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2190 grouped = collections.defaultdict(list)
2190 grouped = collections.defaultdict(list)
2191 for cmt in cmts.all():
2191 for cmt in cmts.all():
2192 grouped[cmt.revision].append(cmt)
2192 grouped[cmt.revision].append(cmt)
2193 return grouped
2193 return grouped
2194
2194
2195 def statuses(self, revisions=None):
2195 def statuses(self, revisions=None):
2196 """
2196 """
2197 Returns statuses for this repository
2197 Returns statuses for this repository
2198
2198
2199 :param revisions: list of revisions to get statuses for
2199 :param revisions: list of revisions to get statuses for
2200 """
2200 """
2201 statuses = ChangesetStatus.query()\
2201 statuses = ChangesetStatus.query()\
2202 .filter(ChangesetStatus.repo == self)\
2202 .filter(ChangesetStatus.repo == self)\
2203 .filter(ChangesetStatus.version == 0)
2203 .filter(ChangesetStatus.version == 0)
2204
2204
2205 if revisions:
2205 if revisions:
2206 # Try doing the filtering in chunks to avoid hitting limits
2206 # Try doing the filtering in chunks to avoid hitting limits
2207 size = 500
2207 size = 500
2208 status_results = []
2208 status_results = []
2209 for chunk in xrange(0, len(revisions), size):
2209 for chunk in xrange(0, len(revisions), size):
2210 status_results += statuses.filter(
2210 status_results += statuses.filter(
2211 ChangesetStatus.revision.in_(
2211 ChangesetStatus.revision.in_(
2212 revisions[chunk: chunk+size])
2212 revisions[chunk: chunk+size])
2213 ).all()
2213 ).all()
2214 else:
2214 else:
2215 status_results = statuses.all()
2215 status_results = statuses.all()
2216
2216
2217 grouped = {}
2217 grouped = {}
2218
2218
2219 # maybe we have open new pullrequest without a status?
2219 # maybe we have open new pullrequest without a status?
2220 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2220 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2221 status_lbl = ChangesetStatus.get_status_lbl(stat)
2221 status_lbl = ChangesetStatus.get_status_lbl(stat)
2222 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2222 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2223 for rev in pr.revisions:
2223 for rev in pr.revisions:
2224 pr_id = pr.pull_request_id
2224 pr_id = pr.pull_request_id
2225 pr_repo = pr.target_repo.repo_name
2225 pr_repo = pr.target_repo.repo_name
2226 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2226 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2227
2227
2228 for stat in status_results:
2228 for stat in status_results:
2229 pr_id = pr_repo = None
2229 pr_id = pr_repo = None
2230 if stat.pull_request:
2230 if stat.pull_request:
2231 pr_id = stat.pull_request.pull_request_id
2231 pr_id = stat.pull_request.pull_request_id
2232 pr_repo = stat.pull_request.target_repo.repo_name
2232 pr_repo = stat.pull_request.target_repo.repo_name
2233 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2233 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2234 pr_id, pr_repo]
2234 pr_id, pr_repo]
2235 return grouped
2235 return grouped
2236
2236
2237 # ==========================================================================
2237 # ==========================================================================
2238 # SCM CACHE INSTANCE
2238 # SCM CACHE INSTANCE
2239 # ==========================================================================
2239 # ==========================================================================
2240
2240
2241 def scm_instance(self, **kwargs):
2241 def scm_instance(self, **kwargs):
2242 import rhodecode
2242 import rhodecode
2243
2243
2244 # Passing a config will not hit the cache currently only used
2244 # Passing a config will not hit the cache currently only used
2245 # for repo2dbmapper
2245 # for repo2dbmapper
2246 config = kwargs.pop('config', None)
2246 config = kwargs.pop('config', None)
2247 cache = kwargs.pop('cache', None)
2247 cache = kwargs.pop('cache', None)
2248 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2248 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2249 # if cache is NOT defined use default global, else we have a full
2249 # if cache is NOT defined use default global, else we have a full
2250 # control over cache behaviour
2250 # control over cache behaviour
2251 if cache is None and full_cache and not config:
2251 if cache is None and full_cache and not config:
2252 return self._get_instance_cached()
2252 return self._get_instance_cached()
2253 return self._get_instance(cache=bool(cache), config=config)
2253 return self._get_instance(cache=bool(cache), config=config)
2254
2254
2255 def _get_instance_cached(self):
2255 def _get_instance_cached(self):
2256 @cache_region('long_term')
2256 @cache_region('long_term')
2257 def _get_repo(cache_key):
2257 def _get_repo(cache_key):
2258 return self._get_instance()
2258 return self._get_instance()
2259
2259
2260 invalidator_context = CacheKey.repo_context_cache(
2260 invalidator_context = CacheKey.repo_context_cache(
2261 _get_repo, self.repo_name, None, thread_scoped=True)
2261 _get_repo, self.repo_name, None, thread_scoped=True)
2262
2262
2263 with invalidator_context as context:
2263 with invalidator_context as context:
2264 context.invalidate()
2264 context.invalidate()
2265 repo = context.compute()
2265 repo = context.compute()
2266
2266
2267 return repo
2267 return repo
2268
2268
2269 def _get_instance(self, cache=True, config=None):
2269 def _get_instance(self, cache=True, config=None):
2270 config = config or self._config
2270 config = config or self._config
2271 custom_wire = {
2271 custom_wire = {
2272 'cache': cache # controls the vcs.remote cache
2272 'cache': cache # controls the vcs.remote cache
2273 }
2273 }
2274 repo = get_vcs_instance(
2274 repo = get_vcs_instance(
2275 repo_path=safe_str(self.repo_full_path),
2275 repo_path=safe_str(self.repo_full_path),
2276 config=config,
2276 config=config,
2277 with_wire=custom_wire,
2277 with_wire=custom_wire,
2278 create=False,
2278 create=False,
2279 _vcs_alias=self.repo_type)
2279 _vcs_alias=self.repo_type)
2280
2280
2281 return repo
2281 return repo
2282
2282
2283 def __json__(self):
2283 def __json__(self):
2284 return {'landing_rev': self.landing_rev}
2284 return {'landing_rev': self.landing_rev}
2285
2285
2286 def get_dict(self):
2286 def get_dict(self):
2287
2287
2288 # Since we transformed `repo_name` to a hybrid property, we need to
2288 # Since we transformed `repo_name` to a hybrid property, we need to
2289 # keep compatibility with the code which uses `repo_name` field.
2289 # keep compatibility with the code which uses `repo_name` field.
2290
2290
2291 result = super(Repository, self).get_dict()
2291 result = super(Repository, self).get_dict()
2292 result['repo_name'] = result.pop('_repo_name', None)
2292 result['repo_name'] = result.pop('_repo_name', None)
2293 return result
2293 return result
2294
2294
2295
2295
2296 class RepoGroup(Base, BaseModel):
2296 class RepoGroup(Base, BaseModel):
2297 __tablename__ = 'groups'
2297 __tablename__ = 'groups'
2298 __table_args__ = (
2298 __table_args__ = (
2299 UniqueConstraint('group_name', 'group_parent_id'),
2299 UniqueConstraint('group_name', 'group_parent_id'),
2300 CheckConstraint('group_id != group_parent_id'),
2300 CheckConstraint('group_id != group_parent_id'),
2301 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2301 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2302 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2302 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2303 )
2303 )
2304 __mapper_args__ = {'order_by': 'group_name'}
2304 __mapper_args__ = {'order_by': 'group_name'}
2305
2305
2306 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2306 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2307
2307
2308 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2308 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2309 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2309 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2310 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2310 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2311 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2311 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2312 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2312 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2313 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2313 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2314 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2314 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2315 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2315 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2316 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2316 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2317
2317
2318 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2318 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2319 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2319 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2320 parent_group = relationship('RepoGroup', remote_side=group_id)
2320 parent_group = relationship('RepoGroup', remote_side=group_id)
2321 user = relationship('User')
2321 user = relationship('User')
2322 integrations = relationship('Integration',
2322 integrations = relationship('Integration',
2323 cascade="all, delete, delete-orphan")
2323 cascade="all, delete, delete-orphan")
2324
2324
2325 def __init__(self, group_name='', parent_group=None):
2325 def __init__(self, group_name='', parent_group=None):
2326 self.group_name = group_name
2326 self.group_name = group_name
2327 self.parent_group = parent_group
2327 self.parent_group = parent_group
2328
2328
2329 def __unicode__(self):
2329 def __unicode__(self):
2330 return u"<%s('id:%s:%s')>" % (
2330 return u"<%s('id:%s:%s')>" % (
2331 self.__class__.__name__, self.group_id, self.group_name)
2331 self.__class__.__name__, self.group_id, self.group_name)
2332
2332
2333 @hybrid_property
2333 @hybrid_property
2334 def description_safe(self):
2334 def description_safe(self):
2335 from rhodecode.lib import helpers as h
2335 from rhodecode.lib import helpers as h
2336 return h.escape(self.group_description)
2336 return h.escape(self.group_description)
2337
2337
2338 @classmethod
2338 @classmethod
2339 def _generate_choice(cls, repo_group):
2339 def _generate_choice(cls, repo_group):
2340 from webhelpers.html import literal as _literal
2340 from webhelpers.html import literal as _literal
2341 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2341 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2342 return repo_group.group_id, _name(repo_group.full_path_splitted)
2342 return repo_group.group_id, _name(repo_group.full_path_splitted)
2343
2343
2344 @classmethod
2344 @classmethod
2345 def groups_choices(cls, groups=None, show_empty_group=True):
2345 def groups_choices(cls, groups=None, show_empty_group=True):
2346 if not groups:
2346 if not groups:
2347 groups = cls.query().all()
2347 groups = cls.query().all()
2348
2348
2349 repo_groups = []
2349 repo_groups = []
2350 if show_empty_group:
2350 if show_empty_group:
2351 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2351 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2352
2352
2353 repo_groups.extend([cls._generate_choice(x) for x in groups])
2353 repo_groups.extend([cls._generate_choice(x) for x in groups])
2354
2354
2355 repo_groups = sorted(
2355 repo_groups = sorted(
2356 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2356 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2357 return repo_groups
2357 return repo_groups
2358
2358
2359 @classmethod
2359 @classmethod
2360 def url_sep(cls):
2360 def url_sep(cls):
2361 return URL_SEP
2361 return URL_SEP
2362
2362
2363 @classmethod
2363 @classmethod
2364 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2364 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2365 if case_insensitive:
2365 if case_insensitive:
2366 gr = cls.query().filter(func.lower(cls.group_name)
2366 gr = cls.query().filter(func.lower(cls.group_name)
2367 == func.lower(group_name))
2367 == func.lower(group_name))
2368 else:
2368 else:
2369 gr = cls.query().filter(cls.group_name == group_name)
2369 gr = cls.query().filter(cls.group_name == group_name)
2370 if cache:
2370 if cache:
2371 name_key = _hash_key(group_name)
2371 name_key = _hash_key(group_name)
2372 gr = gr.options(
2372 gr = gr.options(
2373 FromCache("sql_cache_short", "get_group_%s" % name_key))
2373 FromCache("sql_cache_short", "get_group_%s" % name_key))
2374 return gr.scalar()
2374 return gr.scalar()
2375
2375
2376 @classmethod
2376 @classmethod
2377 def get_user_personal_repo_group(cls, user_id):
2377 def get_user_personal_repo_group(cls, user_id):
2378 user = User.get(user_id)
2378 user = User.get(user_id)
2379 if user.username == User.DEFAULT_USER:
2379 if user.username == User.DEFAULT_USER:
2380 return None
2380 return None
2381
2381
2382 return cls.query()\
2382 return cls.query()\
2383 .filter(cls.personal == true()) \
2383 .filter(cls.personal == true()) \
2384 .filter(cls.user == user).scalar()
2384 .filter(cls.user == user).scalar()
2385
2385
2386 @classmethod
2386 @classmethod
2387 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2387 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2388 case_insensitive=True):
2388 case_insensitive=True):
2389 q = RepoGroup.query()
2389 q = RepoGroup.query()
2390
2390
2391 if not isinstance(user_id, Optional):
2391 if not isinstance(user_id, Optional):
2392 q = q.filter(RepoGroup.user_id == user_id)
2392 q = q.filter(RepoGroup.user_id == user_id)
2393
2393
2394 if not isinstance(group_id, Optional):
2394 if not isinstance(group_id, Optional):
2395 q = q.filter(RepoGroup.group_parent_id == group_id)
2395 q = q.filter(RepoGroup.group_parent_id == group_id)
2396
2396
2397 if case_insensitive:
2397 if case_insensitive:
2398 q = q.order_by(func.lower(RepoGroup.group_name))
2398 q = q.order_by(func.lower(RepoGroup.group_name))
2399 else:
2399 else:
2400 q = q.order_by(RepoGroup.group_name)
2400 q = q.order_by(RepoGroup.group_name)
2401 return q.all()
2401 return q.all()
2402
2402
2403 @property
2403 @property
2404 def parents(self):
2404 def parents(self):
2405 parents_recursion_limit = 10
2405 parents_recursion_limit = 10
2406 groups = []
2406 groups = []
2407 if self.parent_group is None:
2407 if self.parent_group is None:
2408 return groups
2408 return groups
2409 cur_gr = self.parent_group
2409 cur_gr = self.parent_group
2410 groups.insert(0, cur_gr)
2410 groups.insert(0, cur_gr)
2411 cnt = 0
2411 cnt = 0
2412 while 1:
2412 while 1:
2413 cnt += 1
2413 cnt += 1
2414 gr = getattr(cur_gr, 'parent_group', None)
2414 gr = getattr(cur_gr, 'parent_group', None)
2415 cur_gr = cur_gr.parent_group
2415 cur_gr = cur_gr.parent_group
2416 if gr is None:
2416 if gr is None:
2417 break
2417 break
2418 if cnt == parents_recursion_limit:
2418 if cnt == parents_recursion_limit:
2419 # this will prevent accidental infinit loops
2419 # this will prevent accidental infinit loops
2420 log.error(('more than %s parents found for group %s, stopping '
2420 log.error(('more than %s parents found for group %s, stopping '
2421 'recursive parent fetching' % (parents_recursion_limit, self)))
2421 'recursive parent fetching' % (parents_recursion_limit, self)))
2422 break
2422 break
2423
2423
2424 groups.insert(0, gr)
2424 groups.insert(0, gr)
2425 return groups
2425 return groups
2426
2426
2427 @property
2427 @property
2428 def last_db_change(self):
2428 def last_db_change(self):
2429 return self.updated_on
2429 return self.updated_on
2430
2430
2431 @property
2431 @property
2432 def children(self):
2432 def children(self):
2433 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2433 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2434
2434
2435 @property
2435 @property
2436 def name(self):
2436 def name(self):
2437 return self.group_name.split(RepoGroup.url_sep())[-1]
2437 return self.group_name.split(RepoGroup.url_sep())[-1]
2438
2438
2439 @property
2439 @property
2440 def full_path(self):
2440 def full_path(self):
2441 return self.group_name
2441 return self.group_name
2442
2442
2443 @property
2443 @property
2444 def full_path_splitted(self):
2444 def full_path_splitted(self):
2445 return self.group_name.split(RepoGroup.url_sep())
2445 return self.group_name.split(RepoGroup.url_sep())
2446
2446
2447 @property
2447 @property
2448 def repositories(self):
2448 def repositories(self):
2449 return Repository.query()\
2449 return Repository.query()\
2450 .filter(Repository.group == self)\
2450 .filter(Repository.group == self)\
2451 .order_by(Repository.repo_name)
2451 .order_by(Repository.repo_name)
2452
2452
2453 @property
2453 @property
2454 def repositories_recursive_count(self):
2454 def repositories_recursive_count(self):
2455 cnt = self.repositories.count()
2455 cnt = self.repositories.count()
2456
2456
2457 def children_count(group):
2457 def children_count(group):
2458 cnt = 0
2458 cnt = 0
2459 for child in group.children:
2459 for child in group.children:
2460 cnt += child.repositories.count()
2460 cnt += child.repositories.count()
2461 cnt += children_count(child)
2461 cnt += children_count(child)
2462 return cnt
2462 return cnt
2463
2463
2464 return cnt + children_count(self)
2464 return cnt + children_count(self)
2465
2465
2466 def _recursive_objects(self, include_repos=True):
2466 def _recursive_objects(self, include_repos=True):
2467 all_ = []
2467 all_ = []
2468
2468
2469 def _get_members(root_gr):
2469 def _get_members(root_gr):
2470 if include_repos:
2470 if include_repos:
2471 for r in root_gr.repositories:
2471 for r in root_gr.repositories:
2472 all_.append(r)
2472 all_.append(r)
2473 childs = root_gr.children.all()
2473 childs = root_gr.children.all()
2474 if childs:
2474 if childs:
2475 for gr in childs:
2475 for gr in childs:
2476 all_.append(gr)
2476 all_.append(gr)
2477 _get_members(gr)
2477 _get_members(gr)
2478
2478
2479 _get_members(self)
2479 _get_members(self)
2480 return [self] + all_
2480 return [self] + all_
2481
2481
2482 def recursive_groups_and_repos(self):
2482 def recursive_groups_and_repos(self):
2483 """
2483 """
2484 Recursive return all groups, with repositories in those groups
2484 Recursive return all groups, with repositories in those groups
2485 """
2485 """
2486 return self._recursive_objects()
2486 return self._recursive_objects()
2487
2487
2488 def recursive_groups(self):
2488 def recursive_groups(self):
2489 """
2489 """
2490 Returns all children groups for this group including children of children
2490 Returns all children groups for this group including children of children
2491 """
2491 """
2492 return self._recursive_objects(include_repos=False)
2492 return self._recursive_objects(include_repos=False)
2493
2493
2494 def get_new_name(self, group_name):
2494 def get_new_name(self, group_name):
2495 """
2495 """
2496 returns new full group name based on parent and new name
2496 returns new full group name based on parent and new name
2497
2497
2498 :param group_name:
2498 :param group_name:
2499 """
2499 """
2500 path_prefix = (self.parent_group.full_path_splitted if
2500 path_prefix = (self.parent_group.full_path_splitted if
2501 self.parent_group else [])
2501 self.parent_group else [])
2502 return RepoGroup.url_sep().join(path_prefix + [group_name])
2502 return RepoGroup.url_sep().join(path_prefix + [group_name])
2503
2503
2504 def permissions(self, with_admins=True, with_owner=True):
2504 def permissions(self, with_admins=True, with_owner=True):
2505 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2505 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2506 q = q.options(joinedload(UserRepoGroupToPerm.group),
2506 q = q.options(joinedload(UserRepoGroupToPerm.group),
2507 joinedload(UserRepoGroupToPerm.user),
2507 joinedload(UserRepoGroupToPerm.user),
2508 joinedload(UserRepoGroupToPerm.permission),)
2508 joinedload(UserRepoGroupToPerm.permission),)
2509
2509
2510 # get owners and admins and permissions. We do a trick of re-writing
2510 # get owners and admins and permissions. We do a trick of re-writing
2511 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2511 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2512 # has a global reference and changing one object propagates to all
2512 # has a global reference and changing one object propagates to all
2513 # others. This means if admin is also an owner admin_row that change
2513 # others. This means if admin is also an owner admin_row that change
2514 # would propagate to both objects
2514 # would propagate to both objects
2515 perm_rows = []
2515 perm_rows = []
2516 for _usr in q.all():
2516 for _usr in q.all():
2517 usr = AttributeDict(_usr.user.get_dict())
2517 usr = AttributeDict(_usr.user.get_dict())
2518 usr.permission = _usr.permission.permission_name
2518 usr.permission = _usr.permission.permission_name
2519 perm_rows.append(usr)
2519 perm_rows.append(usr)
2520
2520
2521 # filter the perm rows by 'default' first and then sort them by
2521 # filter the perm rows by 'default' first and then sort them by
2522 # admin,write,read,none permissions sorted again alphabetically in
2522 # admin,write,read,none permissions sorted again alphabetically in
2523 # each group
2523 # each group
2524 perm_rows = sorted(perm_rows, key=display_user_sort)
2524 perm_rows = sorted(perm_rows, key=display_user_sort)
2525
2525
2526 _admin_perm = 'group.admin'
2526 _admin_perm = 'group.admin'
2527 owner_row = []
2527 owner_row = []
2528 if with_owner:
2528 if with_owner:
2529 usr = AttributeDict(self.user.get_dict())
2529 usr = AttributeDict(self.user.get_dict())
2530 usr.owner_row = True
2530 usr.owner_row = True
2531 usr.permission = _admin_perm
2531 usr.permission = _admin_perm
2532 owner_row.append(usr)
2532 owner_row.append(usr)
2533
2533
2534 super_admin_rows = []
2534 super_admin_rows = []
2535 if with_admins:
2535 if with_admins:
2536 for usr in User.get_all_super_admins():
2536 for usr in User.get_all_super_admins():
2537 # if this admin is also owner, don't double the record
2537 # if this admin is also owner, don't double the record
2538 if usr.user_id == owner_row[0].user_id:
2538 if usr.user_id == owner_row[0].user_id:
2539 owner_row[0].admin_row = True
2539 owner_row[0].admin_row = True
2540 else:
2540 else:
2541 usr = AttributeDict(usr.get_dict())
2541 usr = AttributeDict(usr.get_dict())
2542 usr.admin_row = True
2542 usr.admin_row = True
2543 usr.permission = _admin_perm
2543 usr.permission = _admin_perm
2544 super_admin_rows.append(usr)
2544 super_admin_rows.append(usr)
2545
2545
2546 return super_admin_rows + owner_row + perm_rows
2546 return super_admin_rows + owner_row + perm_rows
2547
2547
2548 def permission_user_groups(self):
2548 def permission_user_groups(self):
2549 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2549 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2550 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2550 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2551 joinedload(UserGroupRepoGroupToPerm.users_group),
2551 joinedload(UserGroupRepoGroupToPerm.users_group),
2552 joinedload(UserGroupRepoGroupToPerm.permission),)
2552 joinedload(UserGroupRepoGroupToPerm.permission),)
2553
2553
2554 perm_rows = []
2554 perm_rows = []
2555 for _user_group in q.all():
2555 for _user_group in q.all():
2556 usr = AttributeDict(_user_group.users_group.get_dict())
2556 usr = AttributeDict(_user_group.users_group.get_dict())
2557 usr.permission = _user_group.permission.permission_name
2557 usr.permission = _user_group.permission.permission_name
2558 perm_rows.append(usr)
2558 perm_rows.append(usr)
2559
2559
2560 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2560 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2561 return perm_rows
2561 return perm_rows
2562
2562
2563 def get_api_data(self):
2563 def get_api_data(self):
2564 """
2564 """
2565 Common function for generating api data
2565 Common function for generating api data
2566
2566
2567 """
2567 """
2568 group = self
2568 group = self
2569 data = {
2569 data = {
2570 'group_id': group.group_id,
2570 'group_id': group.group_id,
2571 'group_name': group.group_name,
2571 'group_name': group.group_name,
2572 'group_description': group.description_safe,
2572 'group_description': group.description_safe,
2573 'parent_group': group.parent_group.group_name if group.parent_group else None,
2573 'parent_group': group.parent_group.group_name if group.parent_group else None,
2574 'repositories': [x.repo_name for x in group.repositories],
2574 'repositories': [x.repo_name for x in group.repositories],
2575 'owner': group.user.username,
2575 'owner': group.user.username,
2576 }
2576 }
2577 return data
2577 return data
2578
2578
2579
2579
2580 class Permission(Base, BaseModel):
2580 class Permission(Base, BaseModel):
2581 __tablename__ = 'permissions'
2581 __tablename__ = 'permissions'
2582 __table_args__ = (
2582 __table_args__ = (
2583 Index('p_perm_name_idx', 'permission_name'),
2583 Index('p_perm_name_idx', 'permission_name'),
2584 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2584 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2585 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2585 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2586 )
2586 )
2587 PERMS = [
2587 PERMS = [
2588 ('hg.admin', _('RhodeCode Super Administrator')),
2588 ('hg.admin', _('RhodeCode Super Administrator')),
2589
2589
2590 ('repository.none', _('Repository no access')),
2590 ('repository.none', _('Repository no access')),
2591 ('repository.read', _('Repository read access')),
2591 ('repository.read', _('Repository read access')),
2592 ('repository.write', _('Repository write access')),
2592 ('repository.write', _('Repository write access')),
2593 ('repository.admin', _('Repository admin access')),
2593 ('repository.admin', _('Repository admin access')),
2594
2594
2595 ('group.none', _('Repository group no access')),
2595 ('group.none', _('Repository group no access')),
2596 ('group.read', _('Repository group read access')),
2596 ('group.read', _('Repository group read access')),
2597 ('group.write', _('Repository group write access')),
2597 ('group.write', _('Repository group write access')),
2598 ('group.admin', _('Repository group admin access')),
2598 ('group.admin', _('Repository group admin access')),
2599
2599
2600 ('usergroup.none', _('User group no access')),
2600 ('usergroup.none', _('User group no access')),
2601 ('usergroup.read', _('User group read access')),
2601 ('usergroup.read', _('User group read access')),
2602 ('usergroup.write', _('User group write access')),
2602 ('usergroup.write', _('User group write access')),
2603 ('usergroup.admin', _('User group admin access')),
2603 ('usergroup.admin', _('User group admin access')),
2604
2604
2605 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2605 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2606 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2606 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2607
2607
2608 ('hg.usergroup.create.false', _('User Group creation disabled')),
2608 ('hg.usergroup.create.false', _('User Group creation disabled')),
2609 ('hg.usergroup.create.true', _('User Group creation enabled')),
2609 ('hg.usergroup.create.true', _('User Group creation enabled')),
2610
2610
2611 ('hg.create.none', _('Repository creation disabled')),
2611 ('hg.create.none', _('Repository creation disabled')),
2612 ('hg.create.repository', _('Repository creation enabled')),
2612 ('hg.create.repository', _('Repository creation enabled')),
2613 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2613 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2614 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2614 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2615
2615
2616 ('hg.fork.none', _('Repository forking disabled')),
2616 ('hg.fork.none', _('Repository forking disabled')),
2617 ('hg.fork.repository', _('Repository forking enabled')),
2617 ('hg.fork.repository', _('Repository forking enabled')),
2618
2618
2619 ('hg.register.none', _('Registration disabled')),
2619 ('hg.register.none', _('Registration disabled')),
2620 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2620 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2621 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2621 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2622
2622
2623 ('hg.password_reset.enabled', _('Password reset enabled')),
2623 ('hg.password_reset.enabled', _('Password reset enabled')),
2624 ('hg.password_reset.hidden', _('Password reset hidden')),
2624 ('hg.password_reset.hidden', _('Password reset hidden')),
2625 ('hg.password_reset.disabled', _('Password reset disabled')),
2625 ('hg.password_reset.disabled', _('Password reset disabled')),
2626
2626
2627 ('hg.extern_activate.manual', _('Manual activation of external account')),
2627 ('hg.extern_activate.manual', _('Manual activation of external account')),
2628 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2628 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2629
2629
2630 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2630 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2631 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2631 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2632 ]
2632 ]
2633
2633
2634 # definition of system default permissions for DEFAULT user
2634 # definition of system default permissions for DEFAULT user
2635 DEFAULT_USER_PERMISSIONS = [
2635 DEFAULT_USER_PERMISSIONS = [
2636 'repository.read',
2636 'repository.read',
2637 'group.read',
2637 'group.read',
2638 'usergroup.read',
2638 'usergroup.read',
2639 'hg.create.repository',
2639 'hg.create.repository',
2640 'hg.repogroup.create.false',
2640 'hg.repogroup.create.false',
2641 'hg.usergroup.create.false',
2641 'hg.usergroup.create.false',
2642 'hg.create.write_on_repogroup.true',
2642 'hg.create.write_on_repogroup.true',
2643 'hg.fork.repository',
2643 'hg.fork.repository',
2644 'hg.register.manual_activate',
2644 'hg.register.manual_activate',
2645 'hg.password_reset.enabled',
2645 'hg.password_reset.enabled',
2646 'hg.extern_activate.auto',
2646 'hg.extern_activate.auto',
2647 'hg.inherit_default_perms.true',
2647 'hg.inherit_default_perms.true',
2648 ]
2648 ]
2649
2649
2650 # defines which permissions are more important higher the more important
2650 # defines which permissions are more important higher the more important
2651 # Weight defines which permissions are more important.
2651 # Weight defines which permissions are more important.
2652 # The higher number the more important.
2652 # The higher number the more important.
2653 PERM_WEIGHTS = {
2653 PERM_WEIGHTS = {
2654 'repository.none': 0,
2654 'repository.none': 0,
2655 'repository.read': 1,
2655 'repository.read': 1,
2656 'repository.write': 3,
2656 'repository.write': 3,
2657 'repository.admin': 4,
2657 'repository.admin': 4,
2658
2658
2659 'group.none': 0,
2659 'group.none': 0,
2660 'group.read': 1,
2660 'group.read': 1,
2661 'group.write': 3,
2661 'group.write': 3,
2662 'group.admin': 4,
2662 'group.admin': 4,
2663
2663
2664 'usergroup.none': 0,
2664 'usergroup.none': 0,
2665 'usergroup.read': 1,
2665 'usergroup.read': 1,
2666 'usergroup.write': 3,
2666 'usergroup.write': 3,
2667 'usergroup.admin': 4,
2667 'usergroup.admin': 4,
2668
2668
2669 'hg.repogroup.create.false': 0,
2669 'hg.repogroup.create.false': 0,
2670 'hg.repogroup.create.true': 1,
2670 'hg.repogroup.create.true': 1,
2671
2671
2672 'hg.usergroup.create.false': 0,
2672 'hg.usergroup.create.false': 0,
2673 'hg.usergroup.create.true': 1,
2673 'hg.usergroup.create.true': 1,
2674
2674
2675 'hg.fork.none': 0,
2675 'hg.fork.none': 0,
2676 'hg.fork.repository': 1,
2676 'hg.fork.repository': 1,
2677 'hg.create.none': 0,
2677 'hg.create.none': 0,
2678 'hg.create.repository': 1
2678 'hg.create.repository': 1
2679 }
2679 }
2680
2680
2681 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2681 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2682 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2682 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2683 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2683 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2684
2684
2685 def __unicode__(self):
2685 def __unicode__(self):
2686 return u"<%s('%s:%s')>" % (
2686 return u"<%s('%s:%s')>" % (
2687 self.__class__.__name__, self.permission_id, self.permission_name
2687 self.__class__.__name__, self.permission_id, self.permission_name
2688 )
2688 )
2689
2689
2690 @classmethod
2690 @classmethod
2691 def get_by_key(cls, key):
2691 def get_by_key(cls, key):
2692 return cls.query().filter(cls.permission_name == key).scalar()
2692 return cls.query().filter(cls.permission_name == key).scalar()
2693
2693
2694 @classmethod
2694 @classmethod
2695 def get_default_repo_perms(cls, user_id, repo_id=None):
2695 def get_default_repo_perms(cls, user_id, repo_id=None):
2696 q = Session().query(UserRepoToPerm, Repository, Permission)\
2696 q = Session().query(UserRepoToPerm, Repository, Permission)\
2697 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2697 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2698 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2698 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2699 .filter(UserRepoToPerm.user_id == user_id)
2699 .filter(UserRepoToPerm.user_id == user_id)
2700 if repo_id:
2700 if repo_id:
2701 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2701 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2702 return q.all()
2702 return q.all()
2703
2703
2704 @classmethod
2704 @classmethod
2705 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2705 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2706 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2706 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2707 .join(
2707 .join(
2708 Permission,
2708 Permission,
2709 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2709 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2710 .join(
2710 .join(
2711 Repository,
2711 Repository,
2712 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2712 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2713 .join(
2713 .join(
2714 UserGroup,
2714 UserGroup,
2715 UserGroupRepoToPerm.users_group_id ==
2715 UserGroupRepoToPerm.users_group_id ==
2716 UserGroup.users_group_id)\
2716 UserGroup.users_group_id)\
2717 .join(
2717 .join(
2718 UserGroupMember,
2718 UserGroupMember,
2719 UserGroupRepoToPerm.users_group_id ==
2719 UserGroupRepoToPerm.users_group_id ==
2720 UserGroupMember.users_group_id)\
2720 UserGroupMember.users_group_id)\
2721 .filter(
2721 .filter(
2722 UserGroupMember.user_id == user_id,
2722 UserGroupMember.user_id == user_id,
2723 UserGroup.users_group_active == true())
2723 UserGroup.users_group_active == true())
2724 if repo_id:
2724 if repo_id:
2725 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2725 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2726 return q.all()
2726 return q.all()
2727
2727
2728 @classmethod
2728 @classmethod
2729 def get_default_group_perms(cls, user_id, repo_group_id=None):
2729 def get_default_group_perms(cls, user_id, repo_group_id=None):
2730 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2730 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2731 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2731 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2732 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2732 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2733 .filter(UserRepoGroupToPerm.user_id == user_id)
2733 .filter(UserRepoGroupToPerm.user_id == user_id)
2734 if repo_group_id:
2734 if repo_group_id:
2735 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2735 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2736 return q.all()
2736 return q.all()
2737
2737
2738 @classmethod
2738 @classmethod
2739 def get_default_group_perms_from_user_group(
2739 def get_default_group_perms_from_user_group(
2740 cls, user_id, repo_group_id=None):
2740 cls, user_id, repo_group_id=None):
2741 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2741 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2742 .join(
2742 .join(
2743 Permission,
2743 Permission,
2744 UserGroupRepoGroupToPerm.permission_id ==
2744 UserGroupRepoGroupToPerm.permission_id ==
2745 Permission.permission_id)\
2745 Permission.permission_id)\
2746 .join(
2746 .join(
2747 RepoGroup,
2747 RepoGroup,
2748 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2748 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2749 .join(
2749 .join(
2750 UserGroup,
2750 UserGroup,
2751 UserGroupRepoGroupToPerm.users_group_id ==
2751 UserGroupRepoGroupToPerm.users_group_id ==
2752 UserGroup.users_group_id)\
2752 UserGroup.users_group_id)\
2753 .join(
2753 .join(
2754 UserGroupMember,
2754 UserGroupMember,
2755 UserGroupRepoGroupToPerm.users_group_id ==
2755 UserGroupRepoGroupToPerm.users_group_id ==
2756 UserGroupMember.users_group_id)\
2756 UserGroupMember.users_group_id)\
2757 .filter(
2757 .filter(
2758 UserGroupMember.user_id == user_id,
2758 UserGroupMember.user_id == user_id,
2759 UserGroup.users_group_active == true())
2759 UserGroup.users_group_active == true())
2760 if repo_group_id:
2760 if repo_group_id:
2761 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2761 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2762 return q.all()
2762 return q.all()
2763
2763
2764 @classmethod
2764 @classmethod
2765 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2765 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2766 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2766 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2767 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2767 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2769 .filter(UserUserGroupToPerm.user_id == user_id)
2769 .filter(UserUserGroupToPerm.user_id == user_id)
2770 if user_group_id:
2770 if user_group_id:
2771 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2771 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2772 return q.all()
2772 return q.all()
2773
2773
2774 @classmethod
2774 @classmethod
2775 def get_default_user_group_perms_from_user_group(
2775 def get_default_user_group_perms_from_user_group(
2776 cls, user_id, user_group_id=None):
2776 cls, user_id, user_group_id=None):
2777 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2777 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2778 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2778 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2779 .join(
2779 .join(
2780 Permission,
2780 Permission,
2781 UserGroupUserGroupToPerm.permission_id ==
2781 UserGroupUserGroupToPerm.permission_id ==
2782 Permission.permission_id)\
2782 Permission.permission_id)\
2783 .join(
2783 .join(
2784 TargetUserGroup,
2784 TargetUserGroup,
2785 UserGroupUserGroupToPerm.target_user_group_id ==
2785 UserGroupUserGroupToPerm.target_user_group_id ==
2786 TargetUserGroup.users_group_id)\
2786 TargetUserGroup.users_group_id)\
2787 .join(
2787 .join(
2788 UserGroup,
2788 UserGroup,
2789 UserGroupUserGroupToPerm.user_group_id ==
2789 UserGroupUserGroupToPerm.user_group_id ==
2790 UserGroup.users_group_id)\
2790 UserGroup.users_group_id)\
2791 .join(
2791 .join(
2792 UserGroupMember,
2792 UserGroupMember,
2793 UserGroupUserGroupToPerm.user_group_id ==
2793 UserGroupUserGroupToPerm.user_group_id ==
2794 UserGroupMember.users_group_id)\
2794 UserGroupMember.users_group_id)\
2795 .filter(
2795 .filter(
2796 UserGroupMember.user_id == user_id,
2796 UserGroupMember.user_id == user_id,
2797 UserGroup.users_group_active == true())
2797 UserGroup.users_group_active == true())
2798 if user_group_id:
2798 if user_group_id:
2799 q = q.filter(
2799 q = q.filter(
2800 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2800 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2801
2801
2802 return q.all()
2802 return q.all()
2803
2803
2804
2804
2805 class UserRepoToPerm(Base, BaseModel):
2805 class UserRepoToPerm(Base, BaseModel):
2806 __tablename__ = 'repo_to_perm'
2806 __tablename__ = 'repo_to_perm'
2807 __table_args__ = (
2807 __table_args__ = (
2808 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2808 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2809 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2809 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2810 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2810 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2811 )
2811 )
2812 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2812 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2813 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2813 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2814 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2814 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2815 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2815 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2816
2816
2817 user = relationship('User')
2817 user = relationship('User')
2818 repository = relationship('Repository')
2818 repository = relationship('Repository')
2819 permission = relationship('Permission')
2819 permission = relationship('Permission')
2820
2820
2821 @classmethod
2821 @classmethod
2822 def create(cls, user, repository, permission):
2822 def create(cls, user, repository, permission):
2823 n = cls()
2823 n = cls()
2824 n.user = user
2824 n.user = user
2825 n.repository = repository
2825 n.repository = repository
2826 n.permission = permission
2826 n.permission = permission
2827 Session().add(n)
2827 Session().add(n)
2828 return n
2828 return n
2829
2829
2830 def __unicode__(self):
2830 def __unicode__(self):
2831 return u'<%s => %s >' % (self.user, self.repository)
2831 return u'<%s => %s >' % (self.user, self.repository)
2832
2832
2833
2833
2834 class UserUserGroupToPerm(Base, BaseModel):
2834 class UserUserGroupToPerm(Base, BaseModel):
2835 __tablename__ = 'user_user_group_to_perm'
2835 __tablename__ = 'user_user_group_to_perm'
2836 __table_args__ = (
2836 __table_args__ = (
2837 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2837 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2838 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2838 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2839 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2839 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2840 )
2840 )
2841 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2841 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2842 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2842 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2843 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2843 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2844 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2844 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2845
2845
2846 user = relationship('User')
2846 user = relationship('User')
2847 user_group = relationship('UserGroup')
2847 user_group = relationship('UserGroup')
2848 permission = relationship('Permission')
2848 permission = relationship('Permission')
2849
2849
2850 @classmethod
2850 @classmethod
2851 def create(cls, user, user_group, permission):
2851 def create(cls, user, user_group, permission):
2852 n = cls()
2852 n = cls()
2853 n.user = user
2853 n.user = user
2854 n.user_group = user_group
2854 n.user_group = user_group
2855 n.permission = permission
2855 n.permission = permission
2856 Session().add(n)
2856 Session().add(n)
2857 return n
2857 return n
2858
2858
2859 def __unicode__(self):
2859 def __unicode__(self):
2860 return u'<%s => %s >' % (self.user, self.user_group)
2860 return u'<%s => %s >' % (self.user, self.user_group)
2861
2861
2862
2862
2863 class UserToPerm(Base, BaseModel):
2863 class UserToPerm(Base, BaseModel):
2864 __tablename__ = 'user_to_perm'
2864 __tablename__ = 'user_to_perm'
2865 __table_args__ = (
2865 __table_args__ = (
2866 UniqueConstraint('user_id', 'permission_id'),
2866 UniqueConstraint('user_id', 'permission_id'),
2867 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2867 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2868 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2868 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2869 )
2869 )
2870 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2870 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2871 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2871 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2872 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2872 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2873
2873
2874 user = relationship('User')
2874 user = relationship('User')
2875 permission = relationship('Permission', lazy='joined')
2875 permission = relationship('Permission', lazy='joined')
2876
2876
2877 def __unicode__(self):
2877 def __unicode__(self):
2878 return u'<%s => %s >' % (self.user, self.permission)
2878 return u'<%s => %s >' % (self.user, self.permission)
2879
2879
2880
2880
2881 class UserGroupRepoToPerm(Base, BaseModel):
2881 class UserGroupRepoToPerm(Base, BaseModel):
2882 __tablename__ = 'users_group_repo_to_perm'
2882 __tablename__ = 'users_group_repo_to_perm'
2883 __table_args__ = (
2883 __table_args__ = (
2884 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2884 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2885 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2885 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2886 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2886 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2887 )
2887 )
2888 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2888 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2889 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2889 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2890 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2890 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2891 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2891 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2892
2892
2893 users_group = relationship('UserGroup')
2893 users_group = relationship('UserGroup')
2894 permission = relationship('Permission')
2894 permission = relationship('Permission')
2895 repository = relationship('Repository')
2895 repository = relationship('Repository')
2896
2896
2897 @classmethod
2897 @classmethod
2898 def create(cls, users_group, repository, permission):
2898 def create(cls, users_group, repository, permission):
2899 n = cls()
2899 n = cls()
2900 n.users_group = users_group
2900 n.users_group = users_group
2901 n.repository = repository
2901 n.repository = repository
2902 n.permission = permission
2902 n.permission = permission
2903 Session().add(n)
2903 Session().add(n)
2904 return n
2904 return n
2905
2905
2906 def __unicode__(self):
2906 def __unicode__(self):
2907 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2907 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2908
2908
2909
2909
2910 class UserGroupUserGroupToPerm(Base, BaseModel):
2910 class UserGroupUserGroupToPerm(Base, BaseModel):
2911 __tablename__ = 'user_group_user_group_to_perm'
2911 __tablename__ = 'user_group_user_group_to_perm'
2912 __table_args__ = (
2912 __table_args__ = (
2913 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2913 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2914 CheckConstraint('target_user_group_id != user_group_id'),
2914 CheckConstraint('target_user_group_id != user_group_id'),
2915 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2915 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2916 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2916 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2917 )
2917 )
2918 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)
2918 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)
2919 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2919 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2920 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2920 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2921 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2921 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2922
2922
2923 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2923 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2924 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2924 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2925 permission = relationship('Permission')
2925 permission = relationship('Permission')
2926
2926
2927 @classmethod
2927 @classmethod
2928 def create(cls, target_user_group, user_group, permission):
2928 def create(cls, target_user_group, user_group, permission):
2929 n = cls()
2929 n = cls()
2930 n.target_user_group = target_user_group
2930 n.target_user_group = target_user_group
2931 n.user_group = user_group
2931 n.user_group = user_group
2932 n.permission = permission
2932 n.permission = permission
2933 Session().add(n)
2933 Session().add(n)
2934 return n
2934 return n
2935
2935
2936 def __unicode__(self):
2936 def __unicode__(self):
2937 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2937 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2938
2938
2939
2939
2940 class UserGroupToPerm(Base, BaseModel):
2940 class UserGroupToPerm(Base, BaseModel):
2941 __tablename__ = 'users_group_to_perm'
2941 __tablename__ = 'users_group_to_perm'
2942 __table_args__ = (
2942 __table_args__ = (
2943 UniqueConstraint('users_group_id', 'permission_id',),
2943 UniqueConstraint('users_group_id', 'permission_id',),
2944 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2944 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2945 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2945 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2946 )
2946 )
2947 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2947 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2948 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2948 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2949 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2949 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2950
2950
2951 users_group = relationship('UserGroup')
2951 users_group = relationship('UserGroup')
2952 permission = relationship('Permission')
2952 permission = relationship('Permission')
2953
2953
2954
2954
2955 class UserRepoGroupToPerm(Base, BaseModel):
2955 class UserRepoGroupToPerm(Base, BaseModel):
2956 __tablename__ = 'user_repo_group_to_perm'
2956 __tablename__ = 'user_repo_group_to_perm'
2957 __table_args__ = (
2957 __table_args__ = (
2958 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2958 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2961 )
2961 )
2962
2962
2963 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2963 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2964 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2964 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2965 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2965 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2966 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2966 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2967
2967
2968 user = relationship('User')
2968 user = relationship('User')
2969 group = relationship('RepoGroup')
2969 group = relationship('RepoGroup')
2970 permission = relationship('Permission')
2970 permission = relationship('Permission')
2971
2971
2972 @classmethod
2972 @classmethod
2973 def create(cls, user, repository_group, permission):
2973 def create(cls, user, repository_group, permission):
2974 n = cls()
2974 n = cls()
2975 n.user = user
2975 n.user = user
2976 n.group = repository_group
2976 n.group = repository_group
2977 n.permission = permission
2977 n.permission = permission
2978 Session().add(n)
2978 Session().add(n)
2979 return n
2979 return n
2980
2980
2981
2981
2982 class UserGroupRepoGroupToPerm(Base, BaseModel):
2982 class UserGroupRepoGroupToPerm(Base, BaseModel):
2983 __tablename__ = 'users_group_repo_group_to_perm'
2983 __tablename__ = 'users_group_repo_group_to_perm'
2984 __table_args__ = (
2984 __table_args__ = (
2985 UniqueConstraint('users_group_id', 'group_id'),
2985 UniqueConstraint('users_group_id', 'group_id'),
2986 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2986 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2987 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2987 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2988 )
2988 )
2989
2989
2990 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)
2990 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)
2991 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2991 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2992 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2992 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2993 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2993 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2994
2994
2995 users_group = relationship('UserGroup')
2995 users_group = relationship('UserGroup')
2996 permission = relationship('Permission')
2996 permission = relationship('Permission')
2997 group = relationship('RepoGroup')
2997 group = relationship('RepoGroup')
2998
2998
2999 @classmethod
2999 @classmethod
3000 def create(cls, user_group, repository_group, permission):
3000 def create(cls, user_group, repository_group, permission):
3001 n = cls()
3001 n = cls()
3002 n.users_group = user_group
3002 n.users_group = user_group
3003 n.group = repository_group
3003 n.group = repository_group
3004 n.permission = permission
3004 n.permission = permission
3005 Session().add(n)
3005 Session().add(n)
3006 return n
3006 return n
3007
3007
3008 def __unicode__(self):
3008 def __unicode__(self):
3009 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3009 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3010
3010
3011
3011
3012 class Statistics(Base, BaseModel):
3012 class Statistics(Base, BaseModel):
3013 __tablename__ = 'statistics'
3013 __tablename__ = 'statistics'
3014 __table_args__ = (
3014 __table_args__ = (
3015 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3015 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3016 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3016 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3017 )
3017 )
3018 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3018 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3019 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3019 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3020 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3020 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3021 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3021 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3022 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3022 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3023 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3023 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3024
3024
3025 repository = relationship('Repository', single_parent=True)
3025 repository = relationship('Repository', single_parent=True)
3026
3026
3027
3027
3028 class UserFollowing(Base, BaseModel):
3028 class UserFollowing(Base, BaseModel):
3029 __tablename__ = 'user_followings'
3029 __tablename__ = 'user_followings'
3030 __table_args__ = (
3030 __table_args__ = (
3031 UniqueConstraint('user_id', 'follows_repository_id'),
3031 UniqueConstraint('user_id', 'follows_repository_id'),
3032 UniqueConstraint('user_id', 'follows_user_id'),
3032 UniqueConstraint('user_id', 'follows_user_id'),
3033 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3033 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3034 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3034 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3035 )
3035 )
3036
3036
3037 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3037 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3039 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3039 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3040 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3040 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3041 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3041 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3042
3042
3043 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3043 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3044
3044
3045 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3045 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3046 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3046 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3047
3047
3048 @classmethod
3048 @classmethod
3049 def get_repo_followers(cls, repo_id):
3049 def get_repo_followers(cls, repo_id):
3050 return cls.query().filter(cls.follows_repo_id == repo_id)
3050 return cls.query().filter(cls.follows_repo_id == repo_id)
3051
3051
3052
3052
3053 class CacheKey(Base, BaseModel):
3053 class CacheKey(Base, BaseModel):
3054 __tablename__ = 'cache_invalidation'
3054 __tablename__ = 'cache_invalidation'
3055 __table_args__ = (
3055 __table_args__ = (
3056 UniqueConstraint('cache_key'),
3056 UniqueConstraint('cache_key'),
3057 Index('key_idx', 'cache_key'),
3057 Index('key_idx', 'cache_key'),
3058 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3058 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3059 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3059 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3060 )
3060 )
3061 CACHE_TYPE_ATOM = 'ATOM'
3061 CACHE_TYPE_ATOM = 'ATOM'
3062 CACHE_TYPE_RSS = 'RSS'
3062 CACHE_TYPE_RSS = 'RSS'
3063 CACHE_TYPE_README = 'README'
3063 CACHE_TYPE_README = 'README'
3064
3064
3065 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3065 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3066 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3066 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3067 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3067 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3068 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3068 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3069
3069
3070 def __init__(self, cache_key, cache_args=''):
3070 def __init__(self, cache_key, cache_args=''):
3071 self.cache_key = cache_key
3071 self.cache_key = cache_key
3072 self.cache_args = cache_args
3072 self.cache_args = cache_args
3073 self.cache_active = False
3073 self.cache_active = False
3074
3074
3075 def __unicode__(self):
3075 def __unicode__(self):
3076 return u"<%s('%s:%s[%s]')>" % (
3076 return u"<%s('%s:%s[%s]')>" % (
3077 self.__class__.__name__,
3077 self.__class__.__name__,
3078 self.cache_id, self.cache_key, self.cache_active)
3078 self.cache_id, self.cache_key, self.cache_active)
3079
3079
3080 def _cache_key_partition(self):
3080 def _cache_key_partition(self):
3081 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3081 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3082 return prefix, repo_name, suffix
3082 return prefix, repo_name, suffix
3083
3083
3084 def get_prefix(self):
3084 def get_prefix(self):
3085 """
3085 """
3086 Try to extract prefix from existing cache key. The key could consist
3086 Try to extract prefix from existing cache key. The key could consist
3087 of prefix, repo_name, suffix
3087 of prefix, repo_name, suffix
3088 """
3088 """
3089 # this returns prefix, repo_name, suffix
3089 # this returns prefix, repo_name, suffix
3090 return self._cache_key_partition()[0]
3090 return self._cache_key_partition()[0]
3091
3091
3092 def get_suffix(self):
3092 def get_suffix(self):
3093 """
3093 """
3094 get suffix that might have been used in _get_cache_key to
3094 get suffix that might have been used in _get_cache_key to
3095 generate self.cache_key. Only used for informational purposes
3095 generate self.cache_key. Only used for informational purposes
3096 in repo_edit.mako.
3096 in repo_edit.mako.
3097 """
3097 """
3098 # prefix, repo_name, suffix
3098 # prefix, repo_name, suffix
3099 return self._cache_key_partition()[2]
3099 return self._cache_key_partition()[2]
3100
3100
3101 @classmethod
3101 @classmethod
3102 def delete_all_cache(cls):
3102 def delete_all_cache(cls):
3103 """
3103 """
3104 Delete all cache keys from database.
3104 Delete all cache keys from database.
3105 Should only be run when all instances are down and all entries
3105 Should only be run when all instances are down and all entries
3106 thus stale.
3106 thus stale.
3107 """
3107 """
3108 cls.query().delete()
3108 cls.query().delete()
3109 Session().commit()
3109 Session().commit()
3110
3110
3111 @classmethod
3111 @classmethod
3112 def get_cache_key(cls, repo_name, cache_type):
3112 def get_cache_key(cls, repo_name, cache_type):
3113 """
3113 """
3114
3114
3115 Generate a cache key for this process of RhodeCode instance.
3115 Generate a cache key for this process of RhodeCode instance.
3116 Prefix most likely will be process id or maybe explicitly set
3116 Prefix most likely will be process id or maybe explicitly set
3117 instance_id from .ini file.
3117 instance_id from .ini file.
3118 """
3118 """
3119 import rhodecode
3119 import rhodecode
3120 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3120 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3121
3121
3122 repo_as_unicode = safe_unicode(repo_name)
3122 repo_as_unicode = safe_unicode(repo_name)
3123 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3123 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3124 if cache_type else repo_as_unicode
3124 if cache_type else repo_as_unicode
3125
3125
3126 return u'{}{}'.format(prefix, key)
3126 return u'{}{}'.format(prefix, key)
3127
3127
3128 @classmethod
3128 @classmethod
3129 def set_invalidate(cls, repo_name, delete=False):
3129 def set_invalidate(cls, repo_name, delete=False):
3130 """
3130 """
3131 Mark all caches of a repo as invalid in the database.
3131 Mark all caches of a repo as invalid in the database.
3132 """
3132 """
3133
3133
3134 try:
3134 try:
3135 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3135 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3136 if delete:
3136 if delete:
3137 log.debug('cache objects deleted for repo %s',
3137 log.debug('cache objects deleted for repo %s',
3138 safe_str(repo_name))
3138 safe_str(repo_name))
3139 qry.delete()
3139 qry.delete()
3140 else:
3140 else:
3141 log.debug('cache objects marked as invalid for repo %s',
3141 log.debug('cache objects marked as invalid for repo %s',
3142 safe_str(repo_name))
3142 safe_str(repo_name))
3143 qry.update({"cache_active": False})
3143 qry.update({"cache_active": False})
3144
3144
3145 Session().commit()
3145 Session().commit()
3146 except Exception:
3146 except Exception:
3147 log.exception(
3147 log.exception(
3148 'Cache key invalidation failed for repository %s',
3148 'Cache key invalidation failed for repository %s',
3149 safe_str(repo_name))
3149 safe_str(repo_name))
3150 Session().rollback()
3150 Session().rollback()
3151
3151
3152 @classmethod
3152 @classmethod
3153 def get_active_cache(cls, cache_key):
3153 def get_active_cache(cls, cache_key):
3154 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3154 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3155 if inv_obj:
3155 if inv_obj:
3156 return inv_obj
3156 return inv_obj
3157 return None
3157 return None
3158
3158
3159 @classmethod
3159 @classmethod
3160 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3160 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3161 thread_scoped=False):
3161 thread_scoped=False):
3162 """
3162 """
3163 @cache_region('long_term')
3163 @cache_region('long_term')
3164 def _heavy_calculation(cache_key):
3164 def _heavy_calculation(cache_key):
3165 return 'result'
3165 return 'result'
3166
3166
3167 cache_context = CacheKey.repo_context_cache(
3167 cache_context = CacheKey.repo_context_cache(
3168 _heavy_calculation, repo_name, cache_type)
3168 _heavy_calculation, repo_name, cache_type)
3169
3169
3170 with cache_context as context:
3170 with cache_context as context:
3171 context.invalidate()
3171 context.invalidate()
3172 computed = context.compute()
3172 computed = context.compute()
3173
3173
3174 assert computed == 'result'
3174 assert computed == 'result'
3175 """
3175 """
3176 from rhodecode.lib import caches
3176 from rhodecode.lib import caches
3177 return caches.InvalidationContext(
3177 return caches.InvalidationContext(
3178 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3178 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3179
3179
3180
3180
3181 class ChangesetComment(Base, BaseModel):
3181 class ChangesetComment(Base, BaseModel):
3182 __tablename__ = 'changeset_comments'
3182 __tablename__ = 'changeset_comments'
3183 __table_args__ = (
3183 __table_args__ = (
3184 Index('cc_revision_idx', 'revision'),
3184 Index('cc_revision_idx', 'revision'),
3185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3187 )
3187 )
3188
3188
3189 COMMENT_OUTDATED = u'comment_outdated'
3189 COMMENT_OUTDATED = u'comment_outdated'
3190 COMMENT_TYPE_NOTE = u'note'
3190 COMMENT_TYPE_NOTE = u'note'
3191 COMMENT_TYPE_TODO = u'todo'
3191 COMMENT_TYPE_TODO = u'todo'
3192 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3192 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3193
3193
3194 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3194 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3195 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3195 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3196 revision = Column('revision', String(40), nullable=True)
3196 revision = Column('revision', String(40), nullable=True)
3197 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3197 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3198 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3198 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3199 line_no = Column('line_no', Unicode(10), nullable=True)
3199 line_no = Column('line_no', Unicode(10), nullable=True)
3200 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3200 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3201 f_path = Column('f_path', Unicode(1000), nullable=True)
3201 f_path = Column('f_path', Unicode(1000), nullable=True)
3202 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3202 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3203 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3203 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3204 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3204 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3205 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3205 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3206 renderer = Column('renderer', Unicode(64), nullable=True)
3206 renderer = Column('renderer', Unicode(64), nullable=True)
3207 display_state = Column('display_state', Unicode(128), nullable=True)
3207 display_state = Column('display_state', Unicode(128), nullable=True)
3208
3208
3209 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3209 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3210 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3210 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3211 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3211 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3212 author = relationship('User', lazy='joined')
3212 author = relationship('User', lazy='joined')
3213 repo = relationship('Repository')
3213 repo = relationship('Repository')
3214 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3214 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3215 pull_request = relationship('PullRequest', lazy='joined')
3215 pull_request = relationship('PullRequest', lazy='joined')
3216 pull_request_version = relationship('PullRequestVersion')
3216 pull_request_version = relationship('PullRequestVersion')
3217
3217
3218 @classmethod
3218 @classmethod
3219 def get_users(cls, revision=None, pull_request_id=None):
3219 def get_users(cls, revision=None, pull_request_id=None):
3220 """
3220 """
3221 Returns user associated with this ChangesetComment. ie those
3221 Returns user associated with this ChangesetComment. ie those
3222 who actually commented
3222 who actually commented
3223
3223
3224 :param cls:
3224 :param cls:
3225 :param revision:
3225 :param revision:
3226 """
3226 """
3227 q = Session().query(User)\
3227 q = Session().query(User)\
3228 .join(ChangesetComment.author)
3228 .join(ChangesetComment.author)
3229 if revision:
3229 if revision:
3230 q = q.filter(cls.revision == revision)
3230 q = q.filter(cls.revision == revision)
3231 elif pull_request_id:
3231 elif pull_request_id:
3232 q = q.filter(cls.pull_request_id == pull_request_id)
3232 q = q.filter(cls.pull_request_id == pull_request_id)
3233 return q.all()
3233 return q.all()
3234
3234
3235 @classmethod
3235 @classmethod
3236 def get_index_from_version(cls, pr_version, versions):
3236 def get_index_from_version(cls, pr_version, versions):
3237 num_versions = [x.pull_request_version_id for x in versions]
3237 num_versions = [x.pull_request_version_id for x in versions]
3238 try:
3238 try:
3239 return num_versions.index(pr_version) +1
3239 return num_versions.index(pr_version) +1
3240 except (IndexError, ValueError):
3240 except (IndexError, ValueError):
3241 return
3241 return
3242
3242
3243 @property
3243 @property
3244 def outdated(self):
3244 def outdated(self):
3245 return self.display_state == self.COMMENT_OUTDATED
3245 return self.display_state == self.COMMENT_OUTDATED
3246
3246
3247 def outdated_at_version(self, version):
3247 def outdated_at_version(self, version):
3248 """
3248 """
3249 Checks if comment is outdated for given pull request version
3249 Checks if comment is outdated for given pull request version
3250 """
3250 """
3251 return self.outdated and self.pull_request_version_id != version
3251 return self.outdated and self.pull_request_version_id != version
3252
3252
3253 def older_than_version(self, version):
3253 def older_than_version(self, version):
3254 """
3254 """
3255 Checks if comment is made from previous version than given
3255 Checks if comment is made from previous version than given
3256 """
3256 """
3257 if version is None:
3257 if version is None:
3258 return self.pull_request_version_id is not None
3258 return self.pull_request_version_id is not None
3259
3259
3260 return self.pull_request_version_id < version
3260 return self.pull_request_version_id < version
3261
3261
3262 @property
3262 @property
3263 def resolved(self):
3263 def resolved(self):
3264 return self.resolved_by[0] if self.resolved_by else None
3264 return self.resolved_by[0] if self.resolved_by else None
3265
3265
3266 @property
3266 @property
3267 def is_todo(self):
3267 def is_todo(self):
3268 return self.comment_type == self.COMMENT_TYPE_TODO
3268 return self.comment_type == self.COMMENT_TYPE_TODO
3269
3269
3270 @property
3270 @property
3271 def is_inline(self):
3271 def is_inline(self):
3272 return self.line_no and self.f_path
3272 return self.line_no and self.f_path
3273
3273
3274 def get_index_version(self, versions):
3274 def get_index_version(self, versions):
3275 return self.get_index_from_version(
3275 return self.get_index_from_version(
3276 self.pull_request_version_id, versions)
3276 self.pull_request_version_id, versions)
3277
3277
3278 def __repr__(self):
3278 def __repr__(self):
3279 if self.comment_id:
3279 if self.comment_id:
3280 return '<DB:Comment #%s>' % self.comment_id
3280 return '<DB:Comment #%s>' % self.comment_id
3281 else:
3281 else:
3282 return '<DB:Comment at %#x>' % id(self)
3282 return '<DB:Comment at %#x>' % id(self)
3283
3283
3284 def get_api_data(self):
3284 def get_api_data(self):
3285 comment = self
3285 comment = self
3286 data = {
3286 data = {
3287 'comment_id': comment.comment_id,
3287 'comment_id': comment.comment_id,
3288 'comment_type': comment.comment_type,
3288 'comment_type': comment.comment_type,
3289 'comment_text': comment.text,
3289 'comment_text': comment.text,
3290 'comment_status': comment.status_change,
3290 'comment_status': comment.status_change,
3291 'comment_f_path': comment.f_path,
3291 'comment_f_path': comment.f_path,
3292 'comment_lineno': comment.line_no,
3292 'comment_lineno': comment.line_no,
3293 'comment_author': comment.author,
3293 'comment_author': comment.author,
3294 'comment_created_on': comment.created_on
3294 'comment_created_on': comment.created_on
3295 }
3295 }
3296 return data
3296 return data
3297
3297
3298 def __json__(self):
3298 def __json__(self):
3299 data = dict()
3299 data = dict()
3300 data.update(self.get_api_data())
3300 data.update(self.get_api_data())
3301 return data
3301 return data
3302
3302
3303
3303
3304 class ChangesetStatus(Base, BaseModel):
3304 class ChangesetStatus(Base, BaseModel):
3305 __tablename__ = 'changeset_statuses'
3305 __tablename__ = 'changeset_statuses'
3306 __table_args__ = (
3306 __table_args__ = (
3307 Index('cs_revision_idx', 'revision'),
3307 Index('cs_revision_idx', 'revision'),
3308 Index('cs_version_idx', 'version'),
3308 Index('cs_version_idx', 'version'),
3309 UniqueConstraint('repo_id', 'revision', 'version'),
3309 UniqueConstraint('repo_id', 'revision', 'version'),
3310 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3310 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3311 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3311 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3312 )
3312 )
3313 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3313 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3314 STATUS_APPROVED = 'approved'
3314 STATUS_APPROVED = 'approved'
3315 STATUS_REJECTED = 'rejected'
3315 STATUS_REJECTED = 'rejected'
3316 STATUS_UNDER_REVIEW = 'under_review'
3316 STATUS_UNDER_REVIEW = 'under_review'
3317
3317
3318 STATUSES = [
3318 STATUSES = [
3319 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3319 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3320 (STATUS_APPROVED, _("Approved")),
3320 (STATUS_APPROVED, _("Approved")),
3321 (STATUS_REJECTED, _("Rejected")),
3321 (STATUS_REJECTED, _("Rejected")),
3322 (STATUS_UNDER_REVIEW, _("Under Review")),
3322 (STATUS_UNDER_REVIEW, _("Under Review")),
3323 ]
3323 ]
3324
3324
3325 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3325 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3326 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3326 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3327 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3327 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3328 revision = Column('revision', String(40), nullable=False)
3328 revision = Column('revision', String(40), nullable=False)
3329 status = Column('status', String(128), nullable=False, default=DEFAULT)
3329 status = Column('status', String(128), nullable=False, default=DEFAULT)
3330 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3330 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3331 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3331 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3332 version = Column('version', Integer(), nullable=False, default=0)
3332 version = Column('version', Integer(), nullable=False, default=0)
3333 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3333 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3334
3334
3335 author = relationship('User', lazy='joined')
3335 author = relationship('User', lazy='joined')
3336 repo = relationship('Repository')
3336 repo = relationship('Repository')
3337 comment = relationship('ChangesetComment', lazy='joined')
3337 comment = relationship('ChangesetComment', lazy='joined')
3338 pull_request = relationship('PullRequest', lazy='joined')
3338 pull_request = relationship('PullRequest', lazy='joined')
3339
3339
3340 def __unicode__(self):
3340 def __unicode__(self):
3341 return u"<%s('%s[v%s]:%s')>" % (
3341 return u"<%s('%s[v%s]:%s')>" % (
3342 self.__class__.__name__,
3342 self.__class__.__name__,
3343 self.status, self.version, self.author
3343 self.status, self.version, self.author
3344 )
3344 )
3345
3345
3346 @classmethod
3346 @classmethod
3347 def get_status_lbl(cls, value):
3347 def get_status_lbl(cls, value):
3348 return dict(cls.STATUSES).get(value)
3348 return dict(cls.STATUSES).get(value)
3349
3349
3350 @property
3350 @property
3351 def status_lbl(self):
3351 def status_lbl(self):
3352 return ChangesetStatus.get_status_lbl(self.status)
3352 return ChangesetStatus.get_status_lbl(self.status)
3353
3353
3354 def get_api_data(self):
3354 def get_api_data(self):
3355 status = self
3355 status = self
3356 data = {
3356 data = {
3357 'status_id': status.changeset_status_id,
3357 'status_id': status.changeset_status_id,
3358 'status': status.status,
3358 'status': status.status,
3359 }
3359 }
3360 return data
3360 return data
3361
3361
3362 def __json__(self):
3362 def __json__(self):
3363 data = dict()
3363 data = dict()
3364 data.update(self.get_api_data())
3364 data.update(self.get_api_data())
3365 return data
3365 return data
3366
3366
3367
3367
3368 class _PullRequestBase(BaseModel):
3368 class _PullRequestBase(BaseModel):
3369 """
3369 """
3370 Common attributes of pull request and version entries.
3370 Common attributes of pull request and version entries.
3371 """
3371 """
3372
3372
3373 # .status values
3373 # .status values
3374 STATUS_NEW = u'new'
3374 STATUS_NEW = u'new'
3375 STATUS_OPEN = u'open'
3375 STATUS_OPEN = u'open'
3376 STATUS_CLOSED = u'closed'
3376 STATUS_CLOSED = u'closed'
3377
3377
3378 title = Column('title', Unicode(255), nullable=True)
3378 title = Column('title', Unicode(255), nullable=True)
3379 description = Column(
3379 description = Column(
3380 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3380 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3381 nullable=True)
3381 nullable=True)
3382 # new/open/closed status of pull request (not approve/reject/etc)
3382 # new/open/closed status of pull request (not approve/reject/etc)
3383 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3383 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3384 created_on = Column(
3384 created_on = Column(
3385 'created_on', DateTime(timezone=False), nullable=False,
3385 'created_on', DateTime(timezone=False), nullable=False,
3386 default=datetime.datetime.now)
3386 default=datetime.datetime.now)
3387 updated_on = Column(
3387 updated_on = Column(
3388 'updated_on', DateTime(timezone=False), nullable=False,
3388 'updated_on', DateTime(timezone=False), nullable=False,
3389 default=datetime.datetime.now)
3389 default=datetime.datetime.now)
3390
3390
3391 @declared_attr
3391 @declared_attr
3392 def user_id(cls):
3392 def user_id(cls):
3393 return Column(
3393 return Column(
3394 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3394 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3395 unique=None)
3395 unique=None)
3396
3396
3397 # 500 revisions max
3397 # 500 revisions max
3398 _revisions = Column(
3398 _revisions = Column(
3399 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3399 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3400
3400
3401 @declared_attr
3401 @declared_attr
3402 def source_repo_id(cls):
3402 def source_repo_id(cls):
3403 # TODO: dan: rename column to source_repo_id
3403 # TODO: dan: rename column to source_repo_id
3404 return Column(
3404 return Column(
3405 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3405 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3406 nullable=False)
3406 nullable=False)
3407
3407
3408 source_ref = Column('org_ref', Unicode(255), nullable=False)
3408 source_ref = Column('org_ref', Unicode(255), nullable=False)
3409
3409
3410 @declared_attr
3410 @declared_attr
3411 def target_repo_id(cls):
3411 def target_repo_id(cls):
3412 # TODO: dan: rename column to target_repo_id
3412 # TODO: dan: rename column to target_repo_id
3413 return Column(
3413 return Column(
3414 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3414 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3415 nullable=False)
3415 nullable=False)
3416
3416
3417 target_ref = Column('other_ref', Unicode(255), nullable=False)
3417 target_ref = Column('other_ref', Unicode(255), nullable=False)
3418 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3418 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3419
3419
3420 # TODO: dan: rename column to last_merge_source_rev
3420 # TODO: dan: rename column to last_merge_source_rev
3421 _last_merge_source_rev = Column(
3421 _last_merge_source_rev = Column(
3422 'last_merge_org_rev', String(40), nullable=True)
3422 'last_merge_org_rev', String(40), nullable=True)
3423 # TODO: dan: rename column to last_merge_target_rev
3423 # TODO: dan: rename column to last_merge_target_rev
3424 _last_merge_target_rev = Column(
3424 _last_merge_target_rev = Column(
3425 'last_merge_other_rev', String(40), nullable=True)
3425 'last_merge_other_rev', String(40), nullable=True)
3426 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3426 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3427 merge_rev = Column('merge_rev', String(40), nullable=True)
3427 merge_rev = Column('merge_rev', String(40), nullable=True)
3428
3428
3429 reviewer_data = Column(
3429 reviewer_data = Column(
3430 'reviewer_data_json', MutationObj.as_mutable(
3430 'reviewer_data_json', MutationObj.as_mutable(
3431 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3431 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3432
3432
3433 @property
3433 @property
3434 def reviewer_data_json(self):
3434 def reviewer_data_json(self):
3435 return json.dumps(self.reviewer_data)
3435 return json.dumps(self.reviewer_data)
3436
3436
3437 @hybrid_property
3437 @hybrid_property
3438 def description_safe(self):
3438 def description_safe(self):
3439 from rhodecode.lib import helpers as h
3439 from rhodecode.lib import helpers as h
3440 return h.escape(self.description)
3440 return h.escape(self.description)
3441
3441
3442 @hybrid_property
3442 @hybrid_property
3443 def revisions(self):
3443 def revisions(self):
3444 return self._revisions.split(':') if self._revisions else []
3444 return self._revisions.split(':') if self._revisions else []
3445
3445
3446 @revisions.setter
3446 @revisions.setter
3447 def revisions(self, val):
3447 def revisions(self, val):
3448 self._revisions = ':'.join(val)
3448 self._revisions = ':'.join(val)
3449
3449
3450 @hybrid_property
3450 @hybrid_property
3451 def last_merge_status(self):
3451 def last_merge_status(self):
3452 return safe_int(self._last_merge_status)
3452 return safe_int(self._last_merge_status)
3453
3453
3454 @last_merge_status.setter
3454 @last_merge_status.setter
3455 def last_merge_status(self, val):
3455 def last_merge_status(self, val):
3456 self._last_merge_status = val
3456 self._last_merge_status = val
3457
3457
3458 @declared_attr
3458 @declared_attr
3459 def author(cls):
3459 def author(cls):
3460 return relationship('User', lazy='joined')
3460 return relationship('User', lazy='joined')
3461
3461
3462 @declared_attr
3462 @declared_attr
3463 def source_repo(cls):
3463 def source_repo(cls):
3464 return relationship(
3464 return relationship(
3465 'Repository',
3465 'Repository',
3466 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3466 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3467
3467
3468 @property
3468 @property
3469 def source_ref_parts(self):
3469 def source_ref_parts(self):
3470 return self.unicode_to_reference(self.source_ref)
3470 return self.unicode_to_reference(self.source_ref)
3471
3471
3472 @declared_attr
3472 @declared_attr
3473 def target_repo(cls):
3473 def target_repo(cls):
3474 return relationship(
3474 return relationship(
3475 'Repository',
3475 'Repository',
3476 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3476 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3477
3477
3478 @property
3478 @property
3479 def target_ref_parts(self):
3479 def target_ref_parts(self):
3480 return self.unicode_to_reference(self.target_ref)
3480 return self.unicode_to_reference(self.target_ref)
3481
3481
3482 @property
3482 @property
3483 def shadow_merge_ref(self):
3483 def shadow_merge_ref(self):
3484 return self.unicode_to_reference(self._shadow_merge_ref)
3484 return self.unicode_to_reference(self._shadow_merge_ref)
3485
3485
3486 @shadow_merge_ref.setter
3486 @shadow_merge_ref.setter
3487 def shadow_merge_ref(self, ref):
3487 def shadow_merge_ref(self, ref):
3488 self._shadow_merge_ref = self.reference_to_unicode(ref)
3488 self._shadow_merge_ref = self.reference_to_unicode(ref)
3489
3489
3490 def unicode_to_reference(self, raw):
3490 def unicode_to_reference(self, raw):
3491 """
3491 """
3492 Convert a unicode (or string) to a reference object.
3492 Convert a unicode (or string) to a reference object.
3493 If unicode evaluates to False it returns None.
3493 If unicode evaluates to False it returns None.
3494 """
3494 """
3495 if raw:
3495 if raw:
3496 refs = raw.split(':')
3496 refs = raw.split(':')
3497 return Reference(*refs)
3497 return Reference(*refs)
3498 else:
3498 else:
3499 return None
3499 return None
3500
3500
3501 def reference_to_unicode(self, ref):
3501 def reference_to_unicode(self, ref):
3502 """
3502 """
3503 Convert a reference object to unicode.
3503 Convert a reference object to unicode.
3504 If reference is None it returns None.
3504 If reference is None it returns None.
3505 """
3505 """
3506 if ref:
3506 if ref:
3507 return u':'.join(ref)
3507 return u':'.join(ref)
3508 else:
3508 else:
3509 return None
3509 return None
3510
3510
3511 def get_api_data(self, with_merge_state=True):
3511 def get_api_data(self, with_merge_state=True):
3512 from rhodecode.model.pull_request import PullRequestModel
3512 from rhodecode.model.pull_request import PullRequestModel
3513
3513
3514 pull_request = self
3514 pull_request = self
3515 if with_merge_state:
3515 if with_merge_state:
3516 merge_status = PullRequestModel().merge_status(pull_request)
3516 merge_status = PullRequestModel().merge_status(pull_request)
3517 merge_state = {
3517 merge_state = {
3518 'status': merge_status[0],
3518 'status': merge_status[0],
3519 'message': safe_unicode(merge_status[1]),
3519 'message': safe_unicode(merge_status[1]),
3520 }
3520 }
3521 else:
3521 else:
3522 merge_state = {'status': 'not_available',
3522 merge_state = {'status': 'not_available',
3523 'message': 'not_available'}
3523 'message': 'not_available'}
3524
3524
3525 merge_data = {
3525 merge_data = {
3526 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3526 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3527 'reference': (
3527 'reference': (
3528 pull_request.shadow_merge_ref._asdict()
3528 pull_request.shadow_merge_ref._asdict()
3529 if pull_request.shadow_merge_ref else None),
3529 if pull_request.shadow_merge_ref else None),
3530 }
3530 }
3531
3531
3532 data = {
3532 data = {
3533 'pull_request_id': pull_request.pull_request_id,
3533 'pull_request_id': pull_request.pull_request_id,
3534 'url': PullRequestModel().get_url(pull_request),
3534 'url': PullRequestModel().get_url(pull_request),
3535 'title': pull_request.title,
3535 'title': pull_request.title,
3536 'description': pull_request.description,
3536 'description': pull_request.description,
3537 'status': pull_request.status,
3537 'status': pull_request.status,
3538 'created_on': pull_request.created_on,
3538 'created_on': pull_request.created_on,
3539 'updated_on': pull_request.updated_on,
3539 'updated_on': pull_request.updated_on,
3540 'commit_ids': pull_request.revisions,
3540 'commit_ids': pull_request.revisions,
3541 'review_status': pull_request.calculated_review_status(),
3541 'review_status': pull_request.calculated_review_status(),
3542 'mergeable': merge_state,
3542 'mergeable': merge_state,
3543 'source': {
3543 'source': {
3544 'clone_url': pull_request.source_repo.clone_url(),
3544 'clone_url': pull_request.source_repo.clone_url(),
3545 'repository': pull_request.source_repo.repo_name,
3545 'repository': pull_request.source_repo.repo_name,
3546 'reference': {
3546 'reference': {
3547 'name': pull_request.source_ref_parts.name,
3547 'name': pull_request.source_ref_parts.name,
3548 'type': pull_request.source_ref_parts.type,
3548 'type': pull_request.source_ref_parts.type,
3549 'commit_id': pull_request.source_ref_parts.commit_id,
3549 'commit_id': pull_request.source_ref_parts.commit_id,
3550 },
3550 },
3551 },
3551 },
3552 'target': {
3552 'target': {
3553 'clone_url': pull_request.target_repo.clone_url(),
3553 'clone_url': pull_request.target_repo.clone_url(),
3554 'repository': pull_request.target_repo.repo_name,
3554 'repository': pull_request.target_repo.repo_name,
3555 'reference': {
3555 'reference': {
3556 'name': pull_request.target_ref_parts.name,
3556 'name': pull_request.target_ref_parts.name,
3557 'type': pull_request.target_ref_parts.type,
3557 'type': pull_request.target_ref_parts.type,
3558 'commit_id': pull_request.target_ref_parts.commit_id,
3558 'commit_id': pull_request.target_ref_parts.commit_id,
3559 },
3559 },
3560 },
3560 },
3561 'merge': merge_data,
3561 'merge': merge_data,
3562 'author': pull_request.author.get_api_data(include_secrets=False,
3562 'author': pull_request.author.get_api_data(include_secrets=False,
3563 details='basic'),
3563 details='basic'),
3564 'reviewers': [
3564 'reviewers': [
3565 {
3565 {
3566 'user': reviewer.get_api_data(include_secrets=False,
3566 'user': reviewer.get_api_data(include_secrets=False,
3567 details='basic'),
3567 details='basic'),
3568 'reasons': reasons,
3568 'reasons': reasons,
3569 'review_status': st[0][1].status if st else 'not_reviewed',
3569 'review_status': st[0][1].status if st else 'not_reviewed',
3570 }
3570 }
3571 for reviewer, reasons, mandatory, st in
3571 for reviewer, reasons, mandatory, st in
3572 pull_request.reviewers_statuses()
3572 pull_request.reviewers_statuses()
3573 ]
3573 ]
3574 }
3574 }
3575
3575
3576 return data
3576 return data
3577
3577
3578
3578
3579 class PullRequest(Base, _PullRequestBase):
3579 class PullRequest(Base, _PullRequestBase):
3580 __tablename__ = 'pull_requests'
3580 __tablename__ = 'pull_requests'
3581 __table_args__ = (
3581 __table_args__ = (
3582 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3582 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3583 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3583 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3584 )
3584 )
3585
3585
3586 pull_request_id = Column(
3586 pull_request_id = Column(
3587 'pull_request_id', Integer(), nullable=False, primary_key=True)
3587 'pull_request_id', Integer(), nullable=False, primary_key=True)
3588
3588
3589 def __repr__(self):
3589 def __repr__(self):
3590 if self.pull_request_id:
3590 if self.pull_request_id:
3591 return '<DB:PullRequest #%s>' % self.pull_request_id
3591 return '<DB:PullRequest #%s>' % self.pull_request_id
3592 else:
3592 else:
3593 return '<DB:PullRequest at %#x>' % id(self)
3593 return '<DB:PullRequest at %#x>' % id(self)
3594
3594
3595 reviewers = relationship('PullRequestReviewers',
3595 reviewers = relationship('PullRequestReviewers',
3596 cascade="all, delete, delete-orphan")
3596 cascade="all, delete, delete-orphan")
3597 statuses = relationship('ChangesetStatus',
3597 statuses = relationship('ChangesetStatus',
3598 cascade="all, delete, delete-orphan")
3598 cascade="all, delete, delete-orphan")
3599 comments = relationship('ChangesetComment',
3599 comments = relationship('ChangesetComment',
3600 cascade="all, delete, delete-orphan")
3600 cascade="all, delete, delete-orphan")
3601 versions = relationship('PullRequestVersion',
3601 versions = relationship('PullRequestVersion',
3602 cascade="all, delete, delete-orphan",
3602 cascade="all, delete, delete-orphan",
3603 lazy='dynamic')
3603 lazy='dynamic')
3604
3604
3605 @classmethod
3605 @classmethod
3606 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3606 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3607 internal_methods=None):
3607 internal_methods=None):
3608
3608
3609 class PullRequestDisplay(object):
3609 class PullRequestDisplay(object):
3610 """
3610 """
3611 Special object wrapper for showing PullRequest data via Versions
3611 Special object wrapper for showing PullRequest data via Versions
3612 It mimics PR object as close as possible. This is read only object
3612 It mimics PR object as close as possible. This is read only object
3613 just for display
3613 just for display
3614 """
3614 """
3615
3615
3616 def __init__(self, attrs, internal=None):
3616 def __init__(self, attrs, internal=None):
3617 self.attrs = attrs
3617 self.attrs = attrs
3618 # internal have priority over the given ones via attrs
3618 # internal have priority over the given ones via attrs
3619 self.internal = internal or ['versions']
3619 self.internal = internal or ['versions']
3620
3620
3621 def __getattr__(self, item):
3621 def __getattr__(self, item):
3622 if item in self.internal:
3622 if item in self.internal:
3623 return getattr(self, item)
3623 return getattr(self, item)
3624 try:
3624 try:
3625 return self.attrs[item]
3625 return self.attrs[item]
3626 except KeyError:
3626 except KeyError:
3627 raise AttributeError(
3627 raise AttributeError(
3628 '%s object has no attribute %s' % (self, item))
3628 '%s object has no attribute %s' % (self, item))
3629
3629
3630 def __repr__(self):
3630 def __repr__(self):
3631 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3631 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3632
3632
3633 def versions(self):
3633 def versions(self):
3634 return pull_request_obj.versions.order_by(
3634 return pull_request_obj.versions.order_by(
3635 PullRequestVersion.pull_request_version_id).all()
3635 PullRequestVersion.pull_request_version_id).all()
3636
3636
3637 def is_closed(self):
3637 def is_closed(self):
3638 return pull_request_obj.is_closed()
3638 return pull_request_obj.is_closed()
3639
3639
3640 @property
3640 @property
3641 def pull_request_version_id(self):
3641 def pull_request_version_id(self):
3642 return getattr(pull_request_obj, 'pull_request_version_id', None)
3642 return getattr(pull_request_obj, 'pull_request_version_id', None)
3643
3643
3644 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3644 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3645
3645
3646 attrs.author = StrictAttributeDict(
3646 attrs.author = StrictAttributeDict(
3647 pull_request_obj.author.get_api_data())
3647 pull_request_obj.author.get_api_data())
3648 if pull_request_obj.target_repo:
3648 if pull_request_obj.target_repo:
3649 attrs.target_repo = StrictAttributeDict(
3649 attrs.target_repo = StrictAttributeDict(
3650 pull_request_obj.target_repo.get_api_data())
3650 pull_request_obj.target_repo.get_api_data())
3651 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3651 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3652
3652
3653 if pull_request_obj.source_repo:
3653 if pull_request_obj.source_repo:
3654 attrs.source_repo = StrictAttributeDict(
3654 attrs.source_repo = StrictAttributeDict(
3655 pull_request_obj.source_repo.get_api_data())
3655 pull_request_obj.source_repo.get_api_data())
3656 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3656 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3657
3657
3658 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3658 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3659 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3659 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3660 attrs.revisions = pull_request_obj.revisions
3660 attrs.revisions = pull_request_obj.revisions
3661
3661
3662 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3662 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3663 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3663 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3664 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3664 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3665
3665
3666 return PullRequestDisplay(attrs, internal=internal_methods)
3666 return PullRequestDisplay(attrs, internal=internal_methods)
3667
3667
3668 def is_closed(self):
3668 def is_closed(self):
3669 return self.status == self.STATUS_CLOSED
3669 return self.status == self.STATUS_CLOSED
3670
3670
3671 def __json__(self):
3671 def __json__(self):
3672 return {
3672 return {
3673 'revisions': self.revisions,
3673 'revisions': self.revisions,
3674 }
3674 }
3675
3675
3676 def calculated_review_status(self):
3676 def calculated_review_status(self):
3677 from rhodecode.model.changeset_status import ChangesetStatusModel
3677 from rhodecode.model.changeset_status import ChangesetStatusModel
3678 return ChangesetStatusModel().calculated_review_status(self)
3678 return ChangesetStatusModel().calculated_review_status(self)
3679
3679
3680 def reviewers_statuses(self):
3680 def reviewers_statuses(self):
3681 from rhodecode.model.changeset_status import ChangesetStatusModel
3681 from rhodecode.model.changeset_status import ChangesetStatusModel
3682 return ChangesetStatusModel().reviewers_statuses(self)
3682 return ChangesetStatusModel().reviewers_statuses(self)
3683
3683
3684 @property
3684 @property
3685 def workspace_id(self):
3685 def workspace_id(self):
3686 from rhodecode.model.pull_request import PullRequestModel
3686 from rhodecode.model.pull_request import PullRequestModel
3687 return PullRequestModel()._workspace_id(self)
3687 return PullRequestModel()._workspace_id(self)
3688
3688
3689 def get_shadow_repo(self):
3689 def get_shadow_repo(self):
3690 workspace_id = self.workspace_id
3690 workspace_id = self.workspace_id
3691 vcs_obj = self.target_repo.scm_instance()
3691 vcs_obj = self.target_repo.scm_instance()
3692 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3692 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3693 workspace_id)
3693 workspace_id)
3694 return vcs_obj._get_shadow_instance(shadow_repository_path)
3694 return vcs_obj._get_shadow_instance(shadow_repository_path)
3695
3695
3696
3696
3697 class PullRequestVersion(Base, _PullRequestBase):
3697 class PullRequestVersion(Base, _PullRequestBase):
3698 __tablename__ = 'pull_request_versions'
3698 __tablename__ = 'pull_request_versions'
3699 __table_args__ = (
3699 __table_args__ = (
3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3702 )
3702 )
3703
3703
3704 pull_request_version_id = Column(
3704 pull_request_version_id = Column(
3705 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3705 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3706 pull_request_id = Column(
3706 pull_request_id = Column(
3707 'pull_request_id', Integer(),
3707 'pull_request_id', Integer(),
3708 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3708 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3709 pull_request = relationship('PullRequest')
3709 pull_request = relationship('PullRequest')
3710
3710
3711 def __repr__(self):
3711 def __repr__(self):
3712 if self.pull_request_version_id:
3712 if self.pull_request_version_id:
3713 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3713 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3714 else:
3714 else:
3715 return '<DB:PullRequestVersion at %#x>' % id(self)
3715 return '<DB:PullRequestVersion at %#x>' % id(self)
3716
3716
3717 @property
3717 @property
3718 def reviewers(self):
3718 def reviewers(self):
3719 return self.pull_request.reviewers
3719 return self.pull_request.reviewers
3720
3720
3721 @property
3721 @property
3722 def versions(self):
3722 def versions(self):
3723 return self.pull_request.versions
3723 return self.pull_request.versions
3724
3724
3725 def is_closed(self):
3725 def is_closed(self):
3726 # calculate from original
3726 # calculate from original
3727 return self.pull_request.status == self.STATUS_CLOSED
3727 return self.pull_request.status == self.STATUS_CLOSED
3728
3728
3729 def calculated_review_status(self):
3729 def calculated_review_status(self):
3730 return self.pull_request.calculated_review_status()
3730 return self.pull_request.calculated_review_status()
3731
3731
3732 def reviewers_statuses(self):
3732 def reviewers_statuses(self):
3733 return self.pull_request.reviewers_statuses()
3733 return self.pull_request.reviewers_statuses()
3734
3734
3735
3735
3736 class PullRequestReviewers(Base, BaseModel):
3736 class PullRequestReviewers(Base, BaseModel):
3737 __tablename__ = 'pull_request_reviewers'
3737 __tablename__ = 'pull_request_reviewers'
3738 __table_args__ = (
3738 __table_args__ = (
3739 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3739 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3740 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3740 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3741 )
3741 )
3742
3742
3743 @hybrid_property
3743 @hybrid_property
3744 def reasons(self):
3744 def reasons(self):
3745 if not self._reasons:
3745 if not self._reasons:
3746 return []
3746 return []
3747 return self._reasons
3747 return self._reasons
3748
3748
3749 @reasons.setter
3749 @reasons.setter
3750 def reasons(self, val):
3750 def reasons(self, val):
3751 val = val or []
3751 val = val or []
3752 if any(not isinstance(x, basestring) for x in val):
3752 if any(not isinstance(x, basestring) for x in val):
3753 raise Exception('invalid reasons type, must be list of strings')
3753 raise Exception('invalid reasons type, must be list of strings')
3754 self._reasons = val
3754 self._reasons = val
3755
3755
3756 pull_requests_reviewers_id = Column(
3756 pull_requests_reviewers_id = Column(
3757 'pull_requests_reviewers_id', Integer(), nullable=False,
3757 'pull_requests_reviewers_id', Integer(), nullable=False,
3758 primary_key=True)
3758 primary_key=True)
3759 pull_request_id = Column(
3759 pull_request_id = Column(
3760 "pull_request_id", Integer(),
3760 "pull_request_id", Integer(),
3761 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3761 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3762 user_id = Column(
3762 user_id = Column(
3763 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3763 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3764 _reasons = Column(
3764 _reasons = Column(
3765 'reason', MutationList.as_mutable(
3765 'reason', MutationList.as_mutable(
3766 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3766 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3767 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3767 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3768 user = relationship('User')
3768 user = relationship('User')
3769 pull_request = relationship('PullRequest')
3769 pull_request = relationship('PullRequest')
3770
3770
3771
3771
3772 class Notification(Base, BaseModel):
3772 class Notification(Base, BaseModel):
3773 __tablename__ = 'notifications'
3773 __tablename__ = 'notifications'
3774 __table_args__ = (
3774 __table_args__ = (
3775 Index('notification_type_idx', 'type'),
3775 Index('notification_type_idx', 'type'),
3776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3778 )
3778 )
3779
3779
3780 TYPE_CHANGESET_COMMENT = u'cs_comment'
3780 TYPE_CHANGESET_COMMENT = u'cs_comment'
3781 TYPE_MESSAGE = u'message'
3781 TYPE_MESSAGE = u'message'
3782 TYPE_MENTION = u'mention'
3782 TYPE_MENTION = u'mention'
3783 TYPE_REGISTRATION = u'registration'
3783 TYPE_REGISTRATION = u'registration'
3784 TYPE_PULL_REQUEST = u'pull_request'
3784 TYPE_PULL_REQUEST = u'pull_request'
3785 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3785 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3786
3786
3787 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3787 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3788 subject = Column('subject', Unicode(512), nullable=True)
3788 subject = Column('subject', Unicode(512), nullable=True)
3789 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3789 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3790 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3790 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3791 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3791 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3792 type_ = Column('type', Unicode(255))
3792 type_ = Column('type', Unicode(255))
3793
3793
3794 created_by_user = relationship('User')
3794 created_by_user = relationship('User')
3795 notifications_to_users = relationship('UserNotification', lazy='joined',
3795 notifications_to_users = relationship('UserNotification', lazy='joined',
3796 cascade="all, delete, delete-orphan")
3796 cascade="all, delete, delete-orphan")
3797
3797
3798 @property
3798 @property
3799 def recipients(self):
3799 def recipients(self):
3800 return [x.user for x in UserNotification.query()\
3800 return [x.user for x in UserNotification.query()\
3801 .filter(UserNotification.notification == self)\
3801 .filter(UserNotification.notification == self)\
3802 .order_by(UserNotification.user_id.asc()).all()]
3802 .order_by(UserNotification.user_id.asc()).all()]
3803
3803
3804 @classmethod
3804 @classmethod
3805 def create(cls, created_by, subject, body, recipients, type_=None):
3805 def create(cls, created_by, subject, body, recipients, type_=None):
3806 if type_ is None:
3806 if type_ is None:
3807 type_ = Notification.TYPE_MESSAGE
3807 type_ = Notification.TYPE_MESSAGE
3808
3808
3809 notification = cls()
3809 notification = cls()
3810 notification.created_by_user = created_by
3810 notification.created_by_user = created_by
3811 notification.subject = subject
3811 notification.subject = subject
3812 notification.body = body
3812 notification.body = body
3813 notification.type_ = type_
3813 notification.type_ = type_
3814 notification.created_on = datetime.datetime.now()
3814 notification.created_on = datetime.datetime.now()
3815
3815
3816 for u in recipients:
3816 for u in recipients:
3817 assoc = UserNotification()
3817 assoc = UserNotification()
3818 assoc.notification = notification
3818 assoc.notification = notification
3819
3819
3820 # if created_by is inside recipients mark his notification
3820 # if created_by is inside recipients mark his notification
3821 # as read
3821 # as read
3822 if u.user_id == created_by.user_id:
3822 if u.user_id == created_by.user_id:
3823 assoc.read = True
3823 assoc.read = True
3824
3824
3825 u.notifications.append(assoc)
3825 u.notifications.append(assoc)
3826 Session().add(notification)
3826 Session().add(notification)
3827
3827
3828 return notification
3828 return notification
3829
3829
3830
3830
3831 class UserNotification(Base, BaseModel):
3831 class UserNotification(Base, BaseModel):
3832 __tablename__ = 'user_to_notification'
3832 __tablename__ = 'user_to_notification'
3833 __table_args__ = (
3833 __table_args__ = (
3834 UniqueConstraint('user_id', 'notification_id'),
3834 UniqueConstraint('user_id', 'notification_id'),
3835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3837 )
3837 )
3838 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3838 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3839 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3839 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3840 read = Column('read', Boolean, default=False)
3840 read = Column('read', Boolean, default=False)
3841 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3841 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3842
3842
3843 user = relationship('User', lazy="joined")
3843 user = relationship('User', lazy="joined")
3844 notification = relationship('Notification', lazy="joined",
3844 notification = relationship('Notification', lazy="joined",
3845 order_by=lambda: Notification.created_on.desc(),)
3845 order_by=lambda: Notification.created_on.desc(),)
3846
3846
3847 def mark_as_read(self):
3847 def mark_as_read(self):
3848 self.read = True
3848 self.read = True
3849 Session().add(self)
3849 Session().add(self)
3850
3850
3851
3851
3852 class Gist(Base, BaseModel):
3852 class Gist(Base, BaseModel):
3853 __tablename__ = 'gists'
3853 __tablename__ = 'gists'
3854 __table_args__ = (
3854 __table_args__ = (
3855 Index('g_gist_access_id_idx', 'gist_access_id'),
3855 Index('g_gist_access_id_idx', 'gist_access_id'),
3856 Index('g_created_on_idx', 'created_on'),
3856 Index('g_created_on_idx', 'created_on'),
3857 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3857 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3858 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3858 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3859 )
3859 )
3860 GIST_PUBLIC = u'public'
3860 GIST_PUBLIC = u'public'
3861 GIST_PRIVATE = u'private'
3861 GIST_PRIVATE = u'private'
3862 DEFAULT_FILENAME = u'gistfile1.txt'
3862 DEFAULT_FILENAME = u'gistfile1.txt'
3863
3863
3864 ACL_LEVEL_PUBLIC = u'acl_public'
3864 ACL_LEVEL_PUBLIC = u'acl_public'
3865 ACL_LEVEL_PRIVATE = u'acl_private'
3865 ACL_LEVEL_PRIVATE = u'acl_private'
3866
3866
3867 gist_id = Column('gist_id', Integer(), primary_key=True)
3867 gist_id = Column('gist_id', Integer(), primary_key=True)
3868 gist_access_id = Column('gist_access_id', Unicode(250))
3868 gist_access_id = Column('gist_access_id', Unicode(250))
3869 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3869 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3870 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3870 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3871 gist_expires = Column('gist_expires', Float(53), nullable=False)
3871 gist_expires = Column('gist_expires', Float(53), nullable=False)
3872 gist_type = Column('gist_type', Unicode(128), nullable=False)
3872 gist_type = Column('gist_type', Unicode(128), nullable=False)
3873 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3873 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3874 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3874 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3875 acl_level = Column('acl_level', Unicode(128), nullable=True)
3875 acl_level = Column('acl_level', Unicode(128), nullable=True)
3876
3876
3877 owner = relationship('User')
3877 owner = relationship('User')
3878
3878
3879 def __repr__(self):
3879 def __repr__(self):
3880 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3880 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3881
3881
3882 @hybrid_property
3882 @hybrid_property
3883 def description_safe(self):
3883 def description_safe(self):
3884 from rhodecode.lib import helpers as h
3884 from rhodecode.lib import helpers as h
3885 return h.escape(self.gist_description)
3885 return h.escape(self.gist_description)
3886
3886
3887 @classmethod
3887 @classmethod
3888 def get_or_404(cls, id_):
3888 def get_or_404(cls, id_):
3889 from pyramid.httpexceptions import HTTPNotFound
3889 from pyramid.httpexceptions import HTTPNotFound
3890
3890
3891 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3891 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3892 if not res:
3892 if not res:
3893 raise HTTPNotFound()
3893 raise HTTPNotFound()
3894 return res
3894 return res
3895
3895
3896 @classmethod
3896 @classmethod
3897 def get_by_access_id(cls, gist_access_id):
3897 def get_by_access_id(cls, gist_access_id):
3898 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3898 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3899
3899
3900 def gist_url(self):
3900 def gist_url(self):
3901 from rhodecode.model.gist import GistModel
3901 from rhodecode.model.gist import GistModel
3902 return GistModel().get_url(self)
3902 return GistModel().get_url(self)
3903
3903
3904 @classmethod
3904 @classmethod
3905 def base_path(cls):
3905 def base_path(cls):
3906 """
3906 """
3907 Returns base path when all gists are stored
3907 Returns base path when all gists are stored
3908
3908
3909 :param cls:
3909 :param cls:
3910 """
3910 """
3911 from rhodecode.model.gist import GIST_STORE_LOC
3911 from rhodecode.model.gist import GIST_STORE_LOC
3912 q = Session().query(RhodeCodeUi)\
3912 q = Session().query(RhodeCodeUi)\
3913 .filter(RhodeCodeUi.ui_key == URL_SEP)
3913 .filter(RhodeCodeUi.ui_key == URL_SEP)
3914 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3914 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3915 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3915 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3916
3916
3917 def get_api_data(self):
3917 def get_api_data(self):
3918 """
3918 """
3919 Common function for generating gist related data for API
3919 Common function for generating gist related data for API
3920 """
3920 """
3921 gist = self
3921 gist = self
3922 data = {
3922 data = {
3923 'gist_id': gist.gist_id,
3923 'gist_id': gist.gist_id,
3924 'type': gist.gist_type,
3924 'type': gist.gist_type,
3925 'access_id': gist.gist_access_id,
3925 'access_id': gist.gist_access_id,
3926 'description': gist.gist_description,
3926 'description': gist.gist_description,
3927 'url': gist.gist_url(),
3927 'url': gist.gist_url(),
3928 'expires': gist.gist_expires,
3928 'expires': gist.gist_expires,
3929 'created_on': gist.created_on,
3929 'created_on': gist.created_on,
3930 'modified_at': gist.modified_at,
3930 'modified_at': gist.modified_at,
3931 'content': None,
3931 'content': None,
3932 'acl_level': gist.acl_level,
3932 'acl_level': gist.acl_level,
3933 }
3933 }
3934 return data
3934 return data
3935
3935
3936 def __json__(self):
3936 def __json__(self):
3937 data = dict(
3937 data = dict(
3938 )
3938 )
3939 data.update(self.get_api_data())
3939 data.update(self.get_api_data())
3940 return data
3940 return data
3941 # SCM functions
3941 # SCM functions
3942
3942
3943 def scm_instance(self, **kwargs):
3943 def scm_instance(self, **kwargs):
3944 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3944 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3945 return get_vcs_instance(
3945 return get_vcs_instance(
3946 repo_path=safe_str(full_repo_path), create=False)
3946 repo_path=safe_str(full_repo_path), create=False)
3947
3947
3948
3948
3949 class ExternalIdentity(Base, BaseModel):
3949 class ExternalIdentity(Base, BaseModel):
3950 __tablename__ = 'external_identities'
3950 __tablename__ = 'external_identities'
3951 __table_args__ = (
3951 __table_args__ = (
3952 Index('local_user_id_idx', 'local_user_id'),
3952 Index('local_user_id_idx', 'local_user_id'),
3953 Index('external_id_idx', 'external_id'),
3953 Index('external_id_idx', 'external_id'),
3954 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3954 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3955 'mysql_charset': 'utf8'})
3955 'mysql_charset': 'utf8'})
3956
3956
3957 external_id = Column('external_id', Unicode(255), default=u'',
3957 external_id = Column('external_id', Unicode(255), default=u'',
3958 primary_key=True)
3958 primary_key=True)
3959 external_username = Column('external_username', Unicode(1024), default=u'')
3959 external_username = Column('external_username', Unicode(1024), default=u'')
3960 local_user_id = Column('local_user_id', Integer(),
3960 local_user_id = Column('local_user_id', Integer(),
3961 ForeignKey('users.user_id'), primary_key=True)
3961 ForeignKey('users.user_id'), primary_key=True)
3962 provider_name = Column('provider_name', Unicode(255), default=u'',
3962 provider_name = Column('provider_name', Unicode(255), default=u'',
3963 primary_key=True)
3963 primary_key=True)
3964 access_token = Column('access_token', String(1024), default=u'')
3964 access_token = Column('access_token', String(1024), default=u'')
3965 alt_token = Column('alt_token', String(1024), default=u'')
3965 alt_token = Column('alt_token', String(1024), default=u'')
3966 token_secret = Column('token_secret', String(1024), default=u'')
3966 token_secret = Column('token_secret', String(1024), default=u'')
3967
3967
3968 @classmethod
3968 @classmethod
3969 def by_external_id_and_provider(cls, external_id, provider_name,
3969 def by_external_id_and_provider(cls, external_id, provider_name,
3970 local_user_id=None):
3970 local_user_id=None):
3971 """
3971 """
3972 Returns ExternalIdentity instance based on search params
3972 Returns ExternalIdentity instance based on search params
3973
3973
3974 :param external_id:
3974 :param external_id:
3975 :param provider_name:
3975 :param provider_name:
3976 :return: ExternalIdentity
3976 :return: ExternalIdentity
3977 """
3977 """
3978 query = cls.query()
3978 query = cls.query()
3979 query = query.filter(cls.external_id == external_id)
3979 query = query.filter(cls.external_id == external_id)
3980 query = query.filter(cls.provider_name == provider_name)
3980 query = query.filter(cls.provider_name == provider_name)
3981 if local_user_id:
3981 if local_user_id:
3982 query = query.filter(cls.local_user_id == local_user_id)
3982 query = query.filter(cls.local_user_id == local_user_id)
3983 return query.first()
3983 return query.first()
3984
3984
3985 @classmethod
3985 @classmethod
3986 def user_by_external_id_and_provider(cls, external_id, provider_name):
3986 def user_by_external_id_and_provider(cls, external_id, provider_name):
3987 """
3987 """
3988 Returns User instance based on search params
3988 Returns User instance based on search params
3989
3989
3990 :param external_id:
3990 :param external_id:
3991 :param provider_name:
3991 :param provider_name:
3992 :return: User
3992 :return: User
3993 """
3993 """
3994 query = User.query()
3994 query = User.query()
3995 query = query.filter(cls.external_id == external_id)
3995 query = query.filter(cls.external_id == external_id)
3996 query = query.filter(cls.provider_name == provider_name)
3996 query = query.filter(cls.provider_name == provider_name)
3997 query = query.filter(User.user_id == cls.local_user_id)
3997 query = query.filter(User.user_id == cls.local_user_id)
3998 return query.first()
3998 return query.first()
3999
3999
4000 @classmethod
4000 @classmethod
4001 def by_local_user_id(cls, local_user_id):
4001 def by_local_user_id(cls, local_user_id):
4002 """
4002 """
4003 Returns all tokens for user
4003 Returns all tokens for user
4004
4004
4005 :param local_user_id:
4005 :param local_user_id:
4006 :return: ExternalIdentity
4006 :return: ExternalIdentity
4007 """
4007 """
4008 query = cls.query()
4008 query = cls.query()
4009 query = query.filter(cls.local_user_id == local_user_id)
4009 query = query.filter(cls.local_user_id == local_user_id)
4010 return query
4010 return query
4011
4011
4012
4012
4013 class Integration(Base, BaseModel):
4013 class Integration(Base, BaseModel):
4014 __tablename__ = 'integrations'
4014 __tablename__ = 'integrations'
4015 __table_args__ = (
4015 __table_args__ = (
4016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4017 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4017 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4018 )
4018 )
4019
4019
4020 integration_id = Column('integration_id', Integer(), primary_key=True)
4020 integration_id = Column('integration_id', Integer(), primary_key=True)
4021 integration_type = Column('integration_type', String(255))
4021 integration_type = Column('integration_type', String(255))
4022 enabled = Column('enabled', Boolean(), nullable=False)
4022 enabled = Column('enabled', Boolean(), nullable=False)
4023 name = Column('name', String(255), nullable=False)
4023 name = Column('name', String(255), nullable=False)
4024 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4024 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4025 default=False)
4025 default=False)
4026
4026
4027 settings = Column(
4027 settings = Column(
4028 'settings_json', MutationObj.as_mutable(
4028 'settings_json', MutationObj.as_mutable(
4029 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4029 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4030 repo_id = Column(
4030 repo_id = Column(
4031 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4031 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4032 nullable=True, unique=None, default=None)
4032 nullable=True, unique=None, default=None)
4033 repo = relationship('Repository', lazy='joined')
4033 repo = relationship('Repository', lazy='joined')
4034
4034
4035 repo_group_id = Column(
4035 repo_group_id = Column(
4036 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4036 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4037 nullable=True, unique=None, default=None)
4037 nullable=True, unique=None, default=None)
4038 repo_group = relationship('RepoGroup', lazy='joined')
4038 repo_group = relationship('RepoGroup', lazy='joined')
4039
4039
4040 @property
4040 @property
4041 def scope(self):
4041 def scope(self):
4042 if self.repo:
4042 if self.repo:
4043 return repr(self.repo)
4043 return repr(self.repo)
4044 if self.repo_group:
4044 if self.repo_group:
4045 if self.child_repos_only:
4045 if self.child_repos_only:
4046 return repr(self.repo_group) + ' (child repos only)'
4046 return repr(self.repo_group) + ' (child repos only)'
4047 else:
4047 else:
4048 return repr(self.repo_group) + ' (recursive)'
4048 return repr(self.repo_group) + ' (recursive)'
4049 if self.child_repos_only:
4049 if self.child_repos_only:
4050 return 'root_repos'
4050 return 'root_repos'
4051 return 'global'
4051 return 'global'
4052
4052
4053 def __repr__(self):
4053 def __repr__(self):
4054 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4054 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4055
4055
4056
4056
4057 class RepoReviewRuleUser(Base, BaseModel):
4057 class RepoReviewRuleUser(Base, BaseModel):
4058 __tablename__ = 'repo_review_rules_users'
4058 __tablename__ = 'repo_review_rules_users'
4059 __table_args__ = (
4059 __table_args__ = (
4060 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4060 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4061 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4061 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4062 )
4062 )
4063 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4063 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4064 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4064 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4065 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4065 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4066 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4066 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4067 user = relationship('User')
4067 user = relationship('User')
4068
4068
4069 def rule_data(self):
4069 def rule_data(self):
4070 return {
4070 return {
4071 'mandatory': self.mandatory
4071 'mandatory': self.mandatory
4072 }
4072 }
4073
4073
4074
4074
4075 class RepoReviewRuleUserGroup(Base, BaseModel):
4075 class RepoReviewRuleUserGroup(Base, BaseModel):
4076 __tablename__ = 'repo_review_rules_users_groups'
4076 __tablename__ = 'repo_review_rules_users_groups'
4077 __table_args__ = (
4077 __table_args__ = (
4078 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4078 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4079 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4079 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4080 )
4080 )
4081 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4081 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4082 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4082 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4083 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4083 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4084 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4084 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4085 users_group = relationship('UserGroup')
4085 users_group = relationship('UserGroup')
4086
4086
4087 def rule_data(self):
4087 def rule_data(self):
4088 return {
4088 return {
4089 'mandatory': self.mandatory
4089 'mandatory': self.mandatory
4090 }
4090 }
4091
4091
4092
4092
4093 class RepoReviewRule(Base, BaseModel):
4093 class RepoReviewRule(Base, BaseModel):
4094 __tablename__ = 'repo_review_rules'
4094 __tablename__ = 'repo_review_rules'
4095 __table_args__ = (
4095 __table_args__ = (
4096 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4096 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4097 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4097 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4098 )
4098 )
4099
4099
4100 repo_review_rule_id = Column(
4100 repo_review_rule_id = Column(
4101 'repo_review_rule_id', Integer(), primary_key=True)
4101 'repo_review_rule_id', Integer(), primary_key=True)
4102 repo_id = Column(
4102 repo_id = Column(
4103 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4103 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4104 repo = relationship('Repository', backref='review_rules')
4104 repo = relationship('Repository', backref='review_rules')
4105
4105
4106 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4106 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4107 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4107 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4108
4108
4109 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4109 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4110 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4110 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4111 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4111 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4112 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4112 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4113
4113
4114 rule_users = relationship('RepoReviewRuleUser')
4114 rule_users = relationship('RepoReviewRuleUser')
4115 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4115 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4116
4116
4117 @hybrid_property
4117 @hybrid_property
4118 def branch_pattern(self):
4118 def branch_pattern(self):
4119 return self._branch_pattern or '*'
4119 return self._branch_pattern or '*'
4120
4120
4121 def _validate_glob(self, value):
4121 def _validate_glob(self, value):
4122 re.compile('^' + glob2re(value) + '$')
4122 re.compile('^' + glob2re(value) + '$')
4123
4123
4124 @branch_pattern.setter
4124 @branch_pattern.setter
4125 def branch_pattern(self, value):
4125 def branch_pattern(self, value):
4126 self._validate_glob(value)
4126 self._validate_glob(value)
4127 self._branch_pattern = value or '*'
4127 self._branch_pattern = value or '*'
4128
4128
4129 @hybrid_property
4129 @hybrid_property
4130 def file_pattern(self):
4130 def file_pattern(self):
4131 return self._file_pattern or '*'
4131 return self._file_pattern or '*'
4132
4132
4133 @file_pattern.setter
4133 @file_pattern.setter
4134 def file_pattern(self, value):
4134 def file_pattern(self, value):
4135 self._validate_glob(value)
4135 self._validate_glob(value)
4136 self._file_pattern = value or '*'
4136 self._file_pattern = value or '*'
4137
4137
4138 def matches(self, branch, files_changed):
4138 def matches(self, branch, files_changed):
4139 """
4139 """
4140 Check if this review rule matches a branch/files in a pull request
4140 Check if this review rule matches a branch/files in a pull request
4141
4141
4142 :param branch: branch name for the commit
4142 :param branch: branch name for the commit
4143 :param files_changed: list of file paths changed in the pull request
4143 :param files_changed: list of file paths changed in the pull request
4144 """
4144 """
4145
4145
4146 branch = branch or ''
4146 branch = branch or ''
4147 files_changed = files_changed or []
4147 files_changed = files_changed or []
4148
4148
4149 branch_matches = True
4149 branch_matches = True
4150 if branch:
4150 if branch:
4151 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4151 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4152 branch_matches = bool(branch_regex.search(branch))
4152 branch_matches = bool(branch_regex.search(branch))
4153
4153
4154 files_matches = True
4154 files_matches = True
4155 if self.file_pattern != '*':
4155 if self.file_pattern != '*':
4156 files_matches = False
4156 files_matches = False
4157 file_regex = re.compile(glob2re(self.file_pattern))
4157 file_regex = re.compile(glob2re(self.file_pattern))
4158 for filename in files_changed:
4158 for filename in files_changed:
4159 if file_regex.search(filename):
4159 if file_regex.search(filename):
4160 files_matches = True
4160 files_matches = True
4161 break
4161 break
4162
4162
4163 return branch_matches and files_matches
4163 return branch_matches and files_matches
4164
4164
4165 @property
4165 @property
4166 def review_users(self):
4166 def review_users(self):
4167 """ Returns the users which this rule applies to """
4167 """ Returns the users which this rule applies to """
4168
4168
4169 users = collections.OrderedDict()
4169 users = collections.OrderedDict()
4170
4170
4171 for rule_user in self.rule_users:
4171 for rule_user in self.rule_users:
4172 if rule_user.user.active:
4172 if rule_user.user.active:
4173 if rule_user.user not in users:
4173 if rule_user.user not in users:
4174 users[rule_user.user.username] = {
4174 users[rule_user.user.username] = {
4175 'user': rule_user.user,
4175 'user': rule_user.user,
4176 'source': 'user',
4176 'source': 'user',
4177 'source_data': {},
4177 'source_data': {},
4178 'data': rule_user.rule_data()
4178 'data': rule_user.rule_data()
4179 }
4179 }
4180
4180
4181 for rule_user_group in self.rule_user_groups:
4181 for rule_user_group in self.rule_user_groups:
4182 source_data = {
4182 source_data = {
4183 'name': rule_user_group.users_group.users_group_name,
4183 'name': rule_user_group.users_group.users_group_name,
4184 'members': len(rule_user_group.users_group.members)
4184 'members': len(rule_user_group.users_group.members)
4185 }
4185 }
4186 for member in rule_user_group.users_group.members:
4186 for member in rule_user_group.users_group.members:
4187 if member.user.active:
4187 if member.user.active:
4188 users[member.user.username] = {
4188 users[member.user.username] = {
4189 'user': member.user,
4189 'user': member.user,
4190 'source': 'user_group',
4190 'source': 'user_group',
4191 'source_data': source_data,
4191 'source_data': source_data,
4192 'data': rule_user_group.rule_data()
4192 'data': rule_user_group.rule_data()
4193 }
4193 }
4194
4194
4195 return users
4195 return users
4196
4196
4197 def __repr__(self):
4197 def __repr__(self):
4198 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4198 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4199 self.repo_review_rule_id, self.repo)
4199 self.repo_review_rule_id, self.repo)
4200
4200
4201
4201
4202 class DbMigrateVersion(Base, BaseModel):
4202 class DbMigrateVersion(Base, BaseModel):
4203 __tablename__ = 'db_migrate_version'
4203 __tablename__ = 'db_migrate_version'
4204 __table_args__ = (
4204 __table_args__ = (
4205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4207 )
4207 )
4208 repository_id = Column('repository_id', String(250), primary_key=True)
4208 repository_id = Column('repository_id', String(250), primary_key=True)
4209 repository_path = Column('repository_path', Text)
4209 repository_path = Column('repository_path', Text)
4210 version = Column('version', Integer)
4210 version = Column('version', Integer)
4211
4211
4212
4212
4213 class DbSession(Base, BaseModel):
4213 class DbSession(Base, BaseModel):
4214 __tablename__ = 'db_session'
4214 __tablename__ = 'db_session'
4215 __table_args__ = (
4215 __table_args__ = (
4216 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4216 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4217 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4217 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4218 )
4218 )
4219
4219
4220 def __repr__(self):
4220 def __repr__(self):
4221 return '<DB:DbSession({})>'.format(self.id)
4221 return '<DB:DbSession({})>'.format(self.id)
4222
4222
4223 id = Column('id', Integer())
4223 id = Column('id', Integer())
4224 namespace = Column('namespace', String(255), primary_key=True)
4224 namespace = Column('namespace', String(255), primary_key=True)
4225 accessed = Column('accessed', DateTime, nullable=False)
4225 accessed = Column('accessed', DateTime, nullable=False)
4226 created = Column('created', DateTime, nullable=False)
4226 created = Column('created', DateTime, nullable=False)
4227 data = Column('data', PickleType, nullable=False)
4227 data = Column('data', PickleType, nullable=False)
@@ -1,183 +1,188 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib.auth import _RhodeCodeCryptoBCrypt
24 from rhodecode.lib.auth import _RhodeCodeCryptoBCrypt
25 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
25 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
26 from rhodecode.authentication.plugins.auth_ldap import RhodeCodeAuthPlugin
26 from rhodecode.authentication.plugins.auth_ldap import RhodeCodeAuthPlugin
27 from rhodecode.model import db
27 from rhodecode.model import db
28
28
29
29
30 class TestAuthPlugin(RhodeCodeAuthPluginBase):
31
32 def name(self):
33 return 'stub_auth'
34
30 def test_authenticate_returns_from_auth(stub_auth_data):
35 def test_authenticate_returns_from_auth(stub_auth_data):
31 plugin = RhodeCodeAuthPluginBase('stub_id')
36 plugin = TestAuthPlugin('stub_id')
32 with mock.patch.object(plugin, 'auth') as auth_mock:
37 with mock.patch.object(plugin, 'auth') as auth_mock:
33 auth_mock.return_value = stub_auth_data
38 auth_mock.return_value = stub_auth_data
34 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
39 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
35 assert stub_auth_data == result
40 assert stub_auth_data == result
36
41
37
42
38 def test_authenticate_returns_empty_auth_data():
43 def test_authenticate_returns_empty_auth_data():
39 auth_data = {}
44 auth_data = {}
40 plugin = RhodeCodeAuthPluginBase('stub_id')
45 plugin = TestAuthPlugin('stub_id')
41 with mock.patch.object(plugin, 'auth') as auth_mock:
46 with mock.patch.object(plugin, 'auth') as auth_mock:
42 auth_mock.return_value = auth_data
47 auth_mock.return_value = auth_data
43 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
48 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
44 assert auth_data == result
49 assert auth_data == result
45
50
46
51
47 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
52 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
48 stub_auth_data['_hash_migrate'] = 'new-hash'
53 stub_auth_data['_hash_migrate'] = 'new-hash'
49 plugin = RhodeCodeAuthPluginBase('stub_id')
54 plugin = TestAuthPlugin('stub_id')
50 with mock.patch.object(plugin, 'auth') as auth_mock:
55 with mock.patch.object(plugin, 'auth') as auth_mock:
51 auth_mock.return_value = stub_auth_data
56 auth_mock.return_value = stub_auth_data
52 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
57 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
53
58
54 user = db.User.get_by_username(stub_auth_data['username'])
59 user = db.User.get_by_username(stub_auth_data['username'])
55 assert user.password != 'new-hash'
60 assert user.password != 'new-hash'
56 assert result == stub_auth_data
61 assert result == stub_auth_data
57
62
58
63
59 def test_authenticate_migrates_to_new_hash(stub_auth_data):
64 def test_authenticate_migrates_to_new_hash(stub_auth_data):
60 new_password = b'new-password'
65 new_password = b'new-password'
61 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
66 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
62 stub_auth_data['_hash_migrate'] = new_hash
67 stub_auth_data['_hash_migrate'] = new_hash
63 plugin = RhodeCodeAuthPluginBase('stub_id')
68 plugin = TestAuthPlugin('stub_id')
64 with mock.patch.object(plugin, 'auth') as auth_mock:
69 with mock.patch.object(plugin, 'auth') as auth_mock:
65 auth_mock.return_value = stub_auth_data
70 auth_mock.return_value = stub_auth_data
66 result = plugin._authenticate(
71 result = plugin._authenticate(
67 mock.Mock(), stub_auth_data['username'], new_password, {})
72 mock.Mock(), stub_auth_data['username'], new_password, {})
68
73
69 user = db.User.get_by_username(stub_auth_data['username'])
74 user = db.User.get_by_username(stub_auth_data['username'])
70 assert user.password == new_hash
75 assert user.password == new_hash
71 assert result == stub_auth_data
76 assert result == stub_auth_data
72
77
73
78
74 @pytest.fixture
79 @pytest.fixture
75 def stub_auth_data(user_util):
80 def stub_auth_data(user_util):
76 user = user_util.create_user()
81 user = user_util.create_user()
77 data = {
82 data = {
78 'username': user.username,
83 'username': user.username,
79 'password': 'password',
84 'password': 'password',
80 'email': 'test@example.org',
85 'email': 'test@example.org',
81 'firstname': 'John',
86 'firstname': 'John',
82 'lastname': 'Smith',
87 'lastname': 'Smith',
83 'groups': [],
88 'groups': [],
84 'active': True,
89 'active': True,
85 'admin': False,
90 'admin': False,
86 'extern_name': 'test',
91 'extern_name': 'test',
87 'extern_type': 'ldap',
92 'extern_type': 'ldap',
88 'active_from_extern': True
93 'active_from_extern': True
89 }
94 }
90 return data
95 return data
91
96
92
97
93 class TestRhodeCodeAuthPlugin(object):
98 class TestRhodeCodeAuthPlugin(object):
94 def setup_method(self, method):
99 def setup_method(self, method):
95 self.finalizers = []
100 self.finalizers = []
96 self.user = mock.Mock()
101 self.user = mock.Mock()
97 self.user.username = 'test'
102 self.user.username = 'test'
98 self.user.password = 'old-password'
103 self.user.password = 'old-password'
99 self.fake_auth = {
104 self.fake_auth = {
100 'username': 'test',
105 'username': 'test',
101 'password': 'test',
106 'password': 'test',
102 'email': 'test@example.org',
107 'email': 'test@example.org',
103 'firstname': 'John',
108 'firstname': 'John',
104 'lastname': 'Smith',
109 'lastname': 'Smith',
105 'groups': [],
110 'groups': [],
106 'active': True,
111 'active': True,
107 'admin': False,
112 'admin': False,
108 'extern_name': 'test',
113 'extern_name': 'test',
109 'extern_type': 'ldap',
114 'extern_type': 'ldap',
110 'active_from_extern': True
115 'active_from_extern': True
111 }
116 }
112
117
113 def teardown_method(self, method):
118 def teardown_method(self, method):
114 if self.finalizers:
119 if self.finalizers:
115 for finalizer in self.finalizers:
120 for finalizer in self.finalizers:
116 finalizer()
121 finalizer()
117 self.finalizers = []
122 self.finalizers = []
118
123
119 def test_fake_password_is_created_for_the_new_user(self):
124 def test_fake_password_is_created_for_the_new_user(self):
120 self._patch()
125 self._patch()
121 auth_plugin = RhodeCodeAuthPlugin('stub_id')
126 auth_plugin = RhodeCodeAuthPlugin('stub_id')
122 auth_plugin._authenticate(self.user, 'test', 'test', [])
127 auth_plugin._authenticate(self.user, 'test', 'test', [])
123 self.password_generator_mock.assert_called_once_with(length=16)
128 self.password_generator_mock.assert_called_once_with(length=16)
124 create_user_kwargs = self.create_user_mock.call_args[1]
129 create_user_kwargs = self.create_user_mock.call_args[1]
125 assert create_user_kwargs['password'] == 'new-password'
130 assert create_user_kwargs['password'] == 'new-password'
126
131
127 def test_fake_password_is_not_created_for_the_existing_user(self):
132 def test_fake_password_is_not_created_for_the_existing_user(self):
128 self._patch()
133 self._patch()
129 self.get_user_mock.return_value = self.user
134 self.get_user_mock.return_value = self.user
130 auth_plugin = RhodeCodeAuthPlugin('stub_id')
135 auth_plugin = RhodeCodeAuthPlugin('stub_id')
131 auth_plugin._authenticate(self.user, 'test', 'test', [])
136 auth_plugin._authenticate(self.user, 'test', 'test', [])
132 assert self.password_generator_mock.called is False
137 assert self.password_generator_mock.called is False
133 create_user_kwargs = self.create_user_mock.call_args[1]
138 create_user_kwargs = self.create_user_mock.call_args[1]
134 assert create_user_kwargs['password'] == self.user.password
139 assert create_user_kwargs['password'] == self.user.password
135
140
136 def _patch(self):
141 def _patch(self):
137 get_user_patch = mock.patch('rhodecode.model.db.User.get_by_username')
142 get_user_patch = mock.patch('rhodecode.model.db.User.get_by_username')
138 self.get_user_mock = get_user_patch.start()
143 self.get_user_mock = get_user_patch.start()
139 self.get_user_mock.return_value = None
144 self.get_user_mock.return_value = None
140 self.finalizers.append(get_user_patch.stop)
145 self.finalizers.append(get_user_patch.stop)
141
146
142 create_user_patch = mock.patch(
147 create_user_patch = mock.patch(
143 'rhodecode.model.user.UserModel.create_or_update')
148 'rhodecode.model.user.UserModel.create_or_update')
144 self.create_user_mock = create_user_patch.start()
149 self.create_user_mock = create_user_patch.start()
145 self.create_user_mock.return_value = None
150 self.create_user_mock.return_value = None
146 self.finalizers.append(create_user_patch.stop)
151 self.finalizers.append(create_user_patch.stop)
147
152
148 auth_patch = mock.patch.object(RhodeCodeAuthPlugin, 'auth')
153 auth_patch = mock.patch.object(RhodeCodeAuthPlugin, 'auth')
149 self.auth_mock = auth_patch.start()
154 self.auth_mock = auth_patch.start()
150 self.auth_mock.return_value = self.fake_auth
155 self.auth_mock.return_value = self.fake_auth
151 self.finalizers.append(auth_patch.stop)
156 self.finalizers.append(auth_patch.stop)
152
157
153 password_generator_patch = mock.patch(
158 password_generator_patch = mock.patch(
154 'rhodecode.lib.auth.PasswordGenerator.gen_password')
159 'rhodecode.lib.auth.PasswordGenerator.gen_password')
155 self.password_generator_mock = password_generator_patch.start()
160 self.password_generator_mock = password_generator_patch.start()
156 self.password_generator_mock.return_value = 'new-password'
161 self.password_generator_mock.return_value = 'new-password'
157 self.finalizers.append(password_generator_patch.stop)
162 self.finalizers.append(password_generator_patch.stop)
158
163
159
164
160 def test_missing_ldap():
165 def test_missing_ldap():
161 from rhodecode.model.validators import Missing
166 from rhodecode.model.validators import Missing
162
167
163 try:
168 try:
164 import ldap_not_existing
169 import ldap_not_existing
165 except ImportError:
170 except ImportError:
166 # means that python-ldap is not installed
171 # means that python-ldap is not installed
167 ldap_not_existing = Missing
172 ldap_not_existing = Missing
168
173
169 # missing is singleton
174 # missing is singleton
170 assert ldap_not_existing == Missing
175 assert ldap_not_existing == Missing
171
176
172
177
173 def test_import_ldap():
178 def test_import_ldap():
174 from rhodecode.model.validators import Missing
179 from rhodecode.model.validators import Missing
175
180
176 try:
181 try:
177 import ldap
182 import ldap
178 except ImportError:
183 except ImportError:
179 # means that python-ldap is not installed
184 # means that python-ldap is not installed
180 ldap = Missing
185 ldap = Missing
181
186
182 # missing is singleton
187 # missing is singleton
183 assert False is (ldap == Missing)
188 assert False is (ldap == Missing)
General Comments 0
You need to be logged in to leave comments. Login now