##// END OF EJS Templates
authentication: enabled authentication with auth_token and repository scope....
marcink -
r1510:77606b4c default
parent child Browse files
Show More
@@ -1,649 +1,658 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 logging
26 import logging
27 import time
27 import time
28 import traceback
28 import traceback
29 import warnings
29 import warnings
30
30
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from sqlalchemy.ext.hybrid import hybrid_property
33
33
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.lib import caches
36 from rhodecode.lib import caches
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
38 from rhodecode.lib.utils2 import md5_safe, safe_int
38 from rhodecode.lib.utils2 import md5_safe, safe_int
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.user import UserModel
43 from rhodecode.model.user import UserModel
44 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 # auth types that authenticate() function can receive
49 # auth types that authenticate() function can receive
50 VCS_TYPE = 'vcs'
50 VCS_TYPE = 'vcs'
51 HTTP_TYPE = 'http'
51 HTTP_TYPE = 'http'
52
52
53
53
54 class LazyFormencode(object):
54 class LazyFormencode(object):
55 def __init__(self, formencode_obj, *args, **kwargs):
55 def __init__(self, formencode_obj, *args, **kwargs):
56 self.formencode_obj = formencode_obj
56 self.formencode_obj = formencode_obj
57 self.args = args
57 self.args = args
58 self.kwargs = kwargs
58 self.kwargs = kwargs
59
59
60 def __call__(self, *args, **kwargs):
60 def __call__(self, *args, **kwargs):
61 from inspect import isfunction
61 from inspect import isfunction
62 formencode_obj = self.formencode_obj
62 formencode_obj = self.formencode_obj
63 if isfunction(formencode_obj):
63 if isfunction(formencode_obj):
64 # case we wrap validators into functions
64 # case we wrap validators into functions
65 formencode_obj = self.formencode_obj(*args, **kwargs)
65 formencode_obj = self.formencode_obj(*args, **kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
67
67
68
68
69 class RhodeCodeAuthPluginBase(object):
69 class RhodeCodeAuthPluginBase(object):
70 # cache the authentication request for N amount of seconds. Some kind
70 # cache the authentication request for N amount of seconds. Some kind
71 # of authentication methods are very heavy and it's very efficient to cache
71 # of authentication methods are very heavy and it's very efficient to cache
72 # the result of a call. If it's set to None (default) cache is off
72 # the result of a call. If it's set to None (default) cache is off
73 AUTH_CACHE_TTL = None
73 AUTH_CACHE_TTL = None
74 AUTH_CACHE = {}
74 AUTH_CACHE = {}
75
75
76 auth_func_attrs = {
76 auth_func_attrs = {
77 "username": "unique username",
77 "username": "unique username",
78 "firstname": "first name",
78 "firstname": "first name",
79 "lastname": "last name",
79 "lastname": "last name",
80 "email": "email address",
80 "email": "email address",
81 "groups": '["list", "of", "groups"]',
81 "groups": '["list", "of", "groups"]',
82 "extern_name": "name in external source of record",
82 "extern_name": "name in external source of record",
83 "extern_type": "type of external source of record",
83 "extern_type": "type of external source of record",
84 "admin": 'True|False defines if user should be RhodeCode super admin',
84 "admin": 'True|False defines if user should be RhodeCode super admin',
85 "active":
85 "active":
86 'True|False defines active state of user internally for RhodeCode',
86 'True|False defines active state of user internally for RhodeCode',
87 "active_from_extern":
87 "active_from_extern":
88 "True|False\None, active state from the external auth, "
88 "True|False\None, active state from the external auth, "
89 "None means use definition from RhodeCode extern_type active value"
89 "None means use definition from RhodeCode extern_type active value"
90 }
90 }
91 # set on authenticate() method and via set_auth_type func.
91 # set on authenticate() method and via set_auth_type func.
92 auth_type = None
92 auth_type = None
93
93
94 # set on authenticate() method and via set_calling_scope_repo, this is a
95 # calling scope repository when doing authentication most likely on VCS
96 # operations
97 acl_repo_name = None
98
94 # List of setting names to store encrypted. Plugins may override this list
99 # List of setting names to store encrypted. Plugins may override this list
95 # to store settings encrypted.
100 # to store settings encrypted.
96 _settings_encrypted = []
101 _settings_encrypted = []
97
102
98 # Mapping of python to DB settings model types. Plugins may override or
103 # Mapping of python to DB settings model types. Plugins may override or
99 # extend this mapping.
104 # extend this mapping.
100 _settings_type_map = {
105 _settings_type_map = {
101 colander.String: 'unicode',
106 colander.String: 'unicode',
102 colander.Integer: 'int',
107 colander.Integer: 'int',
103 colander.Boolean: 'bool',
108 colander.Boolean: 'bool',
104 colander.List: 'list',
109 colander.List: 'list',
105 }
110 }
106
111
107 def __init__(self, plugin_id):
112 def __init__(self, plugin_id):
108 self._plugin_id = plugin_id
113 self._plugin_id = plugin_id
109
114
110 def __str__(self):
115 def __str__(self):
111 return self.get_id()
116 return self.get_id()
112
117
113 def _get_setting_full_name(self, name):
118 def _get_setting_full_name(self, name):
114 """
119 """
115 Return the full setting name used for storing values in the database.
120 Return the full setting name used for storing values in the database.
116 """
121 """
117 # TODO: johbo: Using the name here is problematic. It would be good to
122 # TODO: johbo: Using the name here is problematic. It would be good to
118 # introduce either new models in the database to hold Plugin and
123 # introduce either new models in the database to hold Plugin and
119 # PluginSetting or to use the plugin id here.
124 # PluginSetting or to use the plugin id here.
120 return 'auth_{}_{}'.format(self.name, name)
125 return 'auth_{}_{}'.format(self.name, name)
121
126
122 def _get_setting_type(self, name):
127 def _get_setting_type(self, name):
123 """
128 """
124 Return the type of a setting. This type is defined by the SettingsModel
129 Return the type of a setting. This type is defined by the SettingsModel
125 and determines how the setting is stored in DB. Optionally the suffix
130 and determines how the setting is stored in DB. Optionally the suffix
126 `.encrypted` is appended to instruct SettingsModel to store it
131 `.encrypted` is appended to instruct SettingsModel to store it
127 encrypted.
132 encrypted.
128 """
133 """
129 schema_node = self.get_settings_schema().get(name)
134 schema_node = self.get_settings_schema().get(name)
130 db_type = self._settings_type_map.get(
135 db_type = self._settings_type_map.get(
131 type(schema_node.typ), 'unicode')
136 type(schema_node.typ), 'unicode')
132 if name in self._settings_encrypted:
137 if name in self._settings_encrypted:
133 db_type = '{}.encrypted'.format(db_type)
138 db_type = '{}.encrypted'.format(db_type)
134 return db_type
139 return db_type
135
140
136 def is_enabled(self):
141 def is_enabled(self):
137 """
142 """
138 Returns true if this plugin is enabled. An enabled plugin can be
143 Returns true if this plugin is enabled. An enabled plugin can be
139 configured in the admin interface but it is not consulted during
144 configured in the admin interface but it is not consulted during
140 authentication.
145 authentication.
141 """
146 """
142 auth_plugins = SettingsModel().get_auth_plugins()
147 auth_plugins = SettingsModel().get_auth_plugins()
143 return self.get_id() in auth_plugins
148 return self.get_id() in auth_plugins
144
149
145 def is_active(self):
150 def is_active(self):
146 """
151 """
147 Returns true if the plugin is activated. An activated plugin is
152 Returns true if the plugin is activated. An activated plugin is
148 consulted during authentication, assumed it is also enabled.
153 consulted during authentication, assumed it is also enabled.
149 """
154 """
150 return self.get_setting_by_name('enabled')
155 return self.get_setting_by_name('enabled')
151
156
152 def get_id(self):
157 def get_id(self):
153 """
158 """
154 Returns the plugin id.
159 Returns the plugin id.
155 """
160 """
156 return self._plugin_id
161 return self._plugin_id
157
162
158 def get_display_name(self):
163 def get_display_name(self):
159 """
164 """
160 Returns a translation string for displaying purposes.
165 Returns a translation string for displaying purposes.
161 """
166 """
162 raise NotImplementedError('Not implemented in base class')
167 raise NotImplementedError('Not implemented in base class')
163
168
164 def get_settings_schema(self):
169 def get_settings_schema(self):
165 """
170 """
166 Returns a colander schema, representing the plugin settings.
171 Returns a colander schema, representing the plugin settings.
167 """
172 """
168 return AuthnPluginSettingsSchemaBase()
173 return AuthnPluginSettingsSchemaBase()
169
174
170 def get_setting_by_name(self, name, default=None):
175 def get_setting_by_name(self, name, default=None):
171 """
176 """
172 Returns a plugin setting by name.
177 Returns a plugin setting by name.
173 """
178 """
174 full_name = self._get_setting_full_name(name)
179 full_name = self._get_setting_full_name(name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
180 db_setting = SettingsModel().get_setting_by_name(full_name)
176 return db_setting.app_settings_value if db_setting else default
181 return db_setting.app_settings_value if db_setting else default
177
182
178 def create_or_update_setting(self, name, value):
183 def create_or_update_setting(self, name, value):
179 """
184 """
180 Create or update a setting for this plugin in the persistent storage.
185 Create or update a setting for this plugin in the persistent storage.
181 """
186 """
182 full_name = self._get_setting_full_name(name)
187 full_name = self._get_setting_full_name(name)
183 type_ = self._get_setting_type(name)
188 type_ = self._get_setting_type(name)
184 db_setting = SettingsModel().create_or_update_setting(
189 db_setting = SettingsModel().create_or_update_setting(
185 full_name, value, type_)
190 full_name, value, type_)
186 return db_setting.app_settings_value
191 return db_setting.app_settings_value
187
192
188 def get_settings(self):
193 def get_settings(self):
189 """
194 """
190 Returns the plugin settings as dictionary.
195 Returns the plugin settings as dictionary.
191 """
196 """
192 settings = {}
197 settings = {}
193 for node in self.get_settings_schema():
198 for node in self.get_settings_schema():
194 settings[node.name] = self.get_setting_by_name(node.name)
199 settings[node.name] = self.get_setting_by_name(node.name)
195 return settings
200 return settings
196
201
197 @property
202 @property
198 def validators(self):
203 def validators(self):
199 """
204 """
200 Exposes RhodeCode validators modules
205 Exposes RhodeCode validators modules
201 """
206 """
202 # this is a hack to overcome issues with pylons threadlocals and
207 # this is a hack to overcome issues with pylons threadlocals and
203 # translator object _() not beein registered properly.
208 # translator object _() not beein registered properly.
204 class LazyCaller(object):
209 class LazyCaller(object):
205 def __init__(self, name):
210 def __init__(self, name):
206 self.validator_name = name
211 self.validator_name = name
207
212
208 def __call__(self, *args, **kwargs):
213 def __call__(self, *args, **kwargs):
209 from rhodecode.model import validators as v
214 from rhodecode.model import validators as v
210 obj = getattr(v, self.validator_name)
215 obj = getattr(v, self.validator_name)
211 # log.debug('Initializing lazy formencode object: %s', obj)
216 # log.debug('Initializing lazy formencode object: %s', obj)
212 return LazyFormencode(obj, *args, **kwargs)
217 return LazyFormencode(obj, *args, **kwargs)
213
218
214 class ProxyGet(object):
219 class ProxyGet(object):
215 def __getattribute__(self, name):
220 def __getattribute__(self, name):
216 return LazyCaller(name)
221 return LazyCaller(name)
217
222
218 return ProxyGet()
223 return ProxyGet()
219
224
220 @hybrid_property
225 @hybrid_property
221 def name(self):
226 def name(self):
222 """
227 """
223 Returns the name of this authentication plugin.
228 Returns the name of this authentication plugin.
224
229
225 :returns: string
230 :returns: string
226 """
231 """
227 raise NotImplementedError("Not implemented in base class")
232 raise NotImplementedError("Not implemented in base class")
228
233
229 def get_url_slug(self):
234 def get_url_slug(self):
230 """
235 """
231 Returns a slug which should be used when constructing URLs which refer
236 Returns a slug which should be used when constructing URLs which refer
232 to this plugin. By default it returns the plugin name. If the name is
237 to this plugin. By default it returns the plugin name. If the name is
233 not suitable for using it in an URL the plugin should override this
238 not suitable for using it in an URL the plugin should override this
234 method.
239 method.
235 """
240 """
236 return self.name
241 return self.name
237
242
238 @property
243 @property
239 def is_headers_auth(self):
244 def is_headers_auth(self):
240 """
245 """
241 Returns True if this authentication plugin uses HTTP headers as
246 Returns True if this authentication plugin uses HTTP headers as
242 authentication method.
247 authentication method.
243 """
248 """
244 return False
249 return False
245
250
246 @hybrid_property
251 @hybrid_property
247 def is_container_auth(self):
252 def is_container_auth(self):
248 """
253 """
249 Deprecated method that indicates if this authentication plugin uses
254 Deprecated method that indicates if this authentication plugin uses
250 HTTP headers as authentication method.
255 HTTP headers as authentication method.
251 """
256 """
252 warnings.warn(
257 warnings.warn(
253 'Use is_headers_auth instead.', category=DeprecationWarning)
258 'Use is_headers_auth instead.', category=DeprecationWarning)
254 return self.is_headers_auth
259 return self.is_headers_auth
255
260
256 @hybrid_property
261 @hybrid_property
257 def allows_creating_users(self):
262 def allows_creating_users(self):
258 """
263 """
259 Defines if Plugin allows users to be created on-the-fly when
264 Defines if Plugin allows users to be created on-the-fly when
260 authentication is called. Controls how external plugins should behave
265 authentication is called. Controls how external plugins should behave
261 in terms if they are allowed to create new users, or not. Base plugins
266 in terms if they are allowed to create new users, or not. Base plugins
262 should not be allowed to, but External ones should be !
267 should not be allowed to, but External ones should be !
263
268
264 :return: bool
269 :return: bool
265 """
270 """
266 return False
271 return False
267
272
268 def set_auth_type(self, auth_type):
273 def set_auth_type(self, auth_type):
269 self.auth_type = auth_type
274 self.auth_type = auth_type
270
275
276 def set_calling_scope_repo(self, acl_repo_name):
277 self.acl_repo_name = acl_repo_name
278
271 def allows_authentication_from(
279 def allows_authentication_from(
272 self, user, allows_non_existing_user=True,
280 self, user, allows_non_existing_user=True,
273 allowed_auth_plugins=None, allowed_auth_sources=None):
281 allowed_auth_plugins=None, allowed_auth_sources=None):
274 """
282 """
275 Checks if this authentication module should accept a request for
283 Checks if this authentication module should accept a request for
276 the current user.
284 the current user.
277
285
278 :param user: user object fetched using plugin's get_user() method.
286 :param user: user object fetched using plugin's get_user() method.
279 :param allows_non_existing_user: if True, don't allow the
287 :param allows_non_existing_user: if True, don't allow the
280 user to be empty, meaning not existing in our database
288 user to be empty, meaning not existing in our database
281 :param allowed_auth_plugins: if provided, users extern_type will be
289 :param allowed_auth_plugins: if provided, users extern_type will be
282 checked against a list of provided extern types, which are plugin
290 checked against a list of provided extern types, which are plugin
283 auth_names in the end
291 auth_names in the end
284 :param allowed_auth_sources: authentication type allowed,
292 :param allowed_auth_sources: authentication type allowed,
285 `http` or `vcs` default is both.
293 `http` or `vcs` default is both.
286 defines if plugin will accept only http authentication vcs
294 defines if plugin will accept only http authentication vcs
287 authentication(git/hg) or both
295 authentication(git/hg) or both
288 :returns: boolean
296 :returns: boolean
289 """
297 """
290 if not user and not allows_non_existing_user:
298 if not user and not allows_non_existing_user:
291 log.debug('User is empty but plugin does not allow empty users,'
299 log.debug('User is empty but plugin does not allow empty users,'
292 'not allowed to authenticate')
300 'not allowed to authenticate')
293 return False
301 return False
294
302
295 expected_auth_plugins = allowed_auth_plugins or [self.name]
303 expected_auth_plugins = allowed_auth_plugins or [self.name]
296 if user and (user.extern_type and
304 if user and (user.extern_type and
297 user.extern_type not in expected_auth_plugins):
305 user.extern_type not in expected_auth_plugins):
298 log.debug(
306 log.debug(
299 'User `%s` is bound to `%s` auth type. Plugin allows only '
307 'User `%s` is bound to `%s` auth type. Plugin allows only '
300 '%s, skipping', user, user.extern_type, expected_auth_plugins)
308 '%s, skipping', user, user.extern_type, expected_auth_plugins)
301
309
302 return False
310 return False
303
311
304 # by default accept both
312 # by default accept both
305 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
313 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
306 if self.auth_type not in expected_auth_from:
314 if self.auth_type not in expected_auth_from:
307 log.debug('Current auth source is %s but plugin only allows %s',
315 log.debug('Current auth source is %s but plugin only allows %s',
308 self.auth_type, expected_auth_from)
316 self.auth_type, expected_auth_from)
309 return False
317 return False
310
318
311 return True
319 return True
312
320
313 def get_user(self, username=None, **kwargs):
321 def get_user(self, username=None, **kwargs):
314 """
322 """
315 Helper method for user fetching in plugins, by default it's using
323 Helper method for user fetching in plugins, by default it's using
316 simple fetch by username, but this method can be custimized in plugins
324 simple fetch by username, but this method can be custimized in plugins
317 eg. headers auth plugin to fetch user by environ params
325 eg. headers auth plugin to fetch user by environ params
318
326
319 :param username: username if given to fetch from database
327 :param username: username if given to fetch from database
320 :param kwargs: extra arguments needed for user fetching.
328 :param kwargs: extra arguments needed for user fetching.
321 """
329 """
322 user = None
330 user = None
323 log.debug(
331 log.debug(
324 'Trying to fetch user `%s` from RhodeCode database', username)
332 'Trying to fetch user `%s` from RhodeCode database', username)
325 if username:
333 if username:
326 user = User.get_by_username(username)
334 user = User.get_by_username(username)
327 if not user:
335 if not user:
328 log.debug('User not found, fallback to fetch user in '
336 log.debug('User not found, fallback to fetch user in '
329 'case insensitive mode')
337 'case insensitive mode')
330 user = User.get_by_username(username, case_insensitive=True)
338 user = User.get_by_username(username, case_insensitive=True)
331 else:
339 else:
332 log.debug('provided username:`%s` is empty skipping...', username)
340 log.debug('provided username:`%s` is empty skipping...', username)
333 if not user:
341 if not user:
334 log.debug('User `%s` not found in database', username)
342 log.debug('User `%s` not found in database', username)
335 else:
343 else:
336 log.debug('Got DB user:%s', user)
344 log.debug('Got DB user:%s', user)
337 return user
345 return user
338
346
339 def user_activation_state(self):
347 def user_activation_state(self):
340 """
348 """
341 Defines user activation state when creating new users
349 Defines user activation state when creating new users
342
350
343 :returns: boolean
351 :returns: boolean
344 """
352 """
345 raise NotImplementedError("Not implemented in base class")
353 raise NotImplementedError("Not implemented in base class")
346
354
347 def auth(self, userobj, username, passwd, settings, **kwargs):
355 def auth(self, userobj, username, passwd, settings, **kwargs):
348 """
356 """
349 Given a user object (which may be null), username, a plaintext
357 Given a user object (which may be null), username, a plaintext
350 password, and a settings object (containing all the keys needed as
358 password, and a settings object (containing all the keys needed as
351 listed in settings()), authenticate this user's login attempt.
359 listed in settings()), authenticate this user's login attempt.
352
360
353 Return None on failure. On success, return a dictionary of the form:
361 Return None on failure. On success, return a dictionary of the form:
354
362
355 see: RhodeCodeAuthPluginBase.auth_func_attrs
363 see: RhodeCodeAuthPluginBase.auth_func_attrs
356 This is later validated for correctness
364 This is later validated for correctness
357 """
365 """
358 raise NotImplementedError("not implemented in base class")
366 raise NotImplementedError("not implemented in base class")
359
367
360 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
368 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
361 """
369 """
362 Wrapper to call self.auth() that validates call on it
370 Wrapper to call self.auth() that validates call on it
363
371
364 :param userobj: userobj
372 :param userobj: userobj
365 :param username: username
373 :param username: username
366 :param passwd: plaintext password
374 :param passwd: plaintext password
367 :param settings: plugin settings
375 :param settings: plugin settings
368 """
376 """
369 auth = self.auth(userobj, username, passwd, settings, **kwargs)
377 auth = self.auth(userobj, username, passwd, settings, **kwargs)
370 if auth:
378 if auth:
371 # check if hash should be migrated ?
379 # check if hash should be migrated ?
372 new_hash = auth.get('_hash_migrate')
380 new_hash = auth.get('_hash_migrate')
373 if new_hash:
381 if new_hash:
374 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
382 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
375 return self._validate_auth_return(auth)
383 return self._validate_auth_return(auth)
376 return auth
384 return auth
377
385
378 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
386 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
379 new_hash_cypher = _RhodeCodeCryptoBCrypt()
387 new_hash_cypher = _RhodeCodeCryptoBCrypt()
380 # extra checks, so make sure new hash is correct.
388 # extra checks, so make sure new hash is correct.
381 password_encoded = safe_str(password)
389 password_encoded = safe_str(password)
382 if new_hash and new_hash_cypher.hash_check(
390 if new_hash and new_hash_cypher.hash_check(
383 password_encoded, new_hash):
391 password_encoded, new_hash):
384 cur_user = User.get_by_username(username)
392 cur_user = User.get_by_username(username)
385 cur_user.password = new_hash
393 cur_user.password = new_hash
386 Session().add(cur_user)
394 Session().add(cur_user)
387 Session().flush()
395 Session().flush()
388 log.info('Migrated user %s hash to bcrypt', cur_user)
396 log.info('Migrated user %s hash to bcrypt', cur_user)
389
397
390 def _validate_auth_return(self, ret):
398 def _validate_auth_return(self, ret):
391 if not isinstance(ret, dict):
399 if not isinstance(ret, dict):
392 raise Exception('returned value from auth must be a dict')
400 raise Exception('returned value from auth must be a dict')
393 for k in self.auth_func_attrs:
401 for k in self.auth_func_attrs:
394 if k not in ret:
402 if k not in ret:
395 raise Exception('Missing %s attribute from returned data' % k)
403 raise Exception('Missing %s attribute from returned data' % k)
396 return ret
404 return ret
397
405
398
406
399 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
407 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
400
408
401 @hybrid_property
409 @hybrid_property
402 def allows_creating_users(self):
410 def allows_creating_users(self):
403 return True
411 return True
404
412
405 def use_fake_password(self):
413 def use_fake_password(self):
406 """
414 """
407 Return a boolean that indicates whether or not we should set the user's
415 Return a boolean that indicates whether or not we should set the user's
408 password to a random value when it is authenticated by this plugin.
416 password to a random value when it is authenticated by this plugin.
409 If your plugin provides authentication, then you will generally
417 If your plugin provides authentication, then you will generally
410 want this.
418 want this.
411
419
412 :returns: boolean
420 :returns: boolean
413 """
421 """
414 raise NotImplementedError("Not implemented in base class")
422 raise NotImplementedError("Not implemented in base class")
415
423
416 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
424 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
417 # at this point _authenticate calls plugin's `auth()` function
425 # at this point _authenticate calls plugin's `auth()` function
418 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
426 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
419 userobj, username, passwd, settings, **kwargs)
427 userobj, username, passwd, settings, **kwargs)
420 if auth:
428 if auth:
421 # maybe plugin will clean the username ?
429 # maybe plugin will clean the username ?
422 # we should use the return value
430 # we should use the return value
423 username = auth['username']
431 username = auth['username']
424
432
425 # if external source tells us that user is not active, we should
433 # if external source tells us that user is not active, we should
426 # skip rest of the process. This can prevent from creating users in
434 # skip rest of the process. This can prevent from creating users in
427 # RhodeCode when using external authentication, but if it's
435 # RhodeCode when using external authentication, but if it's
428 # inactive user we shouldn't create that user anyway
436 # inactive user we shouldn't create that user anyway
429 if auth['active_from_extern'] is False:
437 if auth['active_from_extern'] is False:
430 log.warning(
438 log.warning(
431 "User %s authenticated against %s, but is inactive",
439 "User %s authenticated against %s, but is inactive",
432 username, self.__module__)
440 username, self.__module__)
433 return None
441 return None
434
442
435 cur_user = User.get_by_username(username, case_insensitive=True)
443 cur_user = User.get_by_username(username, case_insensitive=True)
436 is_user_existing = cur_user is not None
444 is_user_existing = cur_user is not None
437
445
438 if is_user_existing:
446 if is_user_existing:
439 log.debug('Syncing user `%s` from '
447 log.debug('Syncing user `%s` from '
440 '`%s` plugin', username, self.name)
448 '`%s` plugin', username, self.name)
441 else:
449 else:
442 log.debug('Creating non existing user `%s` from '
450 log.debug('Creating non existing user `%s` from '
443 '`%s` plugin', username, self.name)
451 '`%s` plugin', username, self.name)
444
452
445 if self.allows_creating_users:
453 if self.allows_creating_users:
446 log.debug('Plugin `%s` allows to '
454 log.debug('Plugin `%s` allows to '
447 'create new users', self.name)
455 'create new users', self.name)
448 else:
456 else:
449 log.debug('Plugin `%s` does not allow to '
457 log.debug('Plugin `%s` does not allow to '
450 'create new users', self.name)
458 'create new users', self.name)
451
459
452 user_parameters = {
460 user_parameters = {
453 'username': username,
461 'username': username,
454 'email': auth["email"],
462 'email': auth["email"],
455 'firstname': auth["firstname"],
463 'firstname': auth["firstname"],
456 'lastname': auth["lastname"],
464 'lastname': auth["lastname"],
457 'active': auth["active"],
465 'active': auth["active"],
458 'admin': auth["admin"],
466 'admin': auth["admin"],
459 'extern_name': auth["extern_name"],
467 'extern_name': auth["extern_name"],
460 'extern_type': self.name,
468 'extern_type': self.name,
461 'plugin': self,
469 'plugin': self,
462 'allow_to_create_user': self.allows_creating_users,
470 'allow_to_create_user': self.allows_creating_users,
463 }
471 }
464
472
465 if not is_user_existing:
473 if not is_user_existing:
466 if self.use_fake_password():
474 if self.use_fake_password():
467 # Randomize the PW because we don't need it, but don't want
475 # Randomize the PW because we don't need it, but don't want
468 # them blank either
476 # them blank either
469 passwd = PasswordGenerator().gen_password(length=16)
477 passwd = PasswordGenerator().gen_password(length=16)
470 user_parameters['password'] = passwd
478 user_parameters['password'] = passwd
471 else:
479 else:
472 # Since the password is required by create_or_update method of
480 # Since the password is required by create_or_update method of
473 # UserModel, we need to set it explicitly.
481 # UserModel, we need to set it explicitly.
474 # The create_or_update method is smart and recognises the
482 # The create_or_update method is smart and recognises the
475 # password hashes as well.
483 # password hashes as well.
476 user_parameters['password'] = cur_user.password
484 user_parameters['password'] = cur_user.password
477
485
478 # we either create or update users, we also pass the flag
486 # we either create or update users, we also pass the flag
479 # that controls if this method can actually do that.
487 # that controls if this method can actually do that.
480 # raises NotAllowedToCreateUserError if it cannot, and we try to.
488 # raises NotAllowedToCreateUserError if it cannot, and we try to.
481 user = UserModel().create_or_update(**user_parameters)
489 user = UserModel().create_or_update(**user_parameters)
482 Session().flush()
490 Session().flush()
483 # enforce user is just in given groups, all of them has to be ones
491 # enforce user is just in given groups, all of them has to be ones
484 # created from plugins. We store this info in _group_data JSON
492 # created from plugins. We store this info in _group_data JSON
485 # field
493 # field
486 try:
494 try:
487 groups = auth['groups'] or []
495 groups = auth['groups'] or []
488 UserGroupModel().enforce_groups(user, groups, self.name)
496 UserGroupModel().enforce_groups(user, groups, self.name)
489 except Exception:
497 except Exception:
490 # for any reason group syncing fails, we should
498 # for any reason group syncing fails, we should
491 # proceed with login
499 # proceed with login
492 log.error(traceback.format_exc())
500 log.error(traceback.format_exc())
493 Session().commit()
501 Session().commit()
494 return auth
502 return auth
495
503
496
504
497 def loadplugin(plugin_id):
505 def loadplugin(plugin_id):
498 """
506 """
499 Loads and returns an instantiated authentication plugin.
507 Loads and returns an instantiated authentication plugin.
500 Returns the RhodeCodeAuthPluginBase subclass on success,
508 Returns the RhodeCodeAuthPluginBase subclass on success,
501 or None on failure.
509 or None on failure.
502 """
510 """
503 # TODO: Disusing pyramids thread locals to retrieve the registry.
511 # TODO: Disusing pyramids thread locals to retrieve the registry.
504 authn_registry = get_authn_registry()
512 authn_registry = get_authn_registry()
505 plugin = authn_registry.get_plugin(plugin_id)
513 plugin = authn_registry.get_plugin(plugin_id)
506 if plugin is None:
514 if plugin is None:
507 log.error('Authentication plugin not found: "%s"', plugin_id)
515 log.error('Authentication plugin not found: "%s"', plugin_id)
508 return plugin
516 return plugin
509
517
510
518
511 def get_authn_registry(registry=None):
519 def get_authn_registry(registry=None):
512 registry = registry or get_current_registry()
520 registry = registry or get_current_registry()
513 authn_registry = registry.getUtility(IAuthnPluginRegistry)
521 authn_registry = registry.getUtility(IAuthnPluginRegistry)
514 return authn_registry
522 return authn_registry
515
523
516
524
517 def get_auth_cache_manager(custom_ttl=None):
525 def get_auth_cache_manager(custom_ttl=None):
518 return caches.get_cache_manager(
526 return caches.get_cache_manager(
519 'auth_plugins', 'rhodecode.authentication', custom_ttl)
527 'auth_plugins', 'rhodecode.authentication', custom_ttl)
520
528
521
529
522 def authenticate(username, password, environ=None, auth_type=None,
530 def authenticate(username, password, environ=None, auth_type=None,
523 skip_missing=False, registry=None):
531 skip_missing=False, registry=None, acl_repo_name=None):
524 """
532 """
525 Authentication function used for access control,
533 Authentication function used for access control,
526 It tries to authenticate based on enabled authentication modules.
534 It tries to authenticate based on enabled authentication modules.
527
535
528 :param username: username can be empty for headers auth
536 :param username: username can be empty for headers auth
529 :param password: password can be empty for headers auth
537 :param password: password can be empty for headers auth
530 :param environ: environ headers passed for headers auth
538 :param environ: environ headers passed for headers auth
531 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
539 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
532 :param skip_missing: ignores plugins that are in db but not in environment
540 :param skip_missing: ignores plugins that are in db but not in environment
533 :returns: None if auth failed, plugin_user dict if auth is correct
541 :returns: None if auth failed, plugin_user dict if auth is correct
534 """
542 """
535 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
543 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
536 raise ValueError('auth type must be on of http, vcs got "%s" instead'
544 raise ValueError('auth type must be on of http, vcs got "%s" instead'
537 % auth_type)
545 % auth_type)
538 headers_only = environ and not (username and password)
546 headers_only = environ and not (username and password)
539
547
540 authn_registry = get_authn_registry(registry)
548 authn_registry = get_authn_registry(registry)
541 for plugin in authn_registry.get_plugins_for_authentication():
549 for plugin in authn_registry.get_plugins_for_authentication():
542 plugin.set_auth_type(auth_type)
550 plugin.set_auth_type(auth_type)
551 plugin.set_calling_scope_repo(acl_repo_name)
543 user = plugin.get_user(username)
552 user = plugin.get_user(username)
544 display_user = user.username if user else username
553 display_user = user.username if user else username
545
554
546 if headers_only and not plugin.is_headers_auth:
555 if headers_only and not plugin.is_headers_auth:
547 log.debug('Auth type is for headers only and plugin `%s` is not '
556 log.debug('Auth type is for headers only and plugin `%s` is not '
548 'headers plugin, skipping...', plugin.get_id())
557 'headers plugin, skipping...', plugin.get_id())
549 continue
558 continue
550
559
551 # load plugin settings from RhodeCode database
560 # load plugin settings from RhodeCode database
552 plugin_settings = plugin.get_settings()
561 plugin_settings = plugin.get_settings()
553 log.debug('Plugin settings:%s', plugin_settings)
562 log.debug('Plugin settings:%s', plugin_settings)
554
563
555 log.debug('Trying authentication using ** %s **', plugin.get_id())
564 log.debug('Trying authentication using ** %s **', plugin.get_id())
556 # use plugin's method of user extraction.
565 # use plugin's method of user extraction.
557 user = plugin.get_user(username, environ=environ,
566 user = plugin.get_user(username, environ=environ,
558 settings=plugin_settings)
567 settings=plugin_settings)
559 display_user = user.username if user else username
568 display_user = user.username if user else username
560 log.debug(
569 log.debug(
561 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
570 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
562
571
563 if not plugin.allows_authentication_from(user):
572 if not plugin.allows_authentication_from(user):
564 log.debug('Plugin %s does not accept user `%s` for authentication',
573 log.debug('Plugin %s does not accept user `%s` for authentication',
565 plugin.get_id(), display_user)
574 plugin.get_id(), display_user)
566 continue
575 continue
567 else:
576 else:
568 log.debug('Plugin %s accepted user `%s` for authentication',
577 log.debug('Plugin %s accepted user `%s` for authentication',
569 plugin.get_id(), display_user)
578 plugin.get_id(), display_user)
570
579
571 log.info('Authenticating user `%s` using %s plugin',
580 log.info('Authenticating user `%s` using %s plugin',
572 display_user, plugin.get_id())
581 display_user, plugin.get_id())
573
582
574 _cache_ttl = 0
583 _cache_ttl = 0
575
584
576 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
585 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
577 # plugin cache set inside is more important than the settings value
586 # plugin cache set inside is more important than the settings value
578 _cache_ttl = plugin.AUTH_CACHE_TTL
587 _cache_ttl = plugin.AUTH_CACHE_TTL
579 elif plugin_settings.get('cache_ttl'):
588 elif plugin_settings.get('cache_ttl'):
580 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
589 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
581
590
582 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
591 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
583
592
584 # get instance of cache manager configured for a namespace
593 # get instance of cache manager configured for a namespace
585 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
594 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
586
595
587 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
596 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
588 plugin.get_id(), plugin_cache_active, _cache_ttl)
597 plugin.get_id(), plugin_cache_active, _cache_ttl)
589
598
590 # for environ based password can be empty, but then the validation is
599 # for environ based password can be empty, but then the validation is
591 # on the server that fills in the env data needed for authentication
600 # on the server that fills in the env data needed for authentication
592 _password_hash = md5_safe(plugin.name + username + (password or ''))
601 _password_hash = md5_safe(plugin.name + username + (password or ''))
593
602
594 # _authenticate is a wrapper for .auth() method of plugin.
603 # _authenticate is a wrapper for .auth() method of plugin.
595 # it checks if .auth() sends proper data.
604 # it checks if .auth() sends proper data.
596 # For RhodeCodeExternalAuthPlugin it also maps users to
605 # For RhodeCodeExternalAuthPlugin it also maps users to
597 # Database and maps the attributes returned from .auth()
606 # Database and maps the attributes returned from .auth()
598 # to RhodeCode database. If this function returns data
607 # to RhodeCode database. If this function returns data
599 # then auth is correct.
608 # then auth is correct.
600 start = time.time()
609 start = time.time()
601 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
610 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
602
611
603 def auth_func():
612 def auth_func():
604 """
613 """
605 This function is used internally in Cache of Beaker to calculate
614 This function is used internally in Cache of Beaker to calculate
606 Results
615 Results
607 """
616 """
608 return plugin._authenticate(
617 return plugin._authenticate(
609 user, username, password, plugin_settings,
618 user, username, password, plugin_settings,
610 environ=environ or {})
619 environ=environ or {})
611
620
612 if plugin_cache_active:
621 if plugin_cache_active:
613 plugin_user = cache_manager.get(
622 plugin_user = cache_manager.get(
614 _password_hash, createfunc=auth_func)
623 _password_hash, createfunc=auth_func)
615 else:
624 else:
616 plugin_user = auth_func()
625 plugin_user = auth_func()
617
626
618 auth_time = time.time() - start
627 auth_time = time.time() - start
619 log.debug('Authentication for plugin `%s` completed in %.3fs, '
628 log.debug('Authentication for plugin `%s` completed in %.3fs, '
620 'expiration time of fetched cache %.1fs.',
629 'expiration time of fetched cache %.1fs.',
621 plugin.get_id(), auth_time, _cache_ttl)
630 plugin.get_id(), auth_time, _cache_ttl)
622
631
623 log.debug('PLUGIN USER DATA: %s', plugin_user)
632 log.debug('PLUGIN USER DATA: %s', plugin_user)
624
633
625 if plugin_user:
634 if plugin_user:
626 log.debug('Plugin returned proper authentication data')
635 log.debug('Plugin returned proper authentication data')
627 return plugin_user
636 return plugin_user
628 # we failed to Auth because .auth() method didn't return proper user
637 # we failed to Auth because .auth() method didn't return proper user
629 log.debug("User `%s` failed to authenticate against %s",
638 log.debug("User `%s` failed to authenticate against %s",
630 display_user, plugin.get_id())
639 display_user, plugin.get_id())
631 return None
640 return None
632
641
633
642
634 def chop_at(s, sub, inclusive=False):
643 def chop_at(s, sub, inclusive=False):
635 """Truncate string ``s`` at the first occurrence of ``sub``.
644 """Truncate string ``s`` at the first occurrence of ``sub``.
636
645
637 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
646 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
638
647
639 >>> chop_at("plutocratic brats", "rat")
648 >>> chop_at("plutocratic brats", "rat")
640 'plutoc'
649 'plutoc'
641 >>> chop_at("plutocratic brats", "rat", True)
650 >>> chop_at("plutocratic brats", "rat", True)
642 'plutocrat'
651 'plutocrat'
643 """
652 """
644 pos = s.find(sub)
653 pos = s.find(sub)
645 if pos == -1:
654 if pos == -1:
646 return s
655 return s
647 if inclusive:
656 if inclusive:
648 return s[:pos+len(sub)]
657 return s[:pos+len(sub)]
649 return s[:pos]
658 return s[:pos]
@@ -1,139 +1,146 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-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 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys
31 from rhodecode.model.db import User, UserApiKeys, Repository
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @hybrid_property
72 @hybrid_property
73 def name(self):
73 def name(self):
74 return "authtoken"
74 return "authtoken"
75
75
76 def user_activation_state(self):
76 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
78 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
79
79
80 def allows_authentication_from(
80 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
83 """
84 Custom method for this auth that doesn't accept empty users. And also
84 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
85 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
87 """
87 """
88 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
90
90
91 active_plugins = set(
91 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
94
94
95 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
96 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
98
98
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping' % (userobj, ))
106 log.debug('userobj was:%s skipping' % (userobj, ))
107 return None
107 return None
108
108
109 user_attrs = {
109 user_attrs = {
110 "username": userobj.username,
110 "username": userobj.username,
111 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
113 "groups": [],
113 "groups": [],
114 "email": userobj.email,
114 "email": userobj.email,
115 "admin": userobj.admin,
115 "admin": userobj.admin,
116 "active": userobj.active,
116 "active": userobj.active,
117 "active_from_extern": userobj.active,
117 "active_from_extern": userobj.active,
118 "extern_name": userobj.user_id,
118 "extern_name": userobj.user_id,
119 "extern_type": userobj.extern_type,
119 "extern_type": userobj.extern_type,
120 }
120 }
121
121
122 log.debug('Authenticating user with args %s', user_attrs)
122 log.debug('Authenticating user with args %s', user_attrs)
123 if userobj.active:
123 if userobj.active:
124 # calling context repo for token scopes
125 scope_repo_id = None
126 if self.acl_repo_name:
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 scope_repo_id = repo.repo_id if repo else None
129
124 token_match = userobj.authenticate_by_token(
130 token_match = userobj.authenticate_by_token(
125 password, roles=[UserApiKeys.ROLE_VCS])
131 password, roles=[UserApiKeys.ROLE_VCS],
132 scope_repo_id=scope_repo_id)
126
133
127 if userobj.username == username and token_match:
134 if userobj.username == username and token_match:
128 log.info(
135 log.info(
129 'user `%s` successfully authenticated via %s',
136 'user `%s` successfully authenticated via %s',
130 user_attrs['username'], self.name)
137 user_attrs['username'], self.name)
131 return user_attrs
138 return user_attrs
132 log.error(
139 log.error(
133 'user `%s` failed to authenticate via %s, reason: bad or '
140 'user `%s` failed to authenticate via %s, reason: bad or '
134 'inactive token.', username, self.name)
141 'inactive token.', username, self.name)
135 else:
142 else:
136 log.warning(
143 log.warning(
137 'user `%s` failed to authenticate via %s, reason: account not '
144 'user `%s` failed to authenticate via %s, reason: account not '
138 'active.', username, self.name)
145 'active.', username, self.name)
139 return None
146 return None
@@ -1,597 +1,598 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 ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def vcs_operation_context(
165 def vcs_operation_context(
166 environ, repo_name, username, action, scm, check_locking=True,
166 environ, repo_name, username, action, scm, check_locking=True,
167 is_shadow_repo=False):
167 is_shadow_repo=False):
168 """
168 """
169 Generate the context for a vcs operation, e.g. push or pull.
169 Generate the context for a vcs operation, e.g. push or pull.
170
170
171 This context is passed over the layers so that hooks triggered by the
171 This context is passed over the layers so that hooks triggered by the
172 vcs operation know details like the user, the user's IP address etc.
172 vcs operation know details like the user, the user's IP address etc.
173
173
174 :param check_locking: Allows to switch of the computation of the locking
174 :param check_locking: Allows to switch of the computation of the locking
175 data. This serves mainly the need of the simplevcs middleware to be
175 data. This serves mainly the need of the simplevcs middleware to be
176 able to disable this for certain operations.
176 able to disable this for certain operations.
177
177
178 """
178 """
179 # Tri-state value: False: unlock, None: nothing, True: lock
179 # Tri-state value: False: unlock, None: nothing, True: lock
180 make_lock = None
180 make_lock = None
181 locked_by = [None, None, None]
181 locked_by = [None, None, None]
182 is_anonymous = username == User.DEFAULT_USER
182 is_anonymous = username == User.DEFAULT_USER
183 if not is_anonymous and check_locking:
183 if not is_anonymous and check_locking:
184 log.debug('Checking locking on repository "%s"', repo_name)
184 log.debug('Checking locking on repository "%s"', repo_name)
185 user = User.get_by_username(username)
185 user = User.get_by_username(username)
186 repo = Repository.get_by_repo_name(repo_name)
186 repo = Repository.get_by_repo_name(repo_name)
187 make_lock, __, locked_by = repo.get_locking_state(
187 make_lock, __, locked_by = repo.get_locking_state(
188 action, user.user_id)
188 action, user.user_id)
189
189
190 settings_model = VcsSettingsModel(repo=repo_name)
190 settings_model = VcsSettingsModel(repo=repo_name)
191 ui_settings = settings_model.get_ui_settings()
191 ui_settings = settings_model.get_ui_settings()
192
192
193 extras = {
193 extras = {
194 'ip': get_ip_addr(environ),
194 'ip': get_ip_addr(environ),
195 'username': username,
195 'username': username,
196 'action': action,
196 'action': action,
197 'repository': repo_name,
197 'repository': repo_name,
198 'scm': scm,
198 'scm': scm,
199 'config': rhodecode.CONFIG['__file__'],
199 'config': rhodecode.CONFIG['__file__'],
200 'make_lock': make_lock,
200 'make_lock': make_lock,
201 'locked_by': locked_by,
201 'locked_by': locked_by,
202 'server_url': utils2.get_server_url(environ),
202 'server_url': utils2.get_server_url(environ),
203 'hooks': get_enabled_hook_classes(ui_settings),
203 'hooks': get_enabled_hook_classes(ui_settings),
204 'is_shadow_repo': is_shadow_repo,
204 'is_shadow_repo': is_shadow_repo,
205 }
205 }
206 return extras
206 return extras
207
207
208
208
209 class BasicAuth(AuthBasicAuthenticator):
209 class BasicAuth(AuthBasicAuthenticator):
210
210
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 initial_call_detection=False):
212 initial_call_detection=False, acl_repo_name=None):
213 self.realm = realm
213 self.realm = realm
214 self.initial_call = initial_call_detection
214 self.initial_call = initial_call_detection
215 self.authfunc = authfunc
215 self.authfunc = authfunc
216 self.registry = registry
216 self.registry = registry
217 self.acl_repo_name = acl_repo_name
217 self._rc_auth_http_code = auth_http_code
218 self._rc_auth_http_code = auth_http_code
218
219
219 def _get_response_from_code(self, http_code):
220 def _get_response_from_code(self, http_code):
220 try:
221 try:
221 return get_exception(safe_int(http_code))
222 return get_exception(safe_int(http_code))
222 except Exception:
223 except Exception:
223 log.exception('Failed to fetch response for code %s' % http_code)
224 log.exception('Failed to fetch response for code %s' % http_code)
224 return HTTPForbidden
225 return HTTPForbidden
225
226
226 def build_authentication(self):
227 def build_authentication(self):
227 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 if self._rc_auth_http_code and not self.initial_call:
229 if self._rc_auth_http_code and not self.initial_call:
229 # return alternative HTTP code if alternative http return code
230 # return alternative HTTP code if alternative http return code
230 # is specified in RhodeCode config, but ONLY if it's not the
231 # is specified in RhodeCode config, but ONLY if it's not the
231 # FIRST call
232 # FIRST call
232 custom_response_klass = self._get_response_from_code(
233 custom_response_klass = self._get_response_from_code(
233 self._rc_auth_http_code)
234 self._rc_auth_http_code)
234 return custom_response_klass(headers=head)
235 return custom_response_klass(headers=head)
235 return HTTPUnauthorized(headers=head)
236 return HTTPUnauthorized(headers=head)
236
237
237 def authenticate(self, environ):
238 def authenticate(self, environ):
238 authorization = AUTHORIZATION(environ)
239 authorization = AUTHORIZATION(environ)
239 if not authorization:
240 if not authorization:
240 return self.build_authentication()
241 return self.build_authentication()
241 (authmeth, auth) = authorization.split(' ', 1)
242 (authmeth, auth) = authorization.split(' ', 1)
242 if 'basic' != authmeth.lower():
243 if 'basic' != authmeth.lower():
243 return self.build_authentication()
244 return self.build_authentication()
244 auth = auth.strip().decode('base64')
245 auth = auth.strip().decode('base64')
245 _parts = auth.split(':', 1)
246 _parts = auth.split(':', 1)
246 if len(_parts) == 2:
247 if len(_parts) == 2:
247 username, password = _parts
248 username, password = _parts
248 if self.authfunc(
249 if self.authfunc(
249 username, password, environ, VCS_TYPE,
250 username, password, environ, VCS_TYPE,
250 registry=self.registry):
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
251 return username
252 return username
252 if username and password:
253 if username and password:
253 # we mark that we actually executed authentication once, at
254 # we mark that we actually executed authentication once, at
254 # that point we can use the alternative auth code
255 # that point we can use the alternative auth code
255 self.initial_call = False
256 self.initial_call = False
256
257
257 return self.build_authentication()
258 return self.build_authentication()
258
259
259 __call__ = authenticate
260 __call__ = authenticate
260
261
261
262
262 def attach_context_attributes(context, request):
263 def attach_context_attributes(context, request):
263 """
264 """
264 Attach variables into template context called `c`, please note that
265 Attach variables into template context called `c`, please note that
265 request could be pylons or pyramid request in here.
266 request could be pylons or pyramid request in here.
266 """
267 """
267 rc_config = SettingsModel().get_all_settings(cache=True)
268 rc_config = SettingsModel().get_all_settings(cache=True)
268
269
269 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_edition = config.get('rhodecode.edition')
271 context.rhodecode_edition = config.get('rhodecode.edition')
271 # unique secret + version does not leak the version but keep consistency
272 # unique secret + version does not leak the version but keep consistency
272 context.rhodecode_version_hash = md5(
273 context.rhodecode_version_hash = md5(
273 config.get('beaker.session.secret', '') +
274 config.get('beaker.session.secret', '') +
274 rhodecode.__version__)[:8]
275 rhodecode.__version__)[:8]
275
276
276 # Default language set for the incoming request
277 # Default language set for the incoming request
277 context.language = translation.get_lang()[0]
278 context.language = translation.get_lang()[0]
278
279
279 # Visual options
280 # Visual options
280 context.visual = AttributeDict({})
281 context.visual = AttributeDict({})
281
282
282 # DB stored Visual Items
283 # DB stored Visual Items
283 context.visual.show_public_icon = str2bool(
284 context.visual.show_public_icon = str2bool(
284 rc_config.get('rhodecode_show_public_icon'))
285 rc_config.get('rhodecode_show_public_icon'))
285 context.visual.show_private_icon = str2bool(
286 context.visual.show_private_icon = str2bool(
286 rc_config.get('rhodecode_show_private_icon'))
287 rc_config.get('rhodecode_show_private_icon'))
287 context.visual.stylify_metatags = str2bool(
288 context.visual.stylify_metatags = str2bool(
288 rc_config.get('rhodecode_stylify_metatags'))
289 rc_config.get('rhodecode_stylify_metatags'))
289 context.visual.dashboard_items = safe_int(
290 context.visual.dashboard_items = safe_int(
290 rc_config.get('rhodecode_dashboard_items', 100))
291 rc_config.get('rhodecode_dashboard_items', 100))
291 context.visual.admin_grid_items = safe_int(
292 context.visual.admin_grid_items = safe_int(
292 rc_config.get('rhodecode_admin_grid_items', 100))
293 rc_config.get('rhodecode_admin_grid_items', 100))
293 context.visual.repository_fields = str2bool(
294 context.visual.repository_fields = str2bool(
294 rc_config.get('rhodecode_repository_fields'))
295 rc_config.get('rhodecode_repository_fields'))
295 context.visual.show_version = str2bool(
296 context.visual.show_version = str2bool(
296 rc_config.get('rhodecode_show_version'))
297 rc_config.get('rhodecode_show_version'))
297 context.visual.use_gravatar = str2bool(
298 context.visual.use_gravatar = str2bool(
298 rc_config.get('rhodecode_use_gravatar'))
299 rc_config.get('rhodecode_use_gravatar'))
299 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.default_renderer = rc_config.get(
301 context.visual.default_renderer = rc_config.get(
301 'rhodecode_markup_renderer', 'rst')
302 'rhodecode_markup_renderer', 'rst')
302 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
303 context.visual.rhodecode_support_url = \
304 context.visual.rhodecode_support_url = \
304 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
305 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
305
306
306 context.pre_code = rc_config.get('rhodecode_pre_code')
307 context.pre_code = rc_config.get('rhodecode_pre_code')
307 context.post_code = rc_config.get('rhodecode_post_code')
308 context.post_code = rc_config.get('rhodecode_post_code')
308 context.rhodecode_name = rc_config.get('rhodecode_title')
309 context.rhodecode_name = rc_config.get('rhodecode_title')
309 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
310 # if we have specified default_encoding in the request, it has more
311 # if we have specified default_encoding in the request, it has more
311 # priority
312 # priority
312 if request.GET.get('default_encoding'):
313 if request.GET.get('default_encoding'):
313 context.default_encodings.insert(0, request.GET.get('default_encoding'))
314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
314 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
315
316
316 # INI stored
317 # INI stored
317 context.labs_active = str2bool(
318 context.labs_active = str2bool(
318 config.get('labs_settings_active', 'false'))
319 config.get('labs_settings_active', 'false'))
319 context.visual.allow_repo_location_change = str2bool(
320 context.visual.allow_repo_location_change = str2bool(
320 config.get('allow_repo_location_change', True))
321 config.get('allow_repo_location_change', True))
321 context.visual.allow_custom_hooks_settings = str2bool(
322 context.visual.allow_custom_hooks_settings = str2bool(
322 config.get('allow_custom_hooks_settings', True))
323 config.get('allow_custom_hooks_settings', True))
323 context.debug_style = str2bool(config.get('debug_style', False))
324 context.debug_style = str2bool(config.get('debug_style', False))
324
325
325 context.rhodecode_instanceid = config.get('instance_id')
326 context.rhodecode_instanceid = config.get('instance_id')
326
327
327 # AppEnlight
328 # AppEnlight
328 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
329 context.appenlight_api_public_key = config.get(
330 context.appenlight_api_public_key = config.get(
330 'appenlight.api_public_key', '')
331 'appenlight.api_public_key', '')
331 context.appenlight_server_url = config.get('appenlight.server_url', '')
332 context.appenlight_server_url = config.get('appenlight.server_url', '')
332
333
333 # JS template context
334 # JS template context
334 context.template_context = {
335 context.template_context = {
335 'repo_name': None,
336 'repo_name': None,
336 'repo_type': None,
337 'repo_type': None,
337 'repo_landing_commit': None,
338 'repo_landing_commit': None,
338 'rhodecode_user': {
339 'rhodecode_user': {
339 'username': None,
340 'username': None,
340 'email': None,
341 'email': None,
341 'notification_status': False
342 'notification_status': False
342 },
343 },
343 'visual': {
344 'visual': {
344 'default_renderer': None
345 'default_renderer': None
345 },
346 },
346 'commit_data': {
347 'commit_data': {
347 'commit_id': None
348 'commit_id': None
348 },
349 },
349 'pull_request_data': {'pull_request_id': None},
350 'pull_request_data': {'pull_request_id': None},
350 'timeago': {
351 'timeago': {
351 'refresh_time': 120 * 1000,
352 'refresh_time': 120 * 1000,
352 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
353 },
354 },
354 'pylons_dispatch': {
355 'pylons_dispatch': {
355 # 'controller': request.environ['pylons.routes_dict']['controller'],
356 # 'controller': request.environ['pylons.routes_dict']['controller'],
356 # 'action': request.environ['pylons.routes_dict']['action'],
357 # 'action': request.environ['pylons.routes_dict']['action'],
357 },
358 },
358 'pyramid_dispatch': {
359 'pyramid_dispatch': {
359
360
360 },
361 },
361 'extra': {'plugins': {}}
362 'extra': {'plugins': {}}
362 }
363 }
363 # END CONFIG VARS
364 # END CONFIG VARS
364
365
365 # TODO: This dosn't work when called from pylons compatibility tween.
366 # TODO: This dosn't work when called from pylons compatibility tween.
366 # Fix this and remove it from base controller.
367 # Fix this and remove it from base controller.
367 # context.repo_name = get_repo_slug(request) # can be empty
368 # context.repo_name = get_repo_slug(request) # can be empty
368
369
369 diffmode = 'sideside'
370 diffmode = 'sideside'
370 if request.GET.get('diffmode'):
371 if request.GET.get('diffmode'):
371 if request.GET['diffmode'] == 'unified':
372 if request.GET['diffmode'] == 'unified':
372 diffmode = 'unified'
373 diffmode = 'unified'
373 elif request.session.get('diffmode'):
374 elif request.session.get('diffmode'):
374 diffmode = request.session['diffmode']
375 diffmode = request.session['diffmode']
375
376
376 context.diffmode = diffmode
377 context.diffmode = diffmode
377
378
378 if request.session.get('diffmode') != diffmode:
379 if request.session.get('diffmode') != diffmode:
379 request.session['diffmode'] = diffmode
380 request.session['diffmode'] = diffmode
380
381
381 context.csrf_token = auth.get_csrf_token()
382 context.csrf_token = auth.get_csrf_token()
382 context.backends = rhodecode.BACKENDS.keys()
383 context.backends = rhodecode.BACKENDS.keys()
383 context.backends.sort()
384 context.backends.sort()
384 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
385 context.rhodecode_user.user_id)
386 context.rhodecode_user.user_id)
386
387
387 context.pyramid_request = pyramid.threadlocal.get_current_request()
388 context.pyramid_request = pyramid.threadlocal.get_current_request()
388
389
389
390
390 def get_auth_user(environ):
391 def get_auth_user(environ):
391 ip_addr = get_ip_addr(environ)
392 ip_addr = get_ip_addr(environ)
392 # make sure that we update permissions each time we call controller
393 # make sure that we update permissions each time we call controller
393 _auth_token = (request.GET.get('auth_token', '') or
394 _auth_token = (request.GET.get('auth_token', '') or
394 request.GET.get('api_key', ''))
395 request.GET.get('api_key', ''))
395
396
396 if _auth_token:
397 if _auth_token:
397 # when using API_KEY we assume user exists, and
398 # when using API_KEY we assume user exists, and
398 # doesn't need auth based on cookies.
399 # doesn't need auth based on cookies.
399 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
400 authenticated = False
401 authenticated = False
401 else:
402 else:
402 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
403 try:
404 try:
404 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
405 ip_addr=ip_addr)
406 ip_addr=ip_addr)
406 except UserCreationError as e:
407 except UserCreationError as e:
407 h.flash(e, 'error')
408 h.flash(e, 'error')
408 # container auth or other auth functions that create users
409 # container auth or other auth functions that create users
409 # on the fly can throw this exception signaling that there's
410 # on the fly can throw this exception signaling that there's
410 # issue with user creation, explanation should be provided
411 # issue with user creation, explanation should be provided
411 # in Exception itself. We then create a simple blank
412 # in Exception itself. We then create a simple blank
412 # AuthUser
413 # AuthUser
413 auth_user = AuthUser(ip_addr=ip_addr)
414 auth_user = AuthUser(ip_addr=ip_addr)
414
415
415 if password_changed(auth_user, session):
416 if password_changed(auth_user, session):
416 session.invalidate()
417 session.invalidate()
417 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 auth_user = AuthUser(ip_addr=ip_addr)
419 auth_user = AuthUser(ip_addr=ip_addr)
419
420
420 authenticated = cookie_store.get('is_authenticated')
421 authenticated = cookie_store.get('is_authenticated')
421
422
422 if not auth_user.is_authenticated and auth_user.is_user_object:
423 if not auth_user.is_authenticated and auth_user.is_user_object:
423 # user is not authenticated and not empty
424 # user is not authenticated and not empty
424 auth_user.set_authenticated(authenticated)
425 auth_user.set_authenticated(authenticated)
425
426
426 return auth_user
427 return auth_user
427
428
428
429
429 class BaseController(WSGIController):
430 class BaseController(WSGIController):
430
431
431 def __before__(self):
432 def __before__(self):
432 """
433 """
433 __before__ is called before controller methods and after __call__
434 __before__ is called before controller methods and after __call__
434 """
435 """
435 # on each call propagate settings calls into global settings.
436 # on each call propagate settings calls into global settings.
436 set_rhodecode_config(config)
437 set_rhodecode_config(config)
437 attach_context_attributes(c, request)
438 attach_context_attributes(c, request)
438
439
439 # TODO: Remove this when fixed in attach_context_attributes()
440 # TODO: Remove this when fixed in attach_context_attributes()
440 c.repo_name = get_repo_slug(request) # can be empty
441 c.repo_name = get_repo_slug(request) # can be empty
441
442
442 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
443 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
443 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
444 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
444 self.sa = meta.Session
445 self.sa = meta.Session
445 self.scm_model = ScmModel(self.sa)
446 self.scm_model = ScmModel(self.sa)
446
447
447 # set user language
448 # set user language
448 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
449 if user_lang:
450 if user_lang:
450 translation.set_lang(user_lang)
451 translation.set_lang(user_lang)
451 log.debug('set language to %s for user %s',
452 log.debug('set language to %s for user %s',
452 user_lang, self._rhodecode_user)
453 user_lang, self._rhodecode_user)
453
454
454 def _dispatch_redirect(self, with_url, environ, start_response):
455 def _dispatch_redirect(self, with_url, environ, start_response):
455 resp = HTTPFound(with_url)
456 resp = HTTPFound(with_url)
456 environ['SCRIPT_NAME'] = '' # handle prefix middleware
457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
457 environ['PATH_INFO'] = with_url
458 environ['PATH_INFO'] = with_url
458 return resp(environ, start_response)
459 return resp(environ, start_response)
459
460
460 def __call__(self, environ, start_response):
461 def __call__(self, environ, start_response):
461 """Invoke the Controller"""
462 """Invoke the Controller"""
462 # WSGIController.__call__ dispatches to the Controller method
463 # WSGIController.__call__ dispatches to the Controller method
463 # the request is routed to. This routing information is
464 # the request is routed to. This routing information is
464 # available in environ['pylons.routes_dict']
465 # available in environ['pylons.routes_dict']
465 from rhodecode.lib import helpers as h
466 from rhodecode.lib import helpers as h
466
467
467 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
468 if environ.get('debugtoolbar.wants_pylons_context', False):
469 if environ.get('debugtoolbar.wants_pylons_context', False):
469 environ['debugtoolbar.pylons_context'] = c._current_obj()
470 environ['debugtoolbar.pylons_context'] = c._current_obj()
470
471
471 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
472 environ['pylons.routes_dict']['action']])
473 environ['pylons.routes_dict']['action']])
473
474
474 self.rc_config = SettingsModel().get_all_settings(cache=True)
475 self.rc_config = SettingsModel().get_all_settings(cache=True)
475 self.ip_addr = get_ip_addr(environ)
476 self.ip_addr = get_ip_addr(environ)
476
477
477 # The rhodecode auth user is looked up and passed through the
478 # The rhodecode auth user is looked up and passed through the
478 # environ by the pylons compatibility tween in pyramid.
479 # environ by the pylons compatibility tween in pyramid.
479 # So we can just grab it from there.
480 # So we can just grab it from there.
480 auth_user = environ['rc_auth_user']
481 auth_user = environ['rc_auth_user']
481
482
482 # set globals for auth user
483 # set globals for auth user
483 request.user = auth_user
484 request.user = auth_user
484 c.rhodecode_user = self._rhodecode_user = auth_user
485 c.rhodecode_user = self._rhodecode_user = auth_user
485
486
486 log.info('IP: %s User: %s accessed %s [%s]' % (
487 log.info('IP: %s User: %s accessed %s [%s]' % (
487 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
488 _route_name)
489 _route_name)
489 )
490 )
490
491
491 # TODO: Maybe this should be move to pyramid to cover all views.
492 # TODO: Maybe this should be move to pyramid to cover all views.
492 # check user attributes for password change flag
493 # check user attributes for password change flag
493 user_obj = auth_user.get_instance()
494 user_obj = auth_user.get_instance()
494 if user_obj and user_obj.user_data.get('force_password_change'):
495 if user_obj and user_obj.user_data.get('force_password_change'):
495 h.flash('You are required to change your password', 'warning',
496 h.flash('You are required to change your password', 'warning',
496 ignore_duplicate=True)
497 ignore_duplicate=True)
497
498
498 skip_user_check_urls = [
499 skip_user_check_urls = [
499 'error.document', 'login.logout', 'login.index',
500 'error.document', 'login.logout', 'login.index',
500 'admin/my_account.my_account_password',
501 'admin/my_account.my_account_password',
501 'admin/my_account.my_account_password_update'
502 'admin/my_account.my_account_password_update'
502 ]
503 ]
503 if _route_name not in skip_user_check_urls:
504 if _route_name not in skip_user_check_urls:
504 return self._dispatch_redirect(
505 return self._dispatch_redirect(
505 url('my_account_password'), environ, start_response)
506 url('my_account_password'), environ, start_response)
506
507
507 return WSGIController.__call__(self, environ, start_response)
508 return WSGIController.__call__(self, environ, start_response)
508
509
509
510
510 class BaseRepoController(BaseController):
511 class BaseRepoController(BaseController):
511 """
512 """
512 Base class for controllers responsible for loading all needed data for
513 Base class for controllers responsible for loading all needed data for
513 repository loaded items are
514 repository loaded items are
514
515
515 c.rhodecode_repo: instance of scm repository
516 c.rhodecode_repo: instance of scm repository
516 c.rhodecode_db_repo: instance of db
517 c.rhodecode_db_repo: instance of db
517 c.repository_requirements_missing: shows that repository specific data
518 c.repository_requirements_missing: shows that repository specific data
518 could not be displayed due to the missing requirements
519 could not be displayed due to the missing requirements
519 c.repository_pull_requests: show number of open pull requests
520 c.repository_pull_requests: show number of open pull requests
520 """
521 """
521
522
522 def __before__(self):
523 def __before__(self):
523 super(BaseRepoController, self).__before__()
524 super(BaseRepoController, self).__before__()
524 if c.repo_name: # extracted from routes
525 if c.repo_name: # extracted from routes
525 db_repo = Repository.get_by_repo_name(c.repo_name)
526 db_repo = Repository.get_by_repo_name(c.repo_name)
526 if not db_repo:
527 if not db_repo:
527 return
528 return
528
529
529 log.debug(
530 log.debug(
530 'Found repository in database %s with state `%s`',
531 'Found repository in database %s with state `%s`',
531 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
532 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
532 route = getattr(request.environ.get('routes.route'), 'name', '')
533 route = getattr(request.environ.get('routes.route'), 'name', '')
533
534
534 # allow to delete repos that are somehow damages in filesystem
535 # allow to delete repos that are somehow damages in filesystem
535 if route in ['delete_repo']:
536 if route in ['delete_repo']:
536 return
537 return
537
538
538 if db_repo.repo_state in [Repository.STATE_PENDING]:
539 if db_repo.repo_state in [Repository.STATE_PENDING]:
539 if route in ['repo_creating_home']:
540 if route in ['repo_creating_home']:
540 return
541 return
541 check_url = url('repo_creating_home', repo_name=c.repo_name)
542 check_url = url('repo_creating_home', repo_name=c.repo_name)
542 return redirect(check_url)
543 return redirect(check_url)
543
544
544 self.rhodecode_db_repo = db_repo
545 self.rhodecode_db_repo = db_repo
545
546
546 missing_requirements = False
547 missing_requirements = False
547 try:
548 try:
548 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
549 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
549 except RepositoryRequirementError as e:
550 except RepositoryRequirementError as e:
550 missing_requirements = True
551 missing_requirements = True
551 self._handle_missing_requirements(e)
552 self._handle_missing_requirements(e)
552
553
553 if self.rhodecode_repo is None and not missing_requirements:
554 if self.rhodecode_repo is None and not missing_requirements:
554 log.error('%s this repository is present in database but it '
555 log.error('%s this repository is present in database but it '
555 'cannot be created as an scm instance', c.repo_name)
556 'cannot be created as an scm instance', c.repo_name)
556
557
557 h.flash(_(
558 h.flash(_(
558 "The repository at %(repo_name)s cannot be located.") %
559 "The repository at %(repo_name)s cannot be located.") %
559 {'repo_name': c.repo_name},
560 {'repo_name': c.repo_name},
560 category='error', ignore_duplicate=True)
561 category='error', ignore_duplicate=True)
561 redirect(url('home'))
562 redirect(url('home'))
562
563
563 # update last change according to VCS data
564 # update last change according to VCS data
564 if not missing_requirements:
565 if not missing_requirements:
565 commit = db_repo.get_commit(
566 commit = db_repo.get_commit(
566 pre_load=["author", "date", "message", "parents"])
567 pre_load=["author", "date", "message", "parents"])
567 db_repo.update_commit_cache(commit)
568 db_repo.update_commit_cache(commit)
568
569
569 # Prepare context
570 # Prepare context
570 c.rhodecode_db_repo = db_repo
571 c.rhodecode_db_repo = db_repo
571 c.rhodecode_repo = self.rhodecode_repo
572 c.rhodecode_repo = self.rhodecode_repo
572 c.repository_requirements_missing = missing_requirements
573 c.repository_requirements_missing = missing_requirements
573
574
574 self._update_global_counters(self.scm_model, db_repo)
575 self._update_global_counters(self.scm_model, db_repo)
575
576
576 def _update_global_counters(self, scm_model, db_repo):
577 def _update_global_counters(self, scm_model, db_repo):
577 """
578 """
578 Base variables that are exposed to every page of repository
579 Base variables that are exposed to every page of repository
579 """
580 """
580 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
581 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
581
582
582 def _handle_missing_requirements(self, error):
583 def _handle_missing_requirements(self, error):
583 self.rhodecode_repo = None
584 self.rhodecode_repo = None
584 log.error(
585 log.error(
585 'Requirements are missing for repository %s: %s',
586 'Requirements are missing for repository %s: %s',
586 c.repo_name, error.message)
587 c.repo_name, error.message)
587
588
588 summary_url = url('summary_home', repo_name=c.repo_name)
589 summary_url = url('summary_home', repo_name=c.repo_name)
589 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
590 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
590 settings_update_url = url('repo', repo_name=c.repo_name)
591 settings_update_url = url('repo', repo_name=c.repo_name)
591 path = request.path
592 path = request.path
592 should_redirect = (
593 should_redirect = (
593 path not in (summary_url, settings_update_url)
594 path not in (summary_url, settings_update_url)
594 and '/settings' not in path or path == statistics_url
595 and '/settings' not in path or path == statistics_url
595 )
596 )
596 if should_redirect:
597 if should_redirect:
597 redirect(summary_url)
598 redirect(summary_url)
@@ -1,526 +1,529 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 logging
27 import logging
28 import importlib
28 import importlib
29 import re
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
33 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 NotAllowedToCreateUserError)
42 NotAllowedToCreateUserError)
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware.utils import scm_app_http
45 from rhodecode.lib.middleware.utils import scm_app_http
46 from rhodecode.lib.utils import (
46 from rhodecode.lib.utils import (
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.backends import base
50 from rhodecode.lib.vcs.backends import base
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.db import User, Repository, PullRequest
52 from rhodecode.model.db import User, Repository, PullRequest
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.pull_request import PullRequestModel
54 from rhodecode.model.pull_request import PullRequestModel
55
55
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 def initialize_generator(factory):
60 def initialize_generator(factory):
61 """
61 """
62 Initializes the returned generator by draining its first element.
62 Initializes the returned generator by draining its first element.
63
63
64 This can be used to give a generator an initializer, which is the code
64 This can be used to give a generator an initializer, which is the code
65 up to the first yield statement. This decorator enforces that the first
65 up to the first yield statement. This decorator enforces that the first
66 produced element has the value ``"__init__"`` to make its special
66 produced element has the value ``"__init__"`` to make its special
67 purpose very explicit in the using code.
67 purpose very explicit in the using code.
68 """
68 """
69
69
70 @wraps(factory)
70 @wraps(factory)
71 def wrapper(*args, **kwargs):
71 def wrapper(*args, **kwargs):
72 gen = factory(*args, **kwargs)
72 gen = factory(*args, **kwargs)
73 try:
73 try:
74 init = gen.next()
74 init = gen.next()
75 except StopIteration:
75 except StopIteration:
76 raise ValueError('Generator must yield at least one element.')
76 raise ValueError('Generator must yield at least one element.')
77 if init != "__init__":
77 if init != "__init__":
78 raise ValueError('First yielded element must be "__init__".')
78 raise ValueError('First yielded element must be "__init__".')
79 return gen
79 return gen
80 return wrapper
80 return wrapper
81
81
82
82
83 class SimpleVCS(object):
83 class SimpleVCS(object):
84 """Common functionality for SCM HTTP handlers."""
84 """Common functionality for SCM HTTP handlers."""
85
85
86 SCM = 'unknown'
86 SCM = 'unknown'
87
87
88 acl_repo_name = None
88 acl_repo_name = None
89 url_repo_name = None
89 url_repo_name = None
90 vcs_repo_name = None
90 vcs_repo_name = None
91
91
92 # We have to handle requests to shadow repositories different than requests
92 # We have to handle requests to shadow repositories different than requests
93 # to normal repositories. Therefore we have to distinguish them. To do this
93 # to normal repositories. Therefore we have to distinguish them. To do this
94 # we use this regex which will match only on URLs pointing to shadow
94 # we use this regex which will match only on URLs pointing to shadow
95 # repositories.
95 # repositories.
96 shadow_repo_re = re.compile(
96 shadow_repo_re = re.compile(
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
98 '(?P<target>{slug_pat})/' # target repo
98 '(?P<target>{slug_pat})/' # target repo
99 'pull-request/(?P<pr_id>\d+)/' # pull request
99 'pull-request/(?P<pr_id>\d+)/' # pull request
100 'repository$' # shadow repo
100 'repository$' # shadow repo
101 .format(slug_pat=SLUG_RE.pattern))
101 .format(slug_pat=SLUG_RE.pattern))
102
102
103 def __init__(self, application, config, registry):
103 def __init__(self, application, config, registry):
104 self.registry = registry
104 self.registry = registry
105 self.application = application
105 self.application = application
106 self.config = config
106 self.config = config
107 # re-populated by specialized middleware
107 # re-populated by specialized middleware
108 self.repo_vcs_config = base.Config()
108 self.repo_vcs_config = base.Config()
109
109
110 # base path of repo locations
110 # base path of repo locations
111 self.basepath = get_rhodecode_base_path()
111 self.basepath = get_rhodecode_base_path()
112 # authenticate this VCS request using authfunc
112 # authenticate this VCS request using authfunc
113 auth_ret_code_detection = \
113 auth_ret_code_detection = \
114 str2bool(self.config.get('auth_ret_code_detection', False))
114 str2bool(self.config.get('auth_ret_code_detection', False))
115 self.authenticate = BasicAuth(
115 self.authenticate = BasicAuth(
116 '', authenticate, registry, config.get('auth_ret_code'),
116 '', authenticate, registry, config.get('auth_ret_code'),
117 auth_ret_code_detection)
117 auth_ret_code_detection)
118 self.ip_addr = '0.0.0.0'
118 self.ip_addr = '0.0.0.0'
119
119
120 def set_repo_names(self, environ):
120 def set_repo_names(self, environ):
121 """
121 """
122 This will populate the attributes acl_repo_name, url_repo_name,
122 This will populate the attributes acl_repo_name, url_repo_name,
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
124 shadow) repositories all names are equal. In case of requests to a
124 shadow) repositories all names are equal. In case of requests to a
125 shadow repository the acl-name points to the target repo of the pull
125 shadow repository the acl-name points to the target repo of the pull
126 request and the vcs-name points to the shadow repo file system path.
126 request and the vcs-name points to the shadow repo file system path.
127 The url-name is always the URL used by the vcs client program.
127 The url-name is always the URL used by the vcs client program.
128
128
129 Example in case of a shadow repo:
129 Example in case of a shadow repo:
130 acl_repo_name = RepoGroup/MyRepo
130 acl_repo_name = RepoGroup/MyRepo
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
133 """
133 """
134 # First we set the repo name from URL for all attributes. This is the
134 # First we set the repo name from URL for all attributes. This is the
135 # default if handling normal (non shadow) repo requests.
135 # default if handling normal (non shadow) repo requests.
136 self.url_repo_name = self._get_repository_name(environ)
136 self.url_repo_name = self._get_repository_name(environ)
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
138 self.is_shadow_repo = False
138 self.is_shadow_repo = False
139
139
140 # Check if this is a request to a shadow repository.
140 # Check if this is a request to a shadow repository.
141 match = self.shadow_repo_re.match(self.url_repo_name)
141 match = self.shadow_repo_re.match(self.url_repo_name)
142 if match:
142 if match:
143 match_dict = match.groupdict()
143 match_dict = match.groupdict()
144
144
145 # Build acl repo name from regex match.
145 # Build acl repo name from regex match.
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
147 groups=match_dict['groups'] or '',
147 groups=match_dict['groups'] or '',
148 target=match_dict['target']))
148 target=match_dict['target']))
149
149
150 # Retrieve pull request instance by ID from regex match.
150 # Retrieve pull request instance by ID from regex match.
151 pull_request = PullRequest.get(match_dict['pr_id'])
151 pull_request = PullRequest.get(match_dict['pr_id'])
152
152
153 # Only proceed if we got a pull request and if acl repo name from
153 # Only proceed if we got a pull request and if acl repo name from
154 # URL equals the target repo name of the pull request.
154 # URL equals the target repo name of the pull request.
155 if pull_request and (acl_repo_name ==
155 if pull_request and (acl_repo_name ==
156 pull_request.target_repo.repo_name):
156 pull_request.target_repo.repo_name):
157 # Get file system path to shadow repository.
157 # Get file system path to shadow repository.
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
159 target_vcs = pull_request.target_repo.scm_instance()
159 target_vcs = pull_request.target_repo.scm_instance()
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
161 workspace_id)
161 workspace_id)
162
162
163 # Store names for later usage.
163 # Store names for later usage.
164 self.vcs_repo_name = vcs_repo_name
164 self.vcs_repo_name = vcs_repo_name
165 self.acl_repo_name = acl_repo_name
165 self.acl_repo_name = acl_repo_name
166 self.is_shadow_repo = True
166 self.is_shadow_repo = True
167
167
168 log.debug('Setting all VCS repository names: %s', {
168 log.debug('Setting all VCS repository names: %s', {
169 'acl_repo_name': self.acl_repo_name,
169 'acl_repo_name': self.acl_repo_name,
170 'url_repo_name': self.url_repo_name,
170 'url_repo_name': self.url_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
172 })
172 })
173
173
174 @property
174 @property
175 def scm_app(self):
175 def scm_app(self):
176 custom_implementation = self.config['vcs.scm_app_implementation']
176 custom_implementation = self.config['vcs.scm_app_implementation']
177 if custom_implementation == 'http':
177 if custom_implementation == 'http':
178 log.info('Using HTTP implementation of scm app.')
178 log.info('Using HTTP implementation of scm app.')
179 scm_app_impl = scm_app_http
179 scm_app_impl = scm_app_http
180 else:
180 else:
181 log.info('Using custom implementation of scm_app: "{}"'.format(
181 log.info('Using custom implementation of scm_app: "{}"'.format(
182 custom_implementation))
182 custom_implementation))
183 scm_app_impl = importlib.import_module(custom_implementation)
183 scm_app_impl = importlib.import_module(custom_implementation)
184 return scm_app_impl
184 return scm_app_impl
185
185
186 def _get_by_id(self, repo_name):
186 def _get_by_id(self, repo_name):
187 """
187 """
188 Gets a special pattern _<ID> from clone url and tries to replace it
188 Gets a special pattern _<ID> from clone url and tries to replace it
189 with a repository_name for support of _<ID> non changeable urls
189 with a repository_name for support of _<ID> non changeable urls
190 """
190 """
191
191
192 data = repo_name.split('/')
192 data = repo_name.split('/')
193 if len(data) >= 2:
193 if len(data) >= 2:
194 from rhodecode.model.repo import RepoModel
194 from rhodecode.model.repo import RepoModel
195 by_id_match = RepoModel().get_repo_by_id(repo_name)
195 by_id_match = RepoModel().get_repo_by_id(repo_name)
196 if by_id_match:
196 if by_id_match:
197 data[1] = by_id_match.repo_name
197 data[1] = by_id_match.repo_name
198
198
199 return safe_str('/'.join(data))
199 return safe_str('/'.join(data))
200
200
201 def _invalidate_cache(self, repo_name):
201 def _invalidate_cache(self, repo_name):
202 """
202 """
203 Set's cache for this repository for invalidation on next access
203 Set's cache for this repository for invalidation on next access
204
204
205 :param repo_name: full repo name, also a cache key
205 :param repo_name: full repo name, also a cache key
206 """
206 """
207 ScmModel().mark_for_invalidation(repo_name)
207 ScmModel().mark_for_invalidation(repo_name)
208
208
209 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
209 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
210 db_repo = Repository.get_by_repo_name(repo_name)
210 db_repo = Repository.get_by_repo_name(repo_name)
211 if not db_repo:
211 if not db_repo:
212 log.debug('Repository `%s` not found inside the database.',
212 log.debug('Repository `%s` not found inside the database.',
213 repo_name)
213 repo_name)
214 return False
214 return False
215
215
216 if db_repo.repo_type != scm_type:
216 if db_repo.repo_type != scm_type:
217 log.warning(
217 log.warning(
218 'Repository `%s` have incorrect scm_type, expected %s got %s',
218 'Repository `%s` have incorrect scm_type, expected %s got %s',
219 repo_name, db_repo.repo_type, scm_type)
219 repo_name, db_repo.repo_type, scm_type)
220 return False
220 return False
221
221
222 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
222 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
223
223
224 def valid_and_active_user(self, user):
224 def valid_and_active_user(self, user):
225 """
225 """
226 Checks if that user is not empty, and if it's actually object it checks
226 Checks if that user is not empty, and if it's actually object it checks
227 if he's active.
227 if he's active.
228
228
229 :param user: user object or None
229 :param user: user object or None
230 :return: boolean
230 :return: boolean
231 """
231 """
232 if user is None:
232 if user is None:
233 return False
233 return False
234
234
235 elif user.active:
235 elif user.active:
236 return True
236 return True
237
237
238 return False
238 return False
239
239
240 def _check_permission(self, action, user, repo_name, ip_addr=None):
240 def _check_permission(self, action, user, repo_name, ip_addr=None):
241 """
241 """
242 Checks permissions using action (push/pull) user and repository
242 Checks permissions using action (push/pull) user and repository
243 name
243 name
244
244
245 :param action: push or pull action
245 :param action: push or pull action
246 :param user: user instance
246 :param user: user instance
247 :param repo_name: repository name
247 :param repo_name: repository name
248 """
248 """
249 # check IP
249 # check IP
250 inherit = user.inherit_default_permissions
250 inherit = user.inherit_default_permissions
251 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
251 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
252 inherit_from_default=inherit)
252 inherit_from_default=inherit)
253 if ip_allowed:
253 if ip_allowed:
254 log.info('Access for IP:%s allowed', ip_addr)
254 log.info('Access for IP:%s allowed', ip_addr)
255 else:
255 else:
256 return False
256 return False
257
257
258 if action == 'push':
258 if action == 'push':
259 if not HasPermissionAnyMiddleware('repository.write',
259 if not HasPermissionAnyMiddleware('repository.write',
260 'repository.admin')(user,
260 'repository.admin')(user,
261 repo_name):
261 repo_name):
262 return False
262 return False
263
263
264 else:
264 else:
265 # any other action need at least read permission
265 # any other action need at least read permission
266 if not HasPermissionAnyMiddleware('repository.read',
266 if not HasPermissionAnyMiddleware('repository.read',
267 'repository.write',
267 'repository.write',
268 'repository.admin')(user,
268 'repository.admin')(user,
269 repo_name):
269 repo_name):
270 return False
270 return False
271
271
272 return True
272 return True
273
273
274 def _check_ssl(self, environ, start_response):
274 def _check_ssl(self, environ, start_response):
275 """
275 """
276 Checks the SSL check flag and returns False if SSL is not present
276 Checks the SSL check flag and returns False if SSL is not present
277 and required True otherwise
277 and required True otherwise
278 """
278 """
279 org_proto = environ['wsgi._org_proto']
279 org_proto = environ['wsgi._org_proto']
280 # check if we have SSL required ! if not it's a bad request !
280 # check if we have SSL required ! if not it's a bad request !
281 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
281 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
282 if require_ssl and org_proto == 'http':
282 if require_ssl and org_proto == 'http':
283 log.debug('proto is %s and SSL is required BAD REQUEST !',
283 log.debug('proto is %s and SSL is required BAD REQUEST !',
284 org_proto)
284 org_proto)
285 return False
285 return False
286 return True
286 return True
287
287
288 def __call__(self, environ, start_response):
288 def __call__(self, environ, start_response):
289 try:
289 try:
290 return self._handle_request(environ, start_response)
290 return self._handle_request(environ, start_response)
291 except Exception:
291 except Exception:
292 log.exception("Exception while handling request")
292 log.exception("Exception while handling request")
293 appenlight.track_exception(environ)
293 appenlight.track_exception(environ)
294 return HTTPInternalServerError()(environ, start_response)
294 return HTTPInternalServerError()(environ, start_response)
295 finally:
295 finally:
296 meta.Session.remove()
296 meta.Session.remove()
297
297
298 def _handle_request(self, environ, start_response):
298 def _handle_request(self, environ, start_response):
299
299
300 if not self._check_ssl(environ, start_response):
300 if not self._check_ssl(environ, start_response):
301 reason = ('SSL required, while RhodeCode was unable '
301 reason = ('SSL required, while RhodeCode was unable '
302 'to detect this as SSL request')
302 'to detect this as SSL request')
303 log.debug('User not allowed to proceed, %s', reason)
303 log.debug('User not allowed to proceed, %s', reason)
304 return HTTPNotAcceptable(reason)(environ, start_response)
304 return HTTPNotAcceptable(reason)(environ, start_response)
305
305
306 if not self.url_repo_name:
306 if not self.url_repo_name:
307 log.warning('Repository name is empty: %s', self.url_repo_name)
307 log.warning('Repository name is empty: %s', self.url_repo_name)
308 # failed to get repo name, we fail now
308 # failed to get repo name, we fail now
309 return HTTPNotFound()(environ, start_response)
309 return HTTPNotFound()(environ, start_response)
310 log.debug('Extracted repo name is %s', self.url_repo_name)
310 log.debug('Extracted repo name is %s', self.url_repo_name)
311
311
312 ip_addr = get_ip_addr(environ)
312 ip_addr = get_ip_addr(environ)
313 username = None
313 username = None
314
314
315 # skip passing error to error controller
315 # skip passing error to error controller
316 environ['pylons.status_code_redirect'] = True
316 environ['pylons.status_code_redirect'] = True
317
317
318 # ======================================================================
318 # ======================================================================
319 # GET ACTION PULL or PUSH
319 # GET ACTION PULL or PUSH
320 # ======================================================================
320 # ======================================================================
321 action = self._get_action(environ)
321 action = self._get_action(environ)
322
322
323 # ======================================================================
323 # ======================================================================
324 # Check if this is a request to a shadow repository of a pull request.
324 # Check if this is a request to a shadow repository of a pull request.
325 # In this case only pull action is allowed.
325 # In this case only pull action is allowed.
326 # ======================================================================
326 # ======================================================================
327 if self.is_shadow_repo and action != 'pull':
327 if self.is_shadow_repo and action != 'pull':
328 reason = 'Only pull action is allowed for shadow repositories.'
328 reason = 'Only pull action is allowed for shadow repositories.'
329 log.debug('User not allowed to proceed, %s', reason)
329 log.debug('User not allowed to proceed, %s', reason)
330 return HTTPNotAcceptable(reason)(environ, start_response)
330 return HTTPNotAcceptable(reason)(environ, start_response)
331
331
332 # ======================================================================
332 # ======================================================================
333 # CHECK ANONYMOUS PERMISSION
333 # CHECK ANONYMOUS PERMISSION
334 # ======================================================================
334 # ======================================================================
335 if action in ['pull', 'push']:
335 if action in ['pull', 'push']:
336 anonymous_user = User.get_default_user()
336 anonymous_user = User.get_default_user()
337 username = anonymous_user.username
337 username = anonymous_user.username
338 if anonymous_user.active:
338 if anonymous_user.active:
339 # ONLY check permissions if the user is activated
339 # ONLY check permissions if the user is activated
340 anonymous_perm = self._check_permission(
340 anonymous_perm = self._check_permission(
341 action, anonymous_user, self.acl_repo_name, ip_addr)
341 action, anonymous_user, self.acl_repo_name, ip_addr)
342 else:
342 else:
343 anonymous_perm = False
343 anonymous_perm = False
344
344
345 if not anonymous_user.active or not anonymous_perm:
345 if not anonymous_user.active or not anonymous_perm:
346 if not anonymous_user.active:
346 if not anonymous_user.active:
347 log.debug('Anonymous access is disabled, running '
347 log.debug('Anonymous access is disabled, running '
348 'authentication')
348 'authentication')
349
349
350 if not anonymous_perm:
350 if not anonymous_perm:
351 log.debug('Not enough credentials to access this '
351 log.debug('Not enough credentials to access this '
352 'repository as anonymous user')
352 'repository as anonymous user')
353
353
354 username = None
354 username = None
355 # ==============================================================
355 # ==============================================================
356 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
356 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
357 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
357 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
358 # ==============================================================
358 # ==============================================================
359
359
360 # try to auth based on environ, container auth methods
360 # try to auth based on environ, container auth methods
361 log.debug('Running PRE-AUTH for container based authentication')
361 log.debug('Running PRE-AUTH for container based authentication')
362 pre_auth = authenticate(
362 pre_auth = authenticate(
363 '', '', environ, VCS_TYPE, registry=self.registry)
363 '', '', environ, VCS_TYPE, registry=self.registry,
364 acl_repo_name=self.acl_repo_name)
364 if pre_auth and pre_auth.get('username'):
365 if pre_auth and pre_auth.get('username'):
365 username = pre_auth['username']
366 username = pre_auth['username']
366 log.debug('PRE-AUTH got %s as username', username)
367 log.debug('PRE-AUTH got %s as username', username)
367
368
368 # If not authenticated by the container, running basic auth
369 # If not authenticated by the container, running basic auth
370 # before inject the calling repo_name for special scope checks
371 self.authenticate.acl_repo_name = self.acl_repo_name
369 if not username:
372 if not username:
370 self.authenticate.realm = get_rhodecode_realm()
373 self.authenticate.realm = get_rhodecode_realm()
371
374
372 try:
375 try:
373 result = self.authenticate(environ)
376 result = self.authenticate(environ)
374 except (UserCreationError, NotAllowedToCreateUserError) as e:
377 except (UserCreationError, NotAllowedToCreateUserError) as e:
375 log.error(e)
378 log.error(e)
376 reason = safe_str(e)
379 reason = safe_str(e)
377 return HTTPNotAcceptable(reason)(environ, start_response)
380 return HTTPNotAcceptable(reason)(environ, start_response)
378
381
379 if isinstance(result, str):
382 if isinstance(result, str):
380 AUTH_TYPE.update(environ, 'basic')
383 AUTH_TYPE.update(environ, 'basic')
381 REMOTE_USER.update(environ, result)
384 REMOTE_USER.update(environ, result)
382 username = result
385 username = result
383 else:
386 else:
384 return result.wsgi_application(environ, start_response)
387 return result.wsgi_application(environ, start_response)
385
388
386 # ==============================================================
389 # ==============================================================
387 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
390 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
388 # ==============================================================
391 # ==============================================================
389 user = User.get_by_username(username)
392 user = User.get_by_username(username)
390 if not self.valid_and_active_user(user):
393 if not self.valid_and_active_user(user):
391 return HTTPForbidden()(environ, start_response)
394 return HTTPForbidden()(environ, start_response)
392 username = user.username
395 username = user.username
393 user.update_lastactivity()
396 user.update_lastactivity()
394 meta.Session().commit()
397 meta.Session().commit()
395
398
396 # check user attributes for password change flag
399 # check user attributes for password change flag
397 user_obj = user
400 user_obj = user
398 if user_obj and user_obj.username != User.DEFAULT_USER and \
401 if user_obj and user_obj.username != User.DEFAULT_USER and \
399 user_obj.user_data.get('force_password_change'):
402 user_obj.user_data.get('force_password_change'):
400 reason = 'password change required'
403 reason = 'password change required'
401 log.debug('User not allowed to authenticate, %s', reason)
404 log.debug('User not allowed to authenticate, %s', reason)
402 return HTTPNotAcceptable(reason)(environ, start_response)
405 return HTTPNotAcceptable(reason)(environ, start_response)
403
406
404 # check permissions for this repository
407 # check permissions for this repository
405 perm = self._check_permission(
408 perm = self._check_permission(
406 action, user, self.acl_repo_name, ip_addr)
409 action, user, self.acl_repo_name, ip_addr)
407 if not perm:
410 if not perm:
408 return HTTPForbidden()(environ, start_response)
411 return HTTPForbidden()(environ, start_response)
409
412
410 # extras are injected into UI object and later available
413 # extras are injected into UI object and later available
411 # in hooks executed by rhodecode
414 # in hooks executed by rhodecode
412 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
415 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
413 extras = vcs_operation_context(
416 extras = vcs_operation_context(
414 environ, repo_name=self.acl_repo_name, username=username,
417 environ, repo_name=self.acl_repo_name, username=username,
415 action=action, scm=self.SCM, check_locking=check_locking,
418 action=action, scm=self.SCM, check_locking=check_locking,
416 is_shadow_repo=self.is_shadow_repo
419 is_shadow_repo=self.is_shadow_repo
417 )
420 )
418
421
419 # ======================================================================
422 # ======================================================================
420 # REQUEST HANDLING
423 # REQUEST HANDLING
421 # ======================================================================
424 # ======================================================================
422 repo_path = os.path.join(
425 repo_path = os.path.join(
423 safe_str(self.basepath), safe_str(self.vcs_repo_name))
426 safe_str(self.basepath), safe_str(self.vcs_repo_name))
424 log.debug('Repository path is %s', repo_path)
427 log.debug('Repository path is %s', repo_path)
425
428
426 fix_PATH()
429 fix_PATH()
427
430
428 log.info(
431 log.info(
429 '%s action on %s repo "%s" by "%s" from %s',
432 '%s action on %s repo "%s" by "%s" from %s',
430 action, self.SCM, safe_str(self.url_repo_name),
433 action, self.SCM, safe_str(self.url_repo_name),
431 safe_str(username), ip_addr)
434 safe_str(username), ip_addr)
432
435
433 return self._generate_vcs_response(
436 return self._generate_vcs_response(
434 environ, start_response, repo_path, extras, action)
437 environ, start_response, repo_path, extras, action)
435
438
436 @initialize_generator
439 @initialize_generator
437 def _generate_vcs_response(
440 def _generate_vcs_response(
438 self, environ, start_response, repo_path, extras, action):
441 self, environ, start_response, repo_path, extras, action):
439 """
442 """
440 Returns a generator for the response content.
443 Returns a generator for the response content.
441
444
442 This method is implemented as a generator, so that it can trigger
445 This method is implemented as a generator, so that it can trigger
443 the cache validation after all content sent back to the client. It
446 the cache validation after all content sent back to the client. It
444 also handles the locking exceptions which will be triggered when
447 also handles the locking exceptions which will be triggered when
445 the first chunk is produced by the underlying WSGI application.
448 the first chunk is produced by the underlying WSGI application.
446 """
449 """
447 callback_daemon, extras = self._prepare_callback_daemon(extras)
450 callback_daemon, extras = self._prepare_callback_daemon(extras)
448 config = self._create_config(extras, self.acl_repo_name)
451 config = self._create_config(extras, self.acl_repo_name)
449 log.debug('HOOKS extras is %s', extras)
452 log.debug('HOOKS extras is %s', extras)
450 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
453 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
451
454
452 try:
455 try:
453 with callback_daemon:
456 with callback_daemon:
454 try:
457 try:
455 response = app(environ, start_response)
458 response = app(environ, start_response)
456 finally:
459 finally:
457 # This statement works together with the decorator
460 # This statement works together with the decorator
458 # "initialize_generator" above. The decorator ensures that
461 # "initialize_generator" above. The decorator ensures that
459 # we hit the first yield statement before the generator is
462 # we hit the first yield statement before the generator is
460 # returned back to the WSGI server. This is needed to
463 # returned back to the WSGI server. This is needed to
461 # ensure that the call to "app" above triggers the
464 # ensure that the call to "app" above triggers the
462 # needed callback to "start_response" before the
465 # needed callback to "start_response" before the
463 # generator is actually used.
466 # generator is actually used.
464 yield "__init__"
467 yield "__init__"
465
468
466 for chunk in response:
469 for chunk in response:
467 yield chunk
470 yield chunk
468 except Exception as exc:
471 except Exception as exc:
469 # TODO: martinb: Exceptions are only raised in case of the Pyro4
472 # TODO: martinb: Exceptions are only raised in case of the Pyro4
470 # backend. Refactor this except block after dropping Pyro4 support.
473 # backend. Refactor this except block after dropping Pyro4 support.
471 # TODO: johbo: Improve "translating" back the exception.
474 # TODO: johbo: Improve "translating" back the exception.
472 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
475 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
473 exc = HTTPLockedRC(*exc.args)
476 exc = HTTPLockedRC(*exc.args)
474 _code = rhodecode.CONFIG.get('lock_ret_code')
477 _code = rhodecode.CONFIG.get('lock_ret_code')
475 log.debug('Repository LOCKED ret code %s!', (_code,))
478 log.debug('Repository LOCKED ret code %s!', (_code,))
476 elif getattr(exc, '_vcs_kind', None) == 'requirement':
479 elif getattr(exc, '_vcs_kind', None) == 'requirement':
477 log.debug(
480 log.debug(
478 'Repository requires features unknown to this Mercurial')
481 'Repository requires features unknown to this Mercurial')
479 exc = HTTPRequirementError(*exc.args)
482 exc = HTTPRequirementError(*exc.args)
480 else:
483 else:
481 raise
484 raise
482
485
483 for chunk in exc(environ, start_response):
486 for chunk in exc(environ, start_response):
484 yield chunk
487 yield chunk
485 finally:
488 finally:
486 # invalidate cache on push
489 # invalidate cache on push
487 try:
490 try:
488 if action == 'push':
491 if action == 'push':
489 self._invalidate_cache(self.url_repo_name)
492 self._invalidate_cache(self.url_repo_name)
490 finally:
493 finally:
491 meta.Session.remove()
494 meta.Session.remove()
492
495
493 def _get_repository_name(self, environ):
496 def _get_repository_name(self, environ):
494 """Get repository name out of the environmnent
497 """Get repository name out of the environmnent
495
498
496 :param environ: WSGI environment
499 :param environ: WSGI environment
497 """
500 """
498 raise NotImplementedError()
501 raise NotImplementedError()
499
502
500 def _get_action(self, environ):
503 def _get_action(self, environ):
501 """Map request commands into a pull or push command.
504 """Map request commands into a pull or push command.
502
505
503 :param environ: WSGI environment
506 :param environ: WSGI environment
504 """
507 """
505 raise NotImplementedError()
508 raise NotImplementedError()
506
509
507 def _create_wsgi_app(self, repo_path, repo_name, config):
510 def _create_wsgi_app(self, repo_path, repo_name, config):
508 """Return the WSGI app that will finally handle the request."""
511 """Return the WSGI app that will finally handle the request."""
509 raise NotImplementedError()
512 raise NotImplementedError()
510
513
511 def _create_config(self, extras, repo_name):
514 def _create_config(self, extras, repo_name):
512 """Create a safe config representation."""
515 """Create a safe config representation."""
513 raise NotImplementedError()
516 raise NotImplementedError()
514
517
515 def _prepare_callback_daemon(self, extras):
518 def _prepare_callback_daemon(self, extras):
516 return prepare_callback_daemon(
519 return prepare_callback_daemon(
517 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
520 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
518 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
521 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
519
522
520
523
521 def _should_check_locking(query_string):
524 def _should_check_locking(query_string):
522 # this is kind of hacky, but due to how mercurial handles client-server
525 # this is kind of hacky, but due to how mercurial handles client-server
523 # server see all operation on commit; bookmarks, phases and
526 # server see all operation on commit; bookmarks, phases and
524 # obsolescence marker in different transaction, we don't want to check
527 # obsolescence marker in different transaction, we don't want to check
525 # locking on those
528 # locking on those
526 return query_string not in ['cmd=listkeys']
529 return query_string not in ['cmd=listkeys']
@@ -1,3934 +1,3945 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 beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pylons import url
48 from pylons import url
49 from pylons.i18n.translation import lazy_ugettext as _
49 from pylons.i18n.translation import lazy_ugettext as _
50
50
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict, cleaned_uri)
56 glob2re, StrictAttributeDict, cleaned_uri)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 try:
211 try:
212 id_ = int(id_)
212 id_ = int(id_)
213 except (TypeError, ValueError):
213 except (TypeError, ValueError):
214 raise HTTPNotFound
214 raise HTTPNotFound
215
215
216 res = cls.query().get(id_)
216 res = cls.query().get(id_)
217 if not res:
217 if not res:
218 raise HTTPNotFound
218 raise HTTPNotFound
219 return res
219 return res
220
220
221 @classmethod
221 @classmethod
222 def getAll(cls):
222 def getAll(cls):
223 # deprecated and left for backward compatibility
223 # deprecated and left for backward compatibility
224 return cls.get_all()
224 return cls.get_all()
225
225
226 @classmethod
226 @classmethod
227 def get_all(cls):
227 def get_all(cls):
228 return cls.query().all()
228 return cls.query().all()
229
229
230 @classmethod
230 @classmethod
231 def delete(cls, id_):
231 def delete(cls, id_):
232 obj = cls.query().get(id_)
232 obj = cls.query().get(id_)
233 Session().delete(obj)
233 Session().delete(obj)
234
234
235 @classmethod
235 @classmethod
236 def identity_cache(cls, session, attr_name, value):
236 def identity_cache(cls, session, attr_name, value):
237 exist_in_session = []
237 exist_in_session = []
238 for (item_cls, pkey), instance in session.identity_map.items():
238 for (item_cls, pkey), instance in session.identity_map.items():
239 if cls == item_cls and getattr(instance, attr_name) == value:
239 if cls == item_cls and getattr(instance, attr_name) == value:
240 exist_in_session.append(instance)
240 exist_in_session.append(instance)
241 if exist_in_session:
241 if exist_in_session:
242 if len(exist_in_session) == 1:
242 if len(exist_in_session) == 1:
243 return exist_in_session[0]
243 return exist_in_session[0]
244 log.exception(
244 log.exception(
245 'multiple objects with attr %s and '
245 'multiple objects with attr %s and '
246 'value %s found with same name: %r',
246 'value %s found with same name: %r',
247 attr_name, value, exist_in_session)
247 attr_name, value, exist_in_session)
248
248
249 def __repr__(self):
249 def __repr__(self):
250 if hasattr(self, '__unicode__'):
250 if hasattr(self, '__unicode__'):
251 # python repr needs to return str
251 # python repr needs to return str
252 try:
252 try:
253 return safe_str(self.__unicode__())
253 return safe_str(self.__unicode__())
254 except UnicodeDecodeError:
254 except UnicodeDecodeError:
255 pass
255 pass
256 return '<DB:%s>' % (self.__class__.__name__)
256 return '<DB:%s>' % (self.__class__.__name__)
257
257
258
258
259 class RhodeCodeSetting(Base, BaseModel):
259 class RhodeCodeSetting(Base, BaseModel):
260 __tablename__ = 'rhodecode_settings'
260 __tablename__ = 'rhodecode_settings'
261 __table_args__ = (
261 __table_args__ = (
262 UniqueConstraint('app_settings_name'),
262 UniqueConstraint('app_settings_name'),
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 )
265 )
266
266
267 SETTINGS_TYPES = {
267 SETTINGS_TYPES = {
268 'str': safe_str,
268 'str': safe_str,
269 'int': safe_int,
269 'int': safe_int,
270 'unicode': safe_unicode,
270 'unicode': safe_unicode,
271 'bool': str2bool,
271 'bool': str2bool,
272 'list': functools.partial(aslist, sep=',')
272 'list': functools.partial(aslist, sep=',')
273 }
273 }
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 GLOBAL_CONF_KEY = 'app_settings'
275 GLOBAL_CONF_KEY = 'app_settings'
276
276
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281
281
282 def __init__(self, key='', val='', type='unicode'):
282 def __init__(self, key='', val='', type='unicode'):
283 self.app_settings_name = key
283 self.app_settings_name = key
284 self.app_settings_type = type
284 self.app_settings_type = type
285 self.app_settings_value = val
285 self.app_settings_value = val
286
286
287 @validates('_app_settings_value')
287 @validates('_app_settings_value')
288 def validate_settings_value(self, key, val):
288 def validate_settings_value(self, key, val):
289 assert type(val) == unicode
289 assert type(val) == unicode
290 return val
290 return val
291
291
292 @hybrid_property
292 @hybrid_property
293 def app_settings_value(self):
293 def app_settings_value(self):
294 v = self._app_settings_value
294 v = self._app_settings_value
295 _type = self.app_settings_type
295 _type = self.app_settings_type
296 if _type:
296 if _type:
297 _type = self.app_settings_type.split('.')[0]
297 _type = self.app_settings_type.split('.')[0]
298 # decode the encrypted value
298 # decode the encrypted value
299 if 'encrypted' in self.app_settings_type:
299 if 'encrypted' in self.app_settings_type:
300 cipher = EncryptedTextValue()
300 cipher = EncryptedTextValue()
301 v = safe_unicode(cipher.process_result_value(v, None))
301 v = safe_unicode(cipher.process_result_value(v, None))
302
302
303 converter = self.SETTINGS_TYPES.get(_type) or \
303 converter = self.SETTINGS_TYPES.get(_type) or \
304 self.SETTINGS_TYPES['unicode']
304 self.SETTINGS_TYPES['unicode']
305 return converter(v)
305 return converter(v)
306
306
307 @app_settings_value.setter
307 @app_settings_value.setter
308 def app_settings_value(self, val):
308 def app_settings_value(self, val):
309 """
309 """
310 Setter that will always make sure we use unicode in app_settings_value
310 Setter that will always make sure we use unicode in app_settings_value
311
311
312 :param val:
312 :param val:
313 """
313 """
314 val = safe_unicode(val)
314 val = safe_unicode(val)
315 # encode the encrypted value
315 # encode the encrypted value
316 if 'encrypted' in self.app_settings_type:
316 if 'encrypted' in self.app_settings_type:
317 cipher = EncryptedTextValue()
317 cipher = EncryptedTextValue()
318 val = safe_unicode(cipher.process_bind_param(val, None))
318 val = safe_unicode(cipher.process_bind_param(val, None))
319 self._app_settings_value = val
319 self._app_settings_value = val
320
320
321 @hybrid_property
321 @hybrid_property
322 def app_settings_type(self):
322 def app_settings_type(self):
323 return self._app_settings_type
323 return self._app_settings_type
324
324
325 @app_settings_type.setter
325 @app_settings_type.setter
326 def app_settings_type(self, val):
326 def app_settings_type(self, val):
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 raise Exception('type must be one of %s got %s'
328 raise Exception('type must be one of %s got %s'
329 % (self.SETTINGS_TYPES.keys(), val))
329 % (self.SETTINGS_TYPES.keys(), val))
330 self._app_settings_type = val
330 self._app_settings_type = val
331
331
332 def __unicode__(self):
332 def __unicode__(self):
333 return u"<%s('%s:%s[%s]')>" % (
333 return u"<%s('%s:%s[%s]')>" % (
334 self.__class__.__name__,
334 self.__class__.__name__,
335 self.app_settings_name, self.app_settings_value,
335 self.app_settings_name, self.app_settings_value,
336 self.app_settings_type
336 self.app_settings_type
337 )
337 )
338
338
339
339
340 class RhodeCodeUi(Base, BaseModel):
340 class RhodeCodeUi(Base, BaseModel):
341 __tablename__ = 'rhodecode_ui'
341 __tablename__ = 'rhodecode_ui'
342 __table_args__ = (
342 __table_args__ = (
343 UniqueConstraint('ui_key'),
343 UniqueConstraint('ui_key'),
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 )
346 )
347
347
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 # HG
349 # HG
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PULL = 'outgoing.pull_logger'
351 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 HOOK_PUSH = 'changegroup.push_logger'
354 HOOK_PUSH = 'changegroup.push_logger'
355
355
356 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 # TODO: johbo: Unify way how hooks are configured for git and hg,
357 # git part is currently hardcoded.
357 # git part is currently hardcoded.
358
358
359 # SVN PATTERNS
359 # SVN PATTERNS
360 SVN_BRANCH_ID = 'vcs_svn_branch'
360 SVN_BRANCH_ID = 'vcs_svn_branch'
361 SVN_TAG_ID = 'vcs_svn_tag'
361 SVN_TAG_ID = 'vcs_svn_tag'
362
362
363 ui_id = Column(
363 ui_id = Column(
364 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 "ui_id", Integer(), nullable=False, unique=True, default=None,
365 primary_key=True)
365 primary_key=True)
366 ui_section = Column(
366 ui_section = Column(
367 "ui_section", String(255), nullable=True, unique=None, default=None)
367 "ui_section", String(255), nullable=True, unique=None, default=None)
368 ui_key = Column(
368 ui_key = Column(
369 "ui_key", String(255), nullable=True, unique=None, default=None)
369 "ui_key", String(255), nullable=True, unique=None, default=None)
370 ui_value = Column(
370 ui_value = Column(
371 "ui_value", String(255), nullable=True, unique=None, default=None)
371 "ui_value", String(255), nullable=True, unique=None, default=None)
372 ui_active = Column(
372 ui_active = Column(
373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
374
374
375 def __repr__(self):
375 def __repr__(self):
376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
377 self.ui_key, self.ui_value)
377 self.ui_key, self.ui_value)
378
378
379
379
380 class RepoRhodeCodeSetting(Base, BaseModel):
380 class RepoRhodeCodeSetting(Base, BaseModel):
381 __tablename__ = 'repo_rhodecode_settings'
381 __tablename__ = 'repo_rhodecode_settings'
382 __table_args__ = (
382 __table_args__ = (
383 UniqueConstraint(
383 UniqueConstraint(
384 'app_settings_name', 'repository_id',
384 'app_settings_name', 'repository_id',
385 name='uq_repo_rhodecode_setting_name_repo_id'),
385 name='uq_repo_rhodecode_setting_name_repo_id'),
386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 )
388 )
389
389
390 repository_id = Column(
390 repository_id = Column(
391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
392 nullable=False)
392 nullable=False)
393 app_settings_id = Column(
393 app_settings_id = Column(
394 "app_settings_id", Integer(), nullable=False, unique=True,
394 "app_settings_id", Integer(), nullable=False, unique=True,
395 default=None, primary_key=True)
395 default=None, primary_key=True)
396 app_settings_name = Column(
396 app_settings_name = Column(
397 "app_settings_name", String(255), nullable=True, unique=None,
397 "app_settings_name", String(255), nullable=True, unique=None,
398 default=None)
398 default=None)
399 _app_settings_value = Column(
399 _app_settings_value = Column(
400 "app_settings_value", String(4096), nullable=True, unique=None,
400 "app_settings_value", String(4096), nullable=True, unique=None,
401 default=None)
401 default=None)
402 _app_settings_type = Column(
402 _app_settings_type = Column(
403 "app_settings_type", String(255), nullable=True, unique=None,
403 "app_settings_type", String(255), nullable=True, unique=None,
404 default=None)
404 default=None)
405
405
406 repository = relationship('Repository')
406 repository = relationship('Repository')
407
407
408 def __init__(self, repository_id, key='', val='', type='unicode'):
408 def __init__(self, repository_id, key='', val='', type='unicode'):
409 self.repository_id = repository_id
409 self.repository_id = repository_id
410 self.app_settings_name = key
410 self.app_settings_name = key
411 self.app_settings_type = type
411 self.app_settings_type = type
412 self.app_settings_value = val
412 self.app_settings_value = val
413
413
414 @validates('_app_settings_value')
414 @validates('_app_settings_value')
415 def validate_settings_value(self, key, val):
415 def validate_settings_value(self, key, val):
416 assert type(val) == unicode
416 assert type(val) == unicode
417 return val
417 return val
418
418
419 @hybrid_property
419 @hybrid_property
420 def app_settings_value(self):
420 def app_settings_value(self):
421 v = self._app_settings_value
421 v = self._app_settings_value
422 type_ = self.app_settings_type
422 type_ = self.app_settings_type
423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
425 return converter(v)
425 return converter(v)
426
426
427 @app_settings_value.setter
427 @app_settings_value.setter
428 def app_settings_value(self, val):
428 def app_settings_value(self, val):
429 """
429 """
430 Setter that will always make sure we use unicode in app_settings_value
430 Setter that will always make sure we use unicode in app_settings_value
431
431
432 :param val:
432 :param val:
433 """
433 """
434 self._app_settings_value = safe_unicode(val)
434 self._app_settings_value = safe_unicode(val)
435
435
436 @hybrid_property
436 @hybrid_property
437 def app_settings_type(self):
437 def app_settings_type(self):
438 return self._app_settings_type
438 return self._app_settings_type
439
439
440 @app_settings_type.setter
440 @app_settings_type.setter
441 def app_settings_type(self, val):
441 def app_settings_type(self, val):
442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
443 if val not in SETTINGS_TYPES:
443 if val not in SETTINGS_TYPES:
444 raise Exception('type must be one of %s got %s'
444 raise Exception('type must be one of %s got %s'
445 % (SETTINGS_TYPES.keys(), val))
445 % (SETTINGS_TYPES.keys(), val))
446 self._app_settings_type = val
446 self._app_settings_type = val
447
447
448 def __unicode__(self):
448 def __unicode__(self):
449 return u"<%s('%s:%s:%s[%s]')>" % (
449 return u"<%s('%s:%s:%s[%s]')>" % (
450 self.__class__.__name__, self.repository.repo_name,
450 self.__class__.__name__, self.repository.repo_name,
451 self.app_settings_name, self.app_settings_value,
451 self.app_settings_name, self.app_settings_value,
452 self.app_settings_type
452 self.app_settings_type
453 )
453 )
454
454
455
455
456 class RepoRhodeCodeUi(Base, BaseModel):
456 class RepoRhodeCodeUi(Base, BaseModel):
457 __tablename__ = 'repo_rhodecode_ui'
457 __tablename__ = 'repo_rhodecode_ui'
458 __table_args__ = (
458 __table_args__ = (
459 UniqueConstraint(
459 UniqueConstraint(
460 'repository_id', 'ui_section', 'ui_key',
460 'repository_id', 'ui_section', 'ui_key',
461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
464 )
464 )
465
465
466 repository_id = Column(
466 repository_id = Column(
467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
468 nullable=False)
468 nullable=False)
469 ui_id = Column(
469 ui_id = Column(
470 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 "ui_id", Integer(), nullable=False, unique=True, default=None,
471 primary_key=True)
471 primary_key=True)
472 ui_section = Column(
472 ui_section = Column(
473 "ui_section", String(255), nullable=True, unique=None, default=None)
473 "ui_section", String(255), nullable=True, unique=None, default=None)
474 ui_key = Column(
474 ui_key = Column(
475 "ui_key", String(255), nullable=True, unique=None, default=None)
475 "ui_key", String(255), nullable=True, unique=None, default=None)
476 ui_value = Column(
476 ui_value = Column(
477 "ui_value", String(255), nullable=True, unique=None, default=None)
477 "ui_value", String(255), nullable=True, unique=None, default=None)
478 ui_active = Column(
478 ui_active = Column(
479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
480
480
481 repository = relationship('Repository')
481 repository = relationship('Repository')
482
482
483 def __repr__(self):
483 def __repr__(self):
484 return '<%s[%s:%s]%s=>%s]>' % (
484 return '<%s[%s:%s]%s=>%s]>' % (
485 self.__class__.__name__, self.repository.repo_name,
485 self.__class__.__name__, self.repository.repo_name,
486 self.ui_section, self.ui_key, self.ui_value)
486 self.ui_section, self.ui_key, self.ui_value)
487
487
488
488
489 class User(Base, BaseModel):
489 class User(Base, BaseModel):
490 __tablename__ = 'users'
490 __tablename__ = 'users'
491 __table_args__ = (
491 __table_args__ = (
492 UniqueConstraint('username'), UniqueConstraint('email'),
492 UniqueConstraint('username'), UniqueConstraint('email'),
493 Index('u_username_idx', 'username'),
493 Index('u_username_idx', 'username'),
494 Index('u_email_idx', 'email'),
494 Index('u_email_idx', 'email'),
495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 )
497 )
498 DEFAULT_USER = 'default'
498 DEFAULT_USER = 'default'
499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
501
501
502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
503 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 username = Column("username", String(255), nullable=True, unique=None, default=None)
504 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 password = Column("password", String(255), nullable=True, unique=None, default=None)
505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
513 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
517
517
518 user_log = relationship('UserLog')
518 user_log = relationship('UserLog')
519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
520
520
521 repositories = relationship('Repository')
521 repositories = relationship('Repository')
522 repository_groups = relationship('RepoGroup')
522 repository_groups = relationship('RepoGroup')
523 user_groups = relationship('UserGroup')
523 user_groups = relationship('UserGroup')
524
524
525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
527
527
528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
531
531
532 group_member = relationship('UserGroupMember', cascade='all')
532 group_member = relationship('UserGroupMember', cascade='all')
533
533
534 notifications = relationship('UserNotification', cascade='all')
534 notifications = relationship('UserNotification', cascade='all')
535 # notifications assigned to this user
535 # notifications assigned to this user
536 user_created_notifications = relationship('Notification', cascade='all')
536 user_created_notifications = relationship('Notification', cascade='all')
537 # comments created by this user
537 # comments created by this user
538 user_comments = relationship('ChangesetComment', cascade='all')
538 user_comments = relationship('ChangesetComment', cascade='all')
539 # user profile extra info
539 # user profile extra info
540 user_emails = relationship('UserEmailMap', cascade='all')
540 user_emails = relationship('UserEmailMap', cascade='all')
541 user_ip_map = relationship('UserIpMap', cascade='all')
541 user_ip_map = relationship('UserIpMap', cascade='all')
542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
543 # gists
543 # gists
544 user_gists = relationship('Gist', cascade='all')
544 user_gists = relationship('Gist', cascade='all')
545 # user pull requests
545 # user pull requests
546 user_pull_requests = relationship('PullRequest', cascade='all')
546 user_pull_requests = relationship('PullRequest', cascade='all')
547 # external identities
547 # external identities
548 extenal_identities = relationship(
548 extenal_identities = relationship(
549 'ExternalIdentity',
549 'ExternalIdentity',
550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
551 cascade='all')
551 cascade='all')
552
552
553 def __unicode__(self):
553 def __unicode__(self):
554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
555 self.user_id, self.username)
555 self.user_id, self.username)
556
556
557 @hybrid_property
557 @hybrid_property
558 def email(self):
558 def email(self):
559 return self._email
559 return self._email
560
560
561 @email.setter
561 @email.setter
562 def email(self, val):
562 def email(self, val):
563 self._email = val.lower() if val else None
563 self._email = val.lower() if val else None
564
564
565 @hybrid_property
565 @hybrid_property
566 def api_key(self):
566 def api_key(self):
567 """
567 """
568 Fetch if exist an auth-token with role ALL connected to this user
568 Fetch if exist an auth-token with role ALL connected to this user
569 """
569 """
570 user_auth_token = UserApiKeys.query()\
570 user_auth_token = UserApiKeys.query()\
571 .filter(UserApiKeys.user_id == self.user_id)\
571 .filter(UserApiKeys.user_id == self.user_id)\
572 .filter(or_(UserApiKeys.expires == -1,
572 .filter(or_(UserApiKeys.expires == -1,
573 UserApiKeys.expires >= time.time()))\
573 UserApiKeys.expires >= time.time()))\
574 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
574 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
575 if user_auth_token:
575 if user_auth_token:
576 user_auth_token = user_auth_token.api_key
576 user_auth_token = user_auth_token.api_key
577
577
578 return user_auth_token
578 return user_auth_token
579
579
580 @api_key.setter
580 @api_key.setter
581 def api_key(self, val):
581 def api_key(self, val):
582 # don't allow to set API key this is deprecated for now
582 # don't allow to set API key this is deprecated for now
583 self._api_key = None
583 self._api_key = None
584
584
585 @property
585 @property
586 def firstname(self):
586 def firstname(self):
587 # alias for future
587 # alias for future
588 return self.name
588 return self.name
589
589
590 @property
590 @property
591 def emails(self):
591 def emails(self):
592 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
592 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
593 return [self.email] + [x.email for x in other]
593 return [self.email] + [x.email for x in other]
594
594
595 @property
595 @property
596 def auth_tokens(self):
596 def auth_tokens(self):
597 return [x.api_key for x in self.extra_auth_tokens]
597 return [x.api_key for x in self.extra_auth_tokens]
598
598
599 @property
599 @property
600 def extra_auth_tokens(self):
600 def extra_auth_tokens(self):
601 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
601 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
602
602
603 @property
603 @property
604 def feed_token(self):
604 def feed_token(self):
605 return self.get_feed_token()
605 return self.get_feed_token()
606
606
607 def get_feed_token(self):
607 def get_feed_token(self):
608 feed_tokens = UserApiKeys.query()\
608 feed_tokens = UserApiKeys.query()\
609 .filter(UserApiKeys.user == self)\
609 .filter(UserApiKeys.user == self)\
610 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
610 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
611 .all()
611 .all()
612 if feed_tokens:
612 if feed_tokens:
613 return feed_tokens[0].api_key
613 return feed_tokens[0].api_key
614 return 'NO_FEED_TOKEN_AVAILABLE'
614 return 'NO_FEED_TOKEN_AVAILABLE'
615
615
616 @classmethod
616 @classmethod
617 def extra_valid_auth_tokens(cls, user, role=None):
617 def extra_valid_auth_tokens(cls, user, role=None):
618 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
618 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
619 .filter(or_(UserApiKeys.expires == -1,
619 .filter(or_(UserApiKeys.expires == -1,
620 UserApiKeys.expires >= time.time()))
620 UserApiKeys.expires >= time.time()))
621 if role:
621 if role:
622 tokens = tokens.filter(or_(UserApiKeys.role == role,
622 tokens = tokens.filter(or_(UserApiKeys.role == role,
623 UserApiKeys.role == UserApiKeys.ROLE_ALL))
623 UserApiKeys.role == UserApiKeys.ROLE_ALL))
624 return tokens.all()
624 return tokens.all()
625
625
626 def authenticate_by_token(self, auth_token, roles=None):
626 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
627 from rhodecode.lib import auth
627 from rhodecode.lib import auth
628
628
629 log.debug('Trying to authenticate user: %s via auth-token, '
629 log.debug('Trying to authenticate user: %s via auth-token, '
630 'and roles: %s', self, roles)
630 'and roles: %s', self, roles)
631
631
632 if not auth_token:
632 if not auth_token:
633 return False
633 return False
634
634
635 crypto_backend = auth.crypto_backend()
635 crypto_backend = auth.crypto_backend()
636
636
637 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
637 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
638 tokens_q = UserApiKeys.query()\
638 tokens_q = UserApiKeys.query()\
639 .filter(UserApiKeys.user_id == self.user_id)\
639 .filter(UserApiKeys.user_id == self.user_id)\
640 .filter(or_(UserApiKeys.expires == -1,
640 .filter(or_(UserApiKeys.expires == -1,
641 UserApiKeys.expires >= time.time()))
641 UserApiKeys.expires >= time.time()))
642
642
643 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
643 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
644
644
645 plain_tokens = []
645 plain_tokens = []
646 hash_tokens = []
646 hash_tokens = []
647
647
648 for token in tokens_q.all():
648 for token in tokens_q.all():
649 # verify scope first
650 if token.repo_id:
651 # token has a scope, we need to verify it
652 if scope_repo_id != token.repo_id:
653 log.debug(
654 'Scope mismatch: token has a set repo scope: %s, '
655 'and calling scope is:%s, skipping further checks',
656 token.repo, scope_repo_id)
657 # token has a scope, and it doesn't match, skip token
658 continue
659
649 if token.api_key.startswith(crypto_backend.ENC_PREF):
660 if token.api_key.startswith(crypto_backend.ENC_PREF):
650 hash_tokens.append(token.api_key)
661 hash_tokens.append(token.api_key)
651 else:
662 else:
652 plain_tokens.append(token.api_key)
663 plain_tokens.append(token.api_key)
653
664
654 is_plain_match = auth_token in plain_tokens
665 is_plain_match = auth_token in plain_tokens
655 if is_plain_match:
666 if is_plain_match:
656 return True
667 return True
657
668
658 for hashed in hash_tokens:
669 for hashed in hash_tokens:
659 # marcink: this is expensive to calculate, but the most secure
670 # TODO(marcink): this is expensive to calculate, but most secure
660 match = crypto_backend.hash_check(auth_token, hashed)
671 match = crypto_backend.hash_check(auth_token, hashed)
661 if match:
672 if match:
662 return True
673 return True
663
674
664 return False
675 return False
665
676
666 @property
677 @property
667 def ip_addresses(self):
678 def ip_addresses(self):
668 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
679 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
669 return [x.ip_addr for x in ret]
680 return [x.ip_addr for x in ret]
670
681
671 @property
682 @property
672 def username_and_name(self):
683 def username_and_name(self):
673 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
684 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
674
685
675 @property
686 @property
676 def username_or_name_or_email(self):
687 def username_or_name_or_email(self):
677 full_name = self.full_name if self.full_name is not ' ' else None
688 full_name = self.full_name if self.full_name is not ' ' else None
678 return self.username or full_name or self.email
689 return self.username or full_name or self.email
679
690
680 @property
691 @property
681 def full_name(self):
692 def full_name(self):
682 return '%s %s' % (self.firstname, self.lastname)
693 return '%s %s' % (self.firstname, self.lastname)
683
694
684 @property
695 @property
685 def full_name_or_username(self):
696 def full_name_or_username(self):
686 return ('%s %s' % (self.firstname, self.lastname)
697 return ('%s %s' % (self.firstname, self.lastname)
687 if (self.firstname and self.lastname) else self.username)
698 if (self.firstname and self.lastname) else self.username)
688
699
689 @property
700 @property
690 def full_contact(self):
701 def full_contact(self):
691 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
702 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
692
703
693 @property
704 @property
694 def short_contact(self):
705 def short_contact(self):
695 return '%s %s' % (self.firstname, self.lastname)
706 return '%s %s' % (self.firstname, self.lastname)
696
707
697 @property
708 @property
698 def is_admin(self):
709 def is_admin(self):
699 return self.admin
710 return self.admin
700
711
701 @property
712 @property
702 def AuthUser(self):
713 def AuthUser(self):
703 """
714 """
704 Returns instance of AuthUser for this user
715 Returns instance of AuthUser for this user
705 """
716 """
706 from rhodecode.lib.auth import AuthUser
717 from rhodecode.lib.auth import AuthUser
707 return AuthUser(user_id=self.user_id, username=self.username)
718 return AuthUser(user_id=self.user_id, username=self.username)
708
719
709 @hybrid_property
720 @hybrid_property
710 def user_data(self):
721 def user_data(self):
711 if not self._user_data:
722 if not self._user_data:
712 return {}
723 return {}
713
724
714 try:
725 try:
715 return json.loads(self._user_data)
726 return json.loads(self._user_data)
716 except TypeError:
727 except TypeError:
717 return {}
728 return {}
718
729
719 @user_data.setter
730 @user_data.setter
720 def user_data(self, val):
731 def user_data(self, val):
721 if not isinstance(val, dict):
732 if not isinstance(val, dict):
722 raise Exception('user_data must be dict, got %s' % type(val))
733 raise Exception('user_data must be dict, got %s' % type(val))
723 try:
734 try:
724 self._user_data = json.dumps(val)
735 self._user_data = json.dumps(val)
725 except Exception:
736 except Exception:
726 log.error(traceback.format_exc())
737 log.error(traceback.format_exc())
727
738
728 @classmethod
739 @classmethod
729 def get_by_username(cls, username, case_insensitive=False,
740 def get_by_username(cls, username, case_insensitive=False,
730 cache=False, identity_cache=False):
741 cache=False, identity_cache=False):
731 session = Session()
742 session = Session()
732
743
733 if case_insensitive:
744 if case_insensitive:
734 q = cls.query().filter(
745 q = cls.query().filter(
735 func.lower(cls.username) == func.lower(username))
746 func.lower(cls.username) == func.lower(username))
736 else:
747 else:
737 q = cls.query().filter(cls.username == username)
748 q = cls.query().filter(cls.username == username)
738
749
739 if cache:
750 if cache:
740 if identity_cache:
751 if identity_cache:
741 val = cls.identity_cache(session, 'username', username)
752 val = cls.identity_cache(session, 'username', username)
742 if val:
753 if val:
743 return val
754 return val
744 else:
755 else:
745 q = q.options(
756 q = q.options(
746 FromCache("sql_cache_short",
757 FromCache("sql_cache_short",
747 "get_user_by_name_%s" % _hash_key(username)))
758 "get_user_by_name_%s" % _hash_key(username)))
748
759
749 return q.scalar()
760 return q.scalar()
750
761
751 @classmethod
762 @classmethod
752 def get_by_auth_token(cls, auth_token, cache=False):
763 def get_by_auth_token(cls, auth_token, cache=False):
753 q = UserApiKeys.query()\
764 q = UserApiKeys.query()\
754 .filter(UserApiKeys.api_key == auth_token)\
765 .filter(UserApiKeys.api_key == auth_token)\
755 .filter(or_(UserApiKeys.expires == -1,
766 .filter(or_(UserApiKeys.expires == -1,
756 UserApiKeys.expires >= time.time()))
767 UserApiKeys.expires >= time.time()))
757 if cache:
768 if cache:
758 q = q.options(FromCache("sql_cache_short",
769 q = q.options(FromCache("sql_cache_short",
759 "get_auth_token_%s" % auth_token))
770 "get_auth_token_%s" % auth_token))
760
771
761 match = q.first()
772 match = q.first()
762 if match:
773 if match:
763 return match.user
774 return match.user
764
775
765 @classmethod
776 @classmethod
766 def get_by_email(cls, email, case_insensitive=False, cache=False):
777 def get_by_email(cls, email, case_insensitive=False, cache=False):
767
778
768 if case_insensitive:
779 if case_insensitive:
769 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
780 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
770
781
771 else:
782 else:
772 q = cls.query().filter(cls.email == email)
783 q = cls.query().filter(cls.email == email)
773
784
774 if cache:
785 if cache:
775 q = q.options(FromCache("sql_cache_short",
786 q = q.options(FromCache("sql_cache_short",
776 "get_email_key_%s" % _hash_key(email)))
787 "get_email_key_%s" % _hash_key(email)))
777
788
778 ret = q.scalar()
789 ret = q.scalar()
779 if ret is None:
790 if ret is None:
780 q = UserEmailMap.query()
791 q = UserEmailMap.query()
781 # try fetching in alternate email map
792 # try fetching in alternate email map
782 if case_insensitive:
793 if case_insensitive:
783 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
794 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
784 else:
795 else:
785 q = q.filter(UserEmailMap.email == email)
796 q = q.filter(UserEmailMap.email == email)
786 q = q.options(joinedload(UserEmailMap.user))
797 q = q.options(joinedload(UserEmailMap.user))
787 if cache:
798 if cache:
788 q = q.options(FromCache("sql_cache_short",
799 q = q.options(FromCache("sql_cache_short",
789 "get_email_map_key_%s" % email))
800 "get_email_map_key_%s" % email))
790 ret = getattr(q.scalar(), 'user', None)
801 ret = getattr(q.scalar(), 'user', None)
791
802
792 return ret
803 return ret
793
804
794 @classmethod
805 @classmethod
795 def get_from_cs_author(cls, author):
806 def get_from_cs_author(cls, author):
796 """
807 """
797 Tries to get User objects out of commit author string
808 Tries to get User objects out of commit author string
798
809
799 :param author:
810 :param author:
800 """
811 """
801 from rhodecode.lib.helpers import email, author_name
812 from rhodecode.lib.helpers import email, author_name
802 # Valid email in the attribute passed, see if they're in the system
813 # Valid email in the attribute passed, see if they're in the system
803 _email = email(author)
814 _email = email(author)
804 if _email:
815 if _email:
805 user = cls.get_by_email(_email, case_insensitive=True)
816 user = cls.get_by_email(_email, case_insensitive=True)
806 if user:
817 if user:
807 return user
818 return user
808 # Maybe we can match by username?
819 # Maybe we can match by username?
809 _author = author_name(author)
820 _author = author_name(author)
810 user = cls.get_by_username(_author, case_insensitive=True)
821 user = cls.get_by_username(_author, case_insensitive=True)
811 if user:
822 if user:
812 return user
823 return user
813
824
814 def update_userdata(self, **kwargs):
825 def update_userdata(self, **kwargs):
815 usr = self
826 usr = self
816 old = usr.user_data
827 old = usr.user_data
817 old.update(**kwargs)
828 old.update(**kwargs)
818 usr.user_data = old
829 usr.user_data = old
819 Session().add(usr)
830 Session().add(usr)
820 log.debug('updated userdata with ', kwargs)
831 log.debug('updated userdata with ', kwargs)
821
832
822 def update_lastlogin(self):
833 def update_lastlogin(self):
823 """Update user lastlogin"""
834 """Update user lastlogin"""
824 self.last_login = datetime.datetime.now()
835 self.last_login = datetime.datetime.now()
825 Session().add(self)
836 Session().add(self)
826 log.debug('updated user %s lastlogin', self.username)
837 log.debug('updated user %s lastlogin', self.username)
827
838
828 def update_lastactivity(self):
839 def update_lastactivity(self):
829 """Update user lastactivity"""
840 """Update user lastactivity"""
830 usr = self
841 usr = self
831 old = usr.user_data
842 old = usr.user_data
832 old.update({'last_activity': time.time()})
843 old.update({'last_activity': time.time()})
833 usr.user_data = old
844 usr.user_data = old
834 Session().add(usr)
845 Session().add(usr)
835 log.debug('updated user %s lastactivity', usr.username)
846 log.debug('updated user %s lastactivity', usr.username)
836
847
837 def update_password(self, new_password):
848 def update_password(self, new_password):
838 from rhodecode.lib.auth import get_crypt_password
849 from rhodecode.lib.auth import get_crypt_password
839
850
840 self.password = get_crypt_password(new_password)
851 self.password = get_crypt_password(new_password)
841 Session().add(self)
852 Session().add(self)
842
853
843 @classmethod
854 @classmethod
844 def get_first_super_admin(cls):
855 def get_first_super_admin(cls):
845 user = User.query().filter(User.admin == true()).first()
856 user = User.query().filter(User.admin == true()).first()
846 if user is None:
857 if user is None:
847 raise Exception('FATAL: Missing administrative account!')
858 raise Exception('FATAL: Missing administrative account!')
848 return user
859 return user
849
860
850 @classmethod
861 @classmethod
851 def get_all_super_admins(cls):
862 def get_all_super_admins(cls):
852 """
863 """
853 Returns all admin accounts sorted by username
864 Returns all admin accounts sorted by username
854 """
865 """
855 return User.query().filter(User.admin == true())\
866 return User.query().filter(User.admin == true())\
856 .order_by(User.username.asc()).all()
867 .order_by(User.username.asc()).all()
857
868
858 @classmethod
869 @classmethod
859 def get_default_user(cls, cache=False):
870 def get_default_user(cls, cache=False):
860 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
871 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
861 if user is None:
872 if user is None:
862 raise Exception('FATAL: Missing default account!')
873 raise Exception('FATAL: Missing default account!')
863 return user
874 return user
864
875
865 def _get_default_perms(self, user, suffix=''):
876 def _get_default_perms(self, user, suffix=''):
866 from rhodecode.model.permission import PermissionModel
877 from rhodecode.model.permission import PermissionModel
867 return PermissionModel().get_default_perms(user.user_perms, suffix)
878 return PermissionModel().get_default_perms(user.user_perms, suffix)
868
879
869 def get_default_perms(self, suffix=''):
880 def get_default_perms(self, suffix=''):
870 return self._get_default_perms(self, suffix)
881 return self._get_default_perms(self, suffix)
871
882
872 def get_api_data(self, include_secrets=False, details='full'):
883 def get_api_data(self, include_secrets=False, details='full'):
873 """
884 """
874 Common function for generating user related data for API
885 Common function for generating user related data for API
875
886
876 :param include_secrets: By default secrets in the API data will be replaced
887 :param include_secrets: By default secrets in the API data will be replaced
877 by a placeholder value to prevent exposing this data by accident. In case
888 by a placeholder value to prevent exposing this data by accident. In case
878 this data shall be exposed, set this flag to ``True``.
889 this data shall be exposed, set this flag to ``True``.
879
890
880 :param details: details can be 'basic|full' basic gives only a subset of
891 :param details: details can be 'basic|full' basic gives only a subset of
881 the available user information that includes user_id, name and emails.
892 the available user information that includes user_id, name and emails.
882 """
893 """
883 user = self
894 user = self
884 user_data = self.user_data
895 user_data = self.user_data
885 data = {
896 data = {
886 'user_id': user.user_id,
897 'user_id': user.user_id,
887 'username': user.username,
898 'username': user.username,
888 'firstname': user.name,
899 'firstname': user.name,
889 'lastname': user.lastname,
900 'lastname': user.lastname,
890 'email': user.email,
901 'email': user.email,
891 'emails': user.emails,
902 'emails': user.emails,
892 }
903 }
893 if details == 'basic':
904 if details == 'basic':
894 return data
905 return data
895
906
896 api_key_length = 40
907 api_key_length = 40
897 api_key_replacement = '*' * api_key_length
908 api_key_replacement = '*' * api_key_length
898
909
899 extras = {
910 extras = {
900 'api_keys': [api_key_replacement],
911 'api_keys': [api_key_replacement],
901 'active': user.active,
912 'active': user.active,
902 'admin': user.admin,
913 'admin': user.admin,
903 'extern_type': user.extern_type,
914 'extern_type': user.extern_type,
904 'extern_name': user.extern_name,
915 'extern_name': user.extern_name,
905 'last_login': user.last_login,
916 'last_login': user.last_login,
906 'ip_addresses': user.ip_addresses,
917 'ip_addresses': user.ip_addresses,
907 'language': user_data.get('language')
918 'language': user_data.get('language')
908 }
919 }
909 data.update(extras)
920 data.update(extras)
910
921
911 if include_secrets:
922 if include_secrets:
912 data['api_keys'] = user.auth_tokens
923 data['api_keys'] = user.auth_tokens
913 return data
924 return data
914
925
915 def __json__(self):
926 def __json__(self):
916 data = {
927 data = {
917 'full_name': self.full_name,
928 'full_name': self.full_name,
918 'full_name_or_username': self.full_name_or_username,
929 'full_name_or_username': self.full_name_or_username,
919 'short_contact': self.short_contact,
930 'short_contact': self.short_contact,
920 'full_contact': self.full_contact,
931 'full_contact': self.full_contact,
921 }
932 }
922 data.update(self.get_api_data())
933 data.update(self.get_api_data())
923 return data
934 return data
924
935
925
936
926 class UserApiKeys(Base, BaseModel):
937 class UserApiKeys(Base, BaseModel):
927 __tablename__ = 'user_api_keys'
938 __tablename__ = 'user_api_keys'
928 __table_args__ = (
939 __table_args__ = (
929 Index('uak_api_key_idx', 'api_key'),
940 Index('uak_api_key_idx', 'api_key'),
930 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
941 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
931 UniqueConstraint('api_key'),
942 UniqueConstraint('api_key'),
932 {'extend_existing': True, 'mysql_engine': 'InnoDB',
943 {'extend_existing': True, 'mysql_engine': 'InnoDB',
933 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
944 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
934 )
945 )
935 __mapper_args__ = {}
946 __mapper_args__ = {}
936
947
937 # ApiKey role
948 # ApiKey role
938 ROLE_ALL = 'token_role_all'
949 ROLE_ALL = 'token_role_all'
939 ROLE_HTTP = 'token_role_http'
950 ROLE_HTTP = 'token_role_http'
940 ROLE_VCS = 'token_role_vcs'
951 ROLE_VCS = 'token_role_vcs'
941 ROLE_API = 'token_role_api'
952 ROLE_API = 'token_role_api'
942 ROLE_FEED = 'token_role_feed'
953 ROLE_FEED = 'token_role_feed'
943 ROLE_PASSWORD_RESET = 'token_password_reset'
954 ROLE_PASSWORD_RESET = 'token_password_reset'
944
955
945 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
956 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
946
957
947 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
958 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
948 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
959 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
949 api_key = Column("api_key", String(255), nullable=False, unique=True)
960 api_key = Column("api_key", String(255), nullable=False, unique=True)
950 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
961 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
951 expires = Column('expires', Float(53), nullable=False)
962 expires = Column('expires', Float(53), nullable=False)
952 role = Column('role', String(255), nullable=True)
963 role = Column('role', String(255), nullable=True)
953 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
954
965
955 # scope columns
966 # scope columns
956 repo_id = Column(
967 repo_id = Column(
957 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
968 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
958 nullable=True, unique=None, default=None)
969 nullable=True, unique=None, default=None)
959 repo = relationship('Repository', lazy='joined')
970 repo = relationship('Repository', lazy='joined')
960
971
961 repo_group_id = Column(
972 repo_group_id = Column(
962 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
973 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
963 nullable=True, unique=None, default=None)
974 nullable=True, unique=None, default=None)
964 repo_group = relationship('RepoGroup', lazy='joined')
975 repo_group = relationship('RepoGroup', lazy='joined')
965
976
966 user = relationship('User', lazy='joined')
977 user = relationship('User', lazy='joined')
967
978
968 def __unicode__(self):
979 def __unicode__(self):
969 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
980 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
970
981
971 @classmethod
982 @classmethod
972 def _get_role_name(cls, role):
983 def _get_role_name(cls, role):
973 return {
984 return {
974 cls.ROLE_ALL: _('all'),
985 cls.ROLE_ALL: _('all'),
975 cls.ROLE_HTTP: _('http/web interface'),
986 cls.ROLE_HTTP: _('http/web interface'),
976 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
987 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
977 cls.ROLE_API: _('api calls'),
988 cls.ROLE_API: _('api calls'),
978 cls.ROLE_FEED: _('feed access'),
989 cls.ROLE_FEED: _('feed access'),
979 }.get(role, role)
990 }.get(role, role)
980
991
981 @property
992 @property
982 def expired(self):
993 def expired(self):
983 if self.expires == -1:
994 if self.expires == -1:
984 return False
995 return False
985 return time.time() > self.expires
996 return time.time() > self.expires
986
997
987 @property
998 @property
988 def role_humanized(self):
999 def role_humanized(self):
989 return self._get_role_name(self.role)
1000 return self._get_role_name(self.role)
990
1001
991 def _get_scope(self):
1002 def _get_scope(self):
992 if self.repo:
1003 if self.repo:
993 return repr(self.repo)
1004 return repr(self.repo)
994 if self.repo_group:
1005 if self.repo_group:
995 return repr(self.repo_group) + ' (recursive)'
1006 return repr(self.repo_group) + ' (recursive)'
996 return 'global'
1007 return 'global'
997
1008
998 @property
1009 @property
999 def scope_humanized(self):
1010 def scope_humanized(self):
1000 return self._get_scope()
1011 return self._get_scope()
1001
1012
1002
1013
1003 class UserEmailMap(Base, BaseModel):
1014 class UserEmailMap(Base, BaseModel):
1004 __tablename__ = 'user_email_map'
1015 __tablename__ = 'user_email_map'
1005 __table_args__ = (
1016 __table_args__ = (
1006 Index('uem_email_idx', 'email'),
1017 Index('uem_email_idx', 'email'),
1007 UniqueConstraint('email'),
1018 UniqueConstraint('email'),
1008 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1009 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1010 )
1021 )
1011 __mapper_args__ = {}
1022 __mapper_args__ = {}
1012
1023
1013 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1014 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1025 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1015 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1026 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1016 user = relationship('User', lazy='joined')
1027 user = relationship('User', lazy='joined')
1017
1028
1018 @validates('_email')
1029 @validates('_email')
1019 def validate_email(self, key, email):
1030 def validate_email(self, key, email):
1020 # check if this email is not main one
1031 # check if this email is not main one
1021 main_email = Session().query(User).filter(User.email == email).scalar()
1032 main_email = Session().query(User).filter(User.email == email).scalar()
1022 if main_email is not None:
1033 if main_email is not None:
1023 raise AttributeError('email %s is present is user table' % email)
1034 raise AttributeError('email %s is present is user table' % email)
1024 return email
1035 return email
1025
1036
1026 @hybrid_property
1037 @hybrid_property
1027 def email(self):
1038 def email(self):
1028 return self._email
1039 return self._email
1029
1040
1030 @email.setter
1041 @email.setter
1031 def email(self, val):
1042 def email(self, val):
1032 self._email = val.lower() if val else None
1043 self._email = val.lower() if val else None
1033
1044
1034
1045
1035 class UserIpMap(Base, BaseModel):
1046 class UserIpMap(Base, BaseModel):
1036 __tablename__ = 'user_ip_map'
1047 __tablename__ = 'user_ip_map'
1037 __table_args__ = (
1048 __table_args__ = (
1038 UniqueConstraint('user_id', 'ip_addr'),
1049 UniqueConstraint('user_id', 'ip_addr'),
1039 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1050 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1040 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1051 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1041 )
1052 )
1042 __mapper_args__ = {}
1053 __mapper_args__ = {}
1043
1054
1044 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1055 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1045 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1056 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1046 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1057 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1047 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1058 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1048 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1059 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1049 user = relationship('User', lazy='joined')
1060 user = relationship('User', lazy='joined')
1050
1061
1051 @classmethod
1062 @classmethod
1052 def _get_ip_range(cls, ip_addr):
1063 def _get_ip_range(cls, ip_addr):
1053 net = ipaddress.ip_network(ip_addr, strict=False)
1064 net = ipaddress.ip_network(ip_addr, strict=False)
1054 return [str(net.network_address), str(net.broadcast_address)]
1065 return [str(net.network_address), str(net.broadcast_address)]
1055
1066
1056 def __json__(self):
1067 def __json__(self):
1057 return {
1068 return {
1058 'ip_addr': self.ip_addr,
1069 'ip_addr': self.ip_addr,
1059 'ip_range': self._get_ip_range(self.ip_addr),
1070 'ip_range': self._get_ip_range(self.ip_addr),
1060 }
1071 }
1061
1072
1062 def __unicode__(self):
1073 def __unicode__(self):
1063 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1074 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1064 self.user_id, self.ip_addr)
1075 self.user_id, self.ip_addr)
1065
1076
1066
1077
1067 class UserLog(Base, BaseModel):
1078 class UserLog(Base, BaseModel):
1068 __tablename__ = 'user_logs'
1079 __tablename__ = 'user_logs'
1069 __table_args__ = (
1080 __table_args__ = (
1070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1081 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1071 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1082 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1072 )
1083 )
1073 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1084 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1074 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1085 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1075 username = Column("username", String(255), nullable=True, unique=None, default=None)
1086 username = Column("username", String(255), nullable=True, unique=None, default=None)
1076 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1087 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1077 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1088 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1078 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1089 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1079 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1090 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1080 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1091 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1081
1092
1082 def __unicode__(self):
1093 def __unicode__(self):
1083 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1094 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1084 self.repository_name,
1095 self.repository_name,
1085 self.action)
1096 self.action)
1086
1097
1087 @property
1098 @property
1088 def action_as_day(self):
1099 def action_as_day(self):
1089 return datetime.date(*self.action_date.timetuple()[:3])
1100 return datetime.date(*self.action_date.timetuple()[:3])
1090
1101
1091 user = relationship('User')
1102 user = relationship('User')
1092 repository = relationship('Repository', cascade='')
1103 repository = relationship('Repository', cascade='')
1093
1104
1094
1105
1095 class UserGroup(Base, BaseModel):
1106 class UserGroup(Base, BaseModel):
1096 __tablename__ = 'users_groups'
1107 __tablename__ = 'users_groups'
1097 __table_args__ = (
1108 __table_args__ = (
1098 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1109 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1099 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1110 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1100 )
1111 )
1101
1112
1102 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1103 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1114 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1104 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1115 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1105 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1116 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1106 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1117 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1107 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1118 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1108 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1119 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1109 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1120 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1110
1121
1111 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1122 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1112 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1123 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1113 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1124 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1114 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1125 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1115 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1126 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1116 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1127 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1117
1128
1118 user = relationship('User')
1129 user = relationship('User')
1119
1130
1120 @hybrid_property
1131 @hybrid_property
1121 def group_data(self):
1132 def group_data(self):
1122 if not self._group_data:
1133 if not self._group_data:
1123 return {}
1134 return {}
1124
1135
1125 try:
1136 try:
1126 return json.loads(self._group_data)
1137 return json.loads(self._group_data)
1127 except TypeError:
1138 except TypeError:
1128 return {}
1139 return {}
1129
1140
1130 @group_data.setter
1141 @group_data.setter
1131 def group_data(self, val):
1142 def group_data(self, val):
1132 try:
1143 try:
1133 self._group_data = json.dumps(val)
1144 self._group_data = json.dumps(val)
1134 except Exception:
1145 except Exception:
1135 log.error(traceback.format_exc())
1146 log.error(traceback.format_exc())
1136
1147
1137 def __unicode__(self):
1148 def __unicode__(self):
1138 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1149 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1139 self.users_group_id,
1150 self.users_group_id,
1140 self.users_group_name)
1151 self.users_group_name)
1141
1152
1142 @classmethod
1153 @classmethod
1143 def get_by_group_name(cls, group_name, cache=False,
1154 def get_by_group_name(cls, group_name, cache=False,
1144 case_insensitive=False):
1155 case_insensitive=False):
1145 if case_insensitive:
1156 if case_insensitive:
1146 q = cls.query().filter(func.lower(cls.users_group_name) ==
1157 q = cls.query().filter(func.lower(cls.users_group_name) ==
1147 func.lower(group_name))
1158 func.lower(group_name))
1148
1159
1149 else:
1160 else:
1150 q = cls.query().filter(cls.users_group_name == group_name)
1161 q = cls.query().filter(cls.users_group_name == group_name)
1151 if cache:
1162 if cache:
1152 q = q.options(FromCache(
1163 q = q.options(FromCache(
1153 "sql_cache_short",
1164 "sql_cache_short",
1154 "get_group_%s" % _hash_key(group_name)))
1165 "get_group_%s" % _hash_key(group_name)))
1155 return q.scalar()
1166 return q.scalar()
1156
1167
1157 @classmethod
1168 @classmethod
1158 def get(cls, user_group_id, cache=False):
1169 def get(cls, user_group_id, cache=False):
1159 user_group = cls.query()
1170 user_group = cls.query()
1160 if cache:
1171 if cache:
1161 user_group = user_group.options(FromCache("sql_cache_short",
1172 user_group = user_group.options(FromCache("sql_cache_short",
1162 "get_users_group_%s" % user_group_id))
1173 "get_users_group_%s" % user_group_id))
1163 return user_group.get(user_group_id)
1174 return user_group.get(user_group_id)
1164
1175
1165 def permissions(self, with_admins=True, with_owner=True):
1176 def permissions(self, with_admins=True, with_owner=True):
1166 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1177 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1167 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1178 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1168 joinedload(UserUserGroupToPerm.user),
1179 joinedload(UserUserGroupToPerm.user),
1169 joinedload(UserUserGroupToPerm.permission),)
1180 joinedload(UserUserGroupToPerm.permission),)
1170
1181
1171 # get owners and admins and permissions. We do a trick of re-writing
1182 # get owners and admins and permissions. We do a trick of re-writing
1172 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1183 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1173 # has a global reference and changing one object propagates to all
1184 # has a global reference and changing one object propagates to all
1174 # others. This means if admin is also an owner admin_row that change
1185 # others. This means if admin is also an owner admin_row that change
1175 # would propagate to both objects
1186 # would propagate to both objects
1176 perm_rows = []
1187 perm_rows = []
1177 for _usr in q.all():
1188 for _usr in q.all():
1178 usr = AttributeDict(_usr.user.get_dict())
1189 usr = AttributeDict(_usr.user.get_dict())
1179 usr.permission = _usr.permission.permission_name
1190 usr.permission = _usr.permission.permission_name
1180 perm_rows.append(usr)
1191 perm_rows.append(usr)
1181
1192
1182 # filter the perm rows by 'default' first and then sort them by
1193 # filter the perm rows by 'default' first and then sort them by
1183 # admin,write,read,none permissions sorted again alphabetically in
1194 # admin,write,read,none permissions sorted again alphabetically in
1184 # each group
1195 # each group
1185 perm_rows = sorted(perm_rows, key=display_sort)
1196 perm_rows = sorted(perm_rows, key=display_sort)
1186
1197
1187 _admin_perm = 'usergroup.admin'
1198 _admin_perm = 'usergroup.admin'
1188 owner_row = []
1199 owner_row = []
1189 if with_owner:
1200 if with_owner:
1190 usr = AttributeDict(self.user.get_dict())
1201 usr = AttributeDict(self.user.get_dict())
1191 usr.owner_row = True
1202 usr.owner_row = True
1192 usr.permission = _admin_perm
1203 usr.permission = _admin_perm
1193 owner_row.append(usr)
1204 owner_row.append(usr)
1194
1205
1195 super_admin_rows = []
1206 super_admin_rows = []
1196 if with_admins:
1207 if with_admins:
1197 for usr in User.get_all_super_admins():
1208 for usr in User.get_all_super_admins():
1198 # if this admin is also owner, don't double the record
1209 # if this admin is also owner, don't double the record
1199 if usr.user_id == owner_row[0].user_id:
1210 if usr.user_id == owner_row[0].user_id:
1200 owner_row[0].admin_row = True
1211 owner_row[0].admin_row = True
1201 else:
1212 else:
1202 usr = AttributeDict(usr.get_dict())
1213 usr = AttributeDict(usr.get_dict())
1203 usr.admin_row = True
1214 usr.admin_row = True
1204 usr.permission = _admin_perm
1215 usr.permission = _admin_perm
1205 super_admin_rows.append(usr)
1216 super_admin_rows.append(usr)
1206
1217
1207 return super_admin_rows + owner_row + perm_rows
1218 return super_admin_rows + owner_row + perm_rows
1208
1219
1209 def permission_user_groups(self):
1220 def permission_user_groups(self):
1210 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1221 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1211 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1222 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1212 joinedload(UserGroupUserGroupToPerm.target_user_group),
1223 joinedload(UserGroupUserGroupToPerm.target_user_group),
1213 joinedload(UserGroupUserGroupToPerm.permission),)
1224 joinedload(UserGroupUserGroupToPerm.permission),)
1214
1225
1215 perm_rows = []
1226 perm_rows = []
1216 for _user_group in q.all():
1227 for _user_group in q.all():
1217 usr = AttributeDict(_user_group.user_group.get_dict())
1228 usr = AttributeDict(_user_group.user_group.get_dict())
1218 usr.permission = _user_group.permission.permission_name
1229 usr.permission = _user_group.permission.permission_name
1219 perm_rows.append(usr)
1230 perm_rows.append(usr)
1220
1231
1221 return perm_rows
1232 return perm_rows
1222
1233
1223 def _get_default_perms(self, user_group, suffix=''):
1234 def _get_default_perms(self, user_group, suffix=''):
1224 from rhodecode.model.permission import PermissionModel
1235 from rhodecode.model.permission import PermissionModel
1225 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1236 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1226
1237
1227 def get_default_perms(self, suffix=''):
1238 def get_default_perms(self, suffix=''):
1228 return self._get_default_perms(self, suffix)
1239 return self._get_default_perms(self, suffix)
1229
1240
1230 def get_api_data(self, with_group_members=True, include_secrets=False):
1241 def get_api_data(self, with_group_members=True, include_secrets=False):
1231 """
1242 """
1232 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1243 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1233 basically forwarded.
1244 basically forwarded.
1234
1245
1235 """
1246 """
1236 user_group = self
1247 user_group = self
1237
1248
1238 data = {
1249 data = {
1239 'users_group_id': user_group.users_group_id,
1250 'users_group_id': user_group.users_group_id,
1240 'group_name': user_group.users_group_name,
1251 'group_name': user_group.users_group_name,
1241 'group_description': user_group.user_group_description,
1252 'group_description': user_group.user_group_description,
1242 'active': user_group.users_group_active,
1253 'active': user_group.users_group_active,
1243 'owner': user_group.user.username,
1254 'owner': user_group.user.username,
1244 }
1255 }
1245 if with_group_members:
1256 if with_group_members:
1246 users = []
1257 users = []
1247 for user in user_group.members:
1258 for user in user_group.members:
1248 user = user.user
1259 user = user.user
1249 users.append(user.get_api_data(include_secrets=include_secrets))
1260 users.append(user.get_api_data(include_secrets=include_secrets))
1250 data['users'] = users
1261 data['users'] = users
1251
1262
1252 return data
1263 return data
1253
1264
1254
1265
1255 class UserGroupMember(Base, BaseModel):
1266 class UserGroupMember(Base, BaseModel):
1256 __tablename__ = 'users_groups_members'
1267 __tablename__ = 'users_groups_members'
1257 __table_args__ = (
1268 __table_args__ = (
1258 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1259 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1260 )
1271 )
1261
1272
1262 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1273 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1263 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1274 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1264 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1275 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1265
1276
1266 user = relationship('User', lazy='joined')
1277 user = relationship('User', lazy='joined')
1267 users_group = relationship('UserGroup')
1278 users_group = relationship('UserGroup')
1268
1279
1269 def __init__(self, gr_id='', u_id=''):
1280 def __init__(self, gr_id='', u_id=''):
1270 self.users_group_id = gr_id
1281 self.users_group_id = gr_id
1271 self.user_id = u_id
1282 self.user_id = u_id
1272
1283
1273
1284
1274 class RepositoryField(Base, BaseModel):
1285 class RepositoryField(Base, BaseModel):
1275 __tablename__ = 'repositories_fields'
1286 __tablename__ = 'repositories_fields'
1276 __table_args__ = (
1287 __table_args__ = (
1277 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1288 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1278 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1279 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1280 )
1291 )
1281 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1292 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1282
1293
1283 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1294 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1284 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1295 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1285 field_key = Column("field_key", String(250))
1296 field_key = Column("field_key", String(250))
1286 field_label = Column("field_label", String(1024), nullable=False)
1297 field_label = Column("field_label", String(1024), nullable=False)
1287 field_value = Column("field_value", String(10000), nullable=False)
1298 field_value = Column("field_value", String(10000), nullable=False)
1288 field_desc = Column("field_desc", String(1024), nullable=False)
1299 field_desc = Column("field_desc", String(1024), nullable=False)
1289 field_type = Column("field_type", String(255), nullable=False, unique=None)
1300 field_type = Column("field_type", String(255), nullable=False, unique=None)
1290 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1301 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1291
1302
1292 repository = relationship('Repository')
1303 repository = relationship('Repository')
1293
1304
1294 @property
1305 @property
1295 def field_key_prefixed(self):
1306 def field_key_prefixed(self):
1296 return 'ex_%s' % self.field_key
1307 return 'ex_%s' % self.field_key
1297
1308
1298 @classmethod
1309 @classmethod
1299 def un_prefix_key(cls, key):
1310 def un_prefix_key(cls, key):
1300 if key.startswith(cls.PREFIX):
1311 if key.startswith(cls.PREFIX):
1301 return key[len(cls.PREFIX):]
1312 return key[len(cls.PREFIX):]
1302 return key
1313 return key
1303
1314
1304 @classmethod
1315 @classmethod
1305 def get_by_key_name(cls, key, repo):
1316 def get_by_key_name(cls, key, repo):
1306 row = cls.query()\
1317 row = cls.query()\
1307 .filter(cls.repository == repo)\
1318 .filter(cls.repository == repo)\
1308 .filter(cls.field_key == key).scalar()
1319 .filter(cls.field_key == key).scalar()
1309 return row
1320 return row
1310
1321
1311
1322
1312 class Repository(Base, BaseModel):
1323 class Repository(Base, BaseModel):
1313 __tablename__ = 'repositories'
1324 __tablename__ = 'repositories'
1314 __table_args__ = (
1325 __table_args__ = (
1315 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1326 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1316 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1327 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1317 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1328 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1318 )
1329 )
1319 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1330 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1320 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1331 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1321
1332
1322 STATE_CREATED = 'repo_state_created'
1333 STATE_CREATED = 'repo_state_created'
1323 STATE_PENDING = 'repo_state_pending'
1334 STATE_PENDING = 'repo_state_pending'
1324 STATE_ERROR = 'repo_state_error'
1335 STATE_ERROR = 'repo_state_error'
1325
1336
1326 LOCK_AUTOMATIC = 'lock_auto'
1337 LOCK_AUTOMATIC = 'lock_auto'
1327 LOCK_API = 'lock_api'
1338 LOCK_API = 'lock_api'
1328 LOCK_WEB = 'lock_web'
1339 LOCK_WEB = 'lock_web'
1329 LOCK_PULL = 'lock_pull'
1340 LOCK_PULL = 'lock_pull'
1330
1341
1331 NAME_SEP = URL_SEP
1342 NAME_SEP = URL_SEP
1332
1343
1333 repo_id = Column(
1344 repo_id = Column(
1334 "repo_id", Integer(), nullable=False, unique=True, default=None,
1345 "repo_id", Integer(), nullable=False, unique=True, default=None,
1335 primary_key=True)
1346 primary_key=True)
1336 _repo_name = Column(
1347 _repo_name = Column(
1337 "repo_name", Text(), nullable=False, default=None)
1348 "repo_name", Text(), nullable=False, default=None)
1338 _repo_name_hash = Column(
1349 _repo_name_hash = Column(
1339 "repo_name_hash", String(255), nullable=False, unique=True)
1350 "repo_name_hash", String(255), nullable=False, unique=True)
1340 repo_state = Column("repo_state", String(255), nullable=True)
1351 repo_state = Column("repo_state", String(255), nullable=True)
1341
1352
1342 clone_uri = Column(
1353 clone_uri = Column(
1343 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1354 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1344 default=None)
1355 default=None)
1345 repo_type = Column(
1356 repo_type = Column(
1346 "repo_type", String(255), nullable=False, unique=False, default=None)
1357 "repo_type", String(255), nullable=False, unique=False, default=None)
1347 user_id = Column(
1358 user_id = Column(
1348 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1359 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1349 unique=False, default=None)
1360 unique=False, default=None)
1350 private = Column(
1361 private = Column(
1351 "private", Boolean(), nullable=True, unique=None, default=None)
1362 "private", Boolean(), nullable=True, unique=None, default=None)
1352 enable_statistics = Column(
1363 enable_statistics = Column(
1353 "statistics", Boolean(), nullable=True, unique=None, default=True)
1364 "statistics", Boolean(), nullable=True, unique=None, default=True)
1354 enable_downloads = Column(
1365 enable_downloads = Column(
1355 "downloads", Boolean(), nullable=True, unique=None, default=True)
1366 "downloads", Boolean(), nullable=True, unique=None, default=True)
1356 description = Column(
1367 description = Column(
1357 "description", String(10000), nullable=True, unique=None, default=None)
1368 "description", String(10000), nullable=True, unique=None, default=None)
1358 created_on = Column(
1369 created_on = Column(
1359 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1370 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1360 default=datetime.datetime.now)
1371 default=datetime.datetime.now)
1361 updated_on = Column(
1372 updated_on = Column(
1362 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1373 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1363 default=datetime.datetime.now)
1374 default=datetime.datetime.now)
1364 _landing_revision = Column(
1375 _landing_revision = Column(
1365 "landing_revision", String(255), nullable=False, unique=False,
1376 "landing_revision", String(255), nullable=False, unique=False,
1366 default=None)
1377 default=None)
1367 enable_locking = Column(
1378 enable_locking = Column(
1368 "enable_locking", Boolean(), nullable=False, unique=None,
1379 "enable_locking", Boolean(), nullable=False, unique=None,
1369 default=False)
1380 default=False)
1370 _locked = Column(
1381 _locked = Column(
1371 "locked", String(255), nullable=True, unique=False, default=None)
1382 "locked", String(255), nullable=True, unique=False, default=None)
1372 _changeset_cache = Column(
1383 _changeset_cache = Column(
1373 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1384 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1374
1385
1375 fork_id = Column(
1386 fork_id = Column(
1376 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1387 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1377 nullable=True, unique=False, default=None)
1388 nullable=True, unique=False, default=None)
1378 group_id = Column(
1389 group_id = Column(
1379 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1390 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1380 unique=False, default=None)
1391 unique=False, default=None)
1381
1392
1382 user = relationship('User', lazy='joined')
1393 user = relationship('User', lazy='joined')
1383 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1394 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1384 group = relationship('RepoGroup', lazy='joined')
1395 group = relationship('RepoGroup', lazy='joined')
1385 repo_to_perm = relationship(
1396 repo_to_perm = relationship(
1386 'UserRepoToPerm', cascade='all',
1397 'UserRepoToPerm', cascade='all',
1387 order_by='UserRepoToPerm.repo_to_perm_id')
1398 order_by='UserRepoToPerm.repo_to_perm_id')
1388 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1399 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1389 stats = relationship('Statistics', cascade='all', uselist=False)
1400 stats = relationship('Statistics', cascade='all', uselist=False)
1390
1401
1391 followers = relationship(
1402 followers = relationship(
1392 'UserFollowing',
1403 'UserFollowing',
1393 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1404 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1394 cascade='all')
1405 cascade='all')
1395 extra_fields = relationship(
1406 extra_fields = relationship(
1396 'RepositoryField', cascade="all, delete, delete-orphan")
1407 'RepositoryField', cascade="all, delete, delete-orphan")
1397 logs = relationship('UserLog')
1408 logs = relationship('UserLog')
1398 comments = relationship(
1409 comments = relationship(
1399 'ChangesetComment', cascade="all, delete, delete-orphan")
1410 'ChangesetComment', cascade="all, delete, delete-orphan")
1400 pull_requests_source = relationship(
1411 pull_requests_source = relationship(
1401 'PullRequest',
1412 'PullRequest',
1402 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1413 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1403 cascade="all, delete, delete-orphan")
1414 cascade="all, delete, delete-orphan")
1404 pull_requests_target = relationship(
1415 pull_requests_target = relationship(
1405 'PullRequest',
1416 'PullRequest',
1406 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1417 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1407 cascade="all, delete, delete-orphan")
1418 cascade="all, delete, delete-orphan")
1408 ui = relationship('RepoRhodeCodeUi', cascade="all")
1419 ui = relationship('RepoRhodeCodeUi', cascade="all")
1409 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1420 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1410 integrations = relationship('Integration',
1421 integrations = relationship('Integration',
1411 cascade="all, delete, delete-orphan")
1422 cascade="all, delete, delete-orphan")
1412
1423
1413 def __unicode__(self):
1424 def __unicode__(self):
1414 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1425 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1415 safe_unicode(self.repo_name))
1426 safe_unicode(self.repo_name))
1416
1427
1417 @hybrid_property
1428 @hybrid_property
1418 def landing_rev(self):
1429 def landing_rev(self):
1419 # always should return [rev_type, rev]
1430 # always should return [rev_type, rev]
1420 if self._landing_revision:
1431 if self._landing_revision:
1421 _rev_info = self._landing_revision.split(':')
1432 _rev_info = self._landing_revision.split(':')
1422 if len(_rev_info) < 2:
1433 if len(_rev_info) < 2:
1423 _rev_info.insert(0, 'rev')
1434 _rev_info.insert(0, 'rev')
1424 return [_rev_info[0], _rev_info[1]]
1435 return [_rev_info[0], _rev_info[1]]
1425 return [None, None]
1436 return [None, None]
1426
1437
1427 @landing_rev.setter
1438 @landing_rev.setter
1428 def landing_rev(self, val):
1439 def landing_rev(self, val):
1429 if ':' not in val:
1440 if ':' not in val:
1430 raise ValueError('value must be delimited with `:` and consist '
1441 raise ValueError('value must be delimited with `:` and consist '
1431 'of <rev_type>:<rev>, got %s instead' % val)
1442 'of <rev_type>:<rev>, got %s instead' % val)
1432 self._landing_revision = val
1443 self._landing_revision = val
1433
1444
1434 @hybrid_property
1445 @hybrid_property
1435 def locked(self):
1446 def locked(self):
1436 if self._locked:
1447 if self._locked:
1437 user_id, timelocked, reason = self._locked.split(':')
1448 user_id, timelocked, reason = self._locked.split(':')
1438 lock_values = int(user_id), timelocked, reason
1449 lock_values = int(user_id), timelocked, reason
1439 else:
1450 else:
1440 lock_values = [None, None, None]
1451 lock_values = [None, None, None]
1441 return lock_values
1452 return lock_values
1442
1453
1443 @locked.setter
1454 @locked.setter
1444 def locked(self, val):
1455 def locked(self, val):
1445 if val and isinstance(val, (list, tuple)):
1456 if val and isinstance(val, (list, tuple)):
1446 self._locked = ':'.join(map(str, val))
1457 self._locked = ':'.join(map(str, val))
1447 else:
1458 else:
1448 self._locked = None
1459 self._locked = None
1449
1460
1450 @hybrid_property
1461 @hybrid_property
1451 def changeset_cache(self):
1462 def changeset_cache(self):
1452 from rhodecode.lib.vcs.backends.base import EmptyCommit
1463 from rhodecode.lib.vcs.backends.base import EmptyCommit
1453 dummy = EmptyCommit().__json__()
1464 dummy = EmptyCommit().__json__()
1454 if not self._changeset_cache:
1465 if not self._changeset_cache:
1455 return dummy
1466 return dummy
1456 try:
1467 try:
1457 return json.loads(self._changeset_cache)
1468 return json.loads(self._changeset_cache)
1458 except TypeError:
1469 except TypeError:
1459 return dummy
1470 return dummy
1460 except Exception:
1471 except Exception:
1461 log.error(traceback.format_exc())
1472 log.error(traceback.format_exc())
1462 return dummy
1473 return dummy
1463
1474
1464 @changeset_cache.setter
1475 @changeset_cache.setter
1465 def changeset_cache(self, val):
1476 def changeset_cache(self, val):
1466 try:
1477 try:
1467 self._changeset_cache = json.dumps(val)
1478 self._changeset_cache = json.dumps(val)
1468 except Exception:
1479 except Exception:
1469 log.error(traceback.format_exc())
1480 log.error(traceback.format_exc())
1470
1481
1471 @hybrid_property
1482 @hybrid_property
1472 def repo_name(self):
1483 def repo_name(self):
1473 return self._repo_name
1484 return self._repo_name
1474
1485
1475 @repo_name.setter
1486 @repo_name.setter
1476 def repo_name(self, value):
1487 def repo_name(self, value):
1477 self._repo_name = value
1488 self._repo_name = value
1478 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1489 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1479
1490
1480 @classmethod
1491 @classmethod
1481 def normalize_repo_name(cls, repo_name):
1492 def normalize_repo_name(cls, repo_name):
1482 """
1493 """
1483 Normalizes os specific repo_name to the format internally stored inside
1494 Normalizes os specific repo_name to the format internally stored inside
1484 database using URL_SEP
1495 database using URL_SEP
1485
1496
1486 :param cls:
1497 :param cls:
1487 :param repo_name:
1498 :param repo_name:
1488 """
1499 """
1489 return cls.NAME_SEP.join(repo_name.split(os.sep))
1500 return cls.NAME_SEP.join(repo_name.split(os.sep))
1490
1501
1491 @classmethod
1502 @classmethod
1492 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1503 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1493 session = Session()
1504 session = Session()
1494 q = session.query(cls).filter(cls.repo_name == repo_name)
1505 q = session.query(cls).filter(cls.repo_name == repo_name)
1495
1506
1496 if cache:
1507 if cache:
1497 if identity_cache:
1508 if identity_cache:
1498 val = cls.identity_cache(session, 'repo_name', repo_name)
1509 val = cls.identity_cache(session, 'repo_name', repo_name)
1499 if val:
1510 if val:
1500 return val
1511 return val
1501 else:
1512 else:
1502 q = q.options(
1513 q = q.options(
1503 FromCache("sql_cache_short",
1514 FromCache("sql_cache_short",
1504 "get_repo_by_name_%s" % _hash_key(repo_name)))
1515 "get_repo_by_name_%s" % _hash_key(repo_name)))
1505
1516
1506 return q.scalar()
1517 return q.scalar()
1507
1518
1508 @classmethod
1519 @classmethod
1509 def get_by_full_path(cls, repo_full_path):
1520 def get_by_full_path(cls, repo_full_path):
1510 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1521 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1511 repo_name = cls.normalize_repo_name(repo_name)
1522 repo_name = cls.normalize_repo_name(repo_name)
1512 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1523 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1513
1524
1514 @classmethod
1525 @classmethod
1515 def get_repo_forks(cls, repo_id):
1526 def get_repo_forks(cls, repo_id):
1516 return cls.query().filter(Repository.fork_id == repo_id)
1527 return cls.query().filter(Repository.fork_id == repo_id)
1517
1528
1518 @classmethod
1529 @classmethod
1519 def base_path(cls):
1530 def base_path(cls):
1520 """
1531 """
1521 Returns base path when all repos are stored
1532 Returns base path when all repos are stored
1522
1533
1523 :param cls:
1534 :param cls:
1524 """
1535 """
1525 q = Session().query(RhodeCodeUi)\
1536 q = Session().query(RhodeCodeUi)\
1526 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1537 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1538 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1528 return q.one().ui_value
1539 return q.one().ui_value
1529
1540
1530 @classmethod
1541 @classmethod
1531 def is_valid(cls, repo_name):
1542 def is_valid(cls, repo_name):
1532 """
1543 """
1533 returns True if given repo name is a valid filesystem repository
1544 returns True if given repo name is a valid filesystem repository
1534
1545
1535 :param cls:
1546 :param cls:
1536 :param repo_name:
1547 :param repo_name:
1537 """
1548 """
1538 from rhodecode.lib.utils import is_valid_repo
1549 from rhodecode.lib.utils import is_valid_repo
1539
1550
1540 return is_valid_repo(repo_name, cls.base_path())
1551 return is_valid_repo(repo_name, cls.base_path())
1541
1552
1542 @classmethod
1553 @classmethod
1543 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1554 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1544 case_insensitive=True):
1555 case_insensitive=True):
1545 q = Repository.query()
1556 q = Repository.query()
1546
1557
1547 if not isinstance(user_id, Optional):
1558 if not isinstance(user_id, Optional):
1548 q = q.filter(Repository.user_id == user_id)
1559 q = q.filter(Repository.user_id == user_id)
1549
1560
1550 if not isinstance(group_id, Optional):
1561 if not isinstance(group_id, Optional):
1551 q = q.filter(Repository.group_id == group_id)
1562 q = q.filter(Repository.group_id == group_id)
1552
1563
1553 if case_insensitive:
1564 if case_insensitive:
1554 q = q.order_by(func.lower(Repository.repo_name))
1565 q = q.order_by(func.lower(Repository.repo_name))
1555 else:
1566 else:
1556 q = q.order_by(Repository.repo_name)
1567 q = q.order_by(Repository.repo_name)
1557 return q.all()
1568 return q.all()
1558
1569
1559 @property
1570 @property
1560 def forks(self):
1571 def forks(self):
1561 """
1572 """
1562 Return forks of this repo
1573 Return forks of this repo
1563 """
1574 """
1564 return Repository.get_repo_forks(self.repo_id)
1575 return Repository.get_repo_forks(self.repo_id)
1565
1576
1566 @property
1577 @property
1567 def parent(self):
1578 def parent(self):
1568 """
1579 """
1569 Returns fork parent
1580 Returns fork parent
1570 """
1581 """
1571 return self.fork
1582 return self.fork
1572
1583
1573 @property
1584 @property
1574 def just_name(self):
1585 def just_name(self):
1575 return self.repo_name.split(self.NAME_SEP)[-1]
1586 return self.repo_name.split(self.NAME_SEP)[-1]
1576
1587
1577 @property
1588 @property
1578 def groups_with_parents(self):
1589 def groups_with_parents(self):
1579 groups = []
1590 groups = []
1580 if self.group is None:
1591 if self.group is None:
1581 return groups
1592 return groups
1582
1593
1583 cur_gr = self.group
1594 cur_gr = self.group
1584 groups.insert(0, cur_gr)
1595 groups.insert(0, cur_gr)
1585 while 1:
1596 while 1:
1586 gr = getattr(cur_gr, 'parent_group', None)
1597 gr = getattr(cur_gr, 'parent_group', None)
1587 cur_gr = cur_gr.parent_group
1598 cur_gr = cur_gr.parent_group
1588 if gr is None:
1599 if gr is None:
1589 break
1600 break
1590 groups.insert(0, gr)
1601 groups.insert(0, gr)
1591
1602
1592 return groups
1603 return groups
1593
1604
1594 @property
1605 @property
1595 def groups_and_repo(self):
1606 def groups_and_repo(self):
1596 return self.groups_with_parents, self
1607 return self.groups_with_parents, self
1597
1608
1598 @LazyProperty
1609 @LazyProperty
1599 def repo_path(self):
1610 def repo_path(self):
1600 """
1611 """
1601 Returns base full path for that repository means where it actually
1612 Returns base full path for that repository means where it actually
1602 exists on a filesystem
1613 exists on a filesystem
1603 """
1614 """
1604 q = Session().query(RhodeCodeUi).filter(
1615 q = Session().query(RhodeCodeUi).filter(
1605 RhodeCodeUi.ui_key == self.NAME_SEP)
1616 RhodeCodeUi.ui_key == self.NAME_SEP)
1606 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1617 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1607 return q.one().ui_value
1618 return q.one().ui_value
1608
1619
1609 @property
1620 @property
1610 def repo_full_path(self):
1621 def repo_full_path(self):
1611 p = [self.repo_path]
1622 p = [self.repo_path]
1612 # we need to split the name by / since this is how we store the
1623 # we need to split the name by / since this is how we store the
1613 # names in the database, but that eventually needs to be converted
1624 # names in the database, but that eventually needs to be converted
1614 # into a valid system path
1625 # into a valid system path
1615 p += self.repo_name.split(self.NAME_SEP)
1626 p += self.repo_name.split(self.NAME_SEP)
1616 return os.path.join(*map(safe_unicode, p))
1627 return os.path.join(*map(safe_unicode, p))
1617
1628
1618 @property
1629 @property
1619 def cache_keys(self):
1630 def cache_keys(self):
1620 """
1631 """
1621 Returns associated cache keys for that repo
1632 Returns associated cache keys for that repo
1622 """
1633 """
1623 return CacheKey.query()\
1634 return CacheKey.query()\
1624 .filter(CacheKey.cache_args == self.repo_name)\
1635 .filter(CacheKey.cache_args == self.repo_name)\
1625 .order_by(CacheKey.cache_key)\
1636 .order_by(CacheKey.cache_key)\
1626 .all()
1637 .all()
1627
1638
1628 def get_new_name(self, repo_name):
1639 def get_new_name(self, repo_name):
1629 """
1640 """
1630 returns new full repository name based on assigned group and new new
1641 returns new full repository name based on assigned group and new new
1631
1642
1632 :param group_name:
1643 :param group_name:
1633 """
1644 """
1634 path_prefix = self.group.full_path_splitted if self.group else []
1645 path_prefix = self.group.full_path_splitted if self.group else []
1635 return self.NAME_SEP.join(path_prefix + [repo_name])
1646 return self.NAME_SEP.join(path_prefix + [repo_name])
1636
1647
1637 @property
1648 @property
1638 def _config(self):
1649 def _config(self):
1639 """
1650 """
1640 Returns db based config object.
1651 Returns db based config object.
1641 """
1652 """
1642 from rhodecode.lib.utils import make_db_config
1653 from rhodecode.lib.utils import make_db_config
1643 return make_db_config(clear_session=False, repo=self)
1654 return make_db_config(clear_session=False, repo=self)
1644
1655
1645 def permissions(self, with_admins=True, with_owner=True):
1656 def permissions(self, with_admins=True, with_owner=True):
1646 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1657 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1647 q = q.options(joinedload(UserRepoToPerm.repository),
1658 q = q.options(joinedload(UserRepoToPerm.repository),
1648 joinedload(UserRepoToPerm.user),
1659 joinedload(UserRepoToPerm.user),
1649 joinedload(UserRepoToPerm.permission),)
1660 joinedload(UserRepoToPerm.permission),)
1650
1661
1651 # get owners and admins and permissions. We do a trick of re-writing
1662 # get owners and admins and permissions. We do a trick of re-writing
1652 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1663 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1653 # has a global reference and changing one object propagates to all
1664 # has a global reference and changing one object propagates to all
1654 # others. This means if admin is also an owner admin_row that change
1665 # others. This means if admin is also an owner admin_row that change
1655 # would propagate to both objects
1666 # would propagate to both objects
1656 perm_rows = []
1667 perm_rows = []
1657 for _usr in q.all():
1668 for _usr in q.all():
1658 usr = AttributeDict(_usr.user.get_dict())
1669 usr = AttributeDict(_usr.user.get_dict())
1659 usr.permission = _usr.permission.permission_name
1670 usr.permission = _usr.permission.permission_name
1660 perm_rows.append(usr)
1671 perm_rows.append(usr)
1661
1672
1662 # filter the perm rows by 'default' first and then sort them by
1673 # filter the perm rows by 'default' first and then sort them by
1663 # admin,write,read,none permissions sorted again alphabetically in
1674 # admin,write,read,none permissions sorted again alphabetically in
1664 # each group
1675 # each group
1665 perm_rows = sorted(perm_rows, key=display_sort)
1676 perm_rows = sorted(perm_rows, key=display_sort)
1666
1677
1667 _admin_perm = 'repository.admin'
1678 _admin_perm = 'repository.admin'
1668 owner_row = []
1679 owner_row = []
1669 if with_owner:
1680 if with_owner:
1670 usr = AttributeDict(self.user.get_dict())
1681 usr = AttributeDict(self.user.get_dict())
1671 usr.owner_row = True
1682 usr.owner_row = True
1672 usr.permission = _admin_perm
1683 usr.permission = _admin_perm
1673 owner_row.append(usr)
1684 owner_row.append(usr)
1674
1685
1675 super_admin_rows = []
1686 super_admin_rows = []
1676 if with_admins:
1687 if with_admins:
1677 for usr in User.get_all_super_admins():
1688 for usr in User.get_all_super_admins():
1678 # if this admin is also owner, don't double the record
1689 # if this admin is also owner, don't double the record
1679 if usr.user_id == owner_row[0].user_id:
1690 if usr.user_id == owner_row[0].user_id:
1680 owner_row[0].admin_row = True
1691 owner_row[0].admin_row = True
1681 else:
1692 else:
1682 usr = AttributeDict(usr.get_dict())
1693 usr = AttributeDict(usr.get_dict())
1683 usr.admin_row = True
1694 usr.admin_row = True
1684 usr.permission = _admin_perm
1695 usr.permission = _admin_perm
1685 super_admin_rows.append(usr)
1696 super_admin_rows.append(usr)
1686
1697
1687 return super_admin_rows + owner_row + perm_rows
1698 return super_admin_rows + owner_row + perm_rows
1688
1699
1689 def permission_user_groups(self):
1700 def permission_user_groups(self):
1690 q = UserGroupRepoToPerm.query().filter(
1701 q = UserGroupRepoToPerm.query().filter(
1691 UserGroupRepoToPerm.repository == self)
1702 UserGroupRepoToPerm.repository == self)
1692 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1703 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1693 joinedload(UserGroupRepoToPerm.users_group),
1704 joinedload(UserGroupRepoToPerm.users_group),
1694 joinedload(UserGroupRepoToPerm.permission),)
1705 joinedload(UserGroupRepoToPerm.permission),)
1695
1706
1696 perm_rows = []
1707 perm_rows = []
1697 for _user_group in q.all():
1708 for _user_group in q.all():
1698 usr = AttributeDict(_user_group.users_group.get_dict())
1709 usr = AttributeDict(_user_group.users_group.get_dict())
1699 usr.permission = _user_group.permission.permission_name
1710 usr.permission = _user_group.permission.permission_name
1700 perm_rows.append(usr)
1711 perm_rows.append(usr)
1701
1712
1702 return perm_rows
1713 return perm_rows
1703
1714
1704 def get_api_data(self, include_secrets=False):
1715 def get_api_data(self, include_secrets=False):
1705 """
1716 """
1706 Common function for generating repo api data
1717 Common function for generating repo api data
1707
1718
1708 :param include_secrets: See :meth:`User.get_api_data`.
1719 :param include_secrets: See :meth:`User.get_api_data`.
1709
1720
1710 """
1721 """
1711 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1722 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1712 # move this methods on models level.
1723 # move this methods on models level.
1713 from rhodecode.model.settings import SettingsModel
1724 from rhodecode.model.settings import SettingsModel
1714
1725
1715 repo = self
1726 repo = self
1716 _user_id, _time, _reason = self.locked
1727 _user_id, _time, _reason = self.locked
1717
1728
1718 data = {
1729 data = {
1719 'repo_id': repo.repo_id,
1730 'repo_id': repo.repo_id,
1720 'repo_name': repo.repo_name,
1731 'repo_name': repo.repo_name,
1721 'repo_type': repo.repo_type,
1732 'repo_type': repo.repo_type,
1722 'clone_uri': repo.clone_uri or '',
1733 'clone_uri': repo.clone_uri or '',
1723 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1734 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1724 'private': repo.private,
1735 'private': repo.private,
1725 'created_on': repo.created_on,
1736 'created_on': repo.created_on,
1726 'description': repo.description,
1737 'description': repo.description,
1727 'landing_rev': repo.landing_rev,
1738 'landing_rev': repo.landing_rev,
1728 'owner': repo.user.username,
1739 'owner': repo.user.username,
1729 'fork_of': repo.fork.repo_name if repo.fork else None,
1740 'fork_of': repo.fork.repo_name if repo.fork else None,
1730 'enable_statistics': repo.enable_statistics,
1741 'enable_statistics': repo.enable_statistics,
1731 'enable_locking': repo.enable_locking,
1742 'enable_locking': repo.enable_locking,
1732 'enable_downloads': repo.enable_downloads,
1743 'enable_downloads': repo.enable_downloads,
1733 'last_changeset': repo.changeset_cache,
1744 'last_changeset': repo.changeset_cache,
1734 'locked_by': User.get(_user_id).get_api_data(
1745 'locked_by': User.get(_user_id).get_api_data(
1735 include_secrets=include_secrets) if _user_id else None,
1746 include_secrets=include_secrets) if _user_id else None,
1736 'locked_date': time_to_datetime(_time) if _time else None,
1747 'locked_date': time_to_datetime(_time) if _time else None,
1737 'lock_reason': _reason if _reason else None,
1748 'lock_reason': _reason if _reason else None,
1738 }
1749 }
1739
1750
1740 # TODO: mikhail: should be per-repo settings here
1751 # TODO: mikhail: should be per-repo settings here
1741 rc_config = SettingsModel().get_all_settings()
1752 rc_config = SettingsModel().get_all_settings()
1742 repository_fields = str2bool(
1753 repository_fields = str2bool(
1743 rc_config.get('rhodecode_repository_fields'))
1754 rc_config.get('rhodecode_repository_fields'))
1744 if repository_fields:
1755 if repository_fields:
1745 for f in self.extra_fields:
1756 for f in self.extra_fields:
1746 data[f.field_key_prefixed] = f.field_value
1757 data[f.field_key_prefixed] = f.field_value
1747
1758
1748 return data
1759 return data
1749
1760
1750 @classmethod
1761 @classmethod
1751 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1762 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1752 if not lock_time:
1763 if not lock_time:
1753 lock_time = time.time()
1764 lock_time = time.time()
1754 if not lock_reason:
1765 if not lock_reason:
1755 lock_reason = cls.LOCK_AUTOMATIC
1766 lock_reason = cls.LOCK_AUTOMATIC
1756 repo.locked = [user_id, lock_time, lock_reason]
1767 repo.locked = [user_id, lock_time, lock_reason]
1757 Session().add(repo)
1768 Session().add(repo)
1758 Session().commit()
1769 Session().commit()
1759
1770
1760 @classmethod
1771 @classmethod
1761 def unlock(cls, repo):
1772 def unlock(cls, repo):
1762 repo.locked = None
1773 repo.locked = None
1763 Session().add(repo)
1774 Session().add(repo)
1764 Session().commit()
1775 Session().commit()
1765
1776
1766 @classmethod
1777 @classmethod
1767 def getlock(cls, repo):
1778 def getlock(cls, repo):
1768 return repo.locked
1779 return repo.locked
1769
1780
1770 def is_user_lock(self, user_id):
1781 def is_user_lock(self, user_id):
1771 if self.lock[0]:
1782 if self.lock[0]:
1772 lock_user_id = safe_int(self.lock[0])
1783 lock_user_id = safe_int(self.lock[0])
1773 user_id = safe_int(user_id)
1784 user_id = safe_int(user_id)
1774 # both are ints, and they are equal
1785 # both are ints, and they are equal
1775 return all([lock_user_id, user_id]) and lock_user_id == user_id
1786 return all([lock_user_id, user_id]) and lock_user_id == user_id
1776
1787
1777 return False
1788 return False
1778
1789
1779 def get_locking_state(self, action, user_id, only_when_enabled=True):
1790 def get_locking_state(self, action, user_id, only_when_enabled=True):
1780 """
1791 """
1781 Checks locking on this repository, if locking is enabled and lock is
1792 Checks locking on this repository, if locking is enabled and lock is
1782 present returns a tuple of make_lock, locked, locked_by.
1793 present returns a tuple of make_lock, locked, locked_by.
1783 make_lock can have 3 states None (do nothing) True, make lock
1794 make_lock can have 3 states None (do nothing) True, make lock
1784 False release lock, This value is later propagated to hooks, which
1795 False release lock, This value is later propagated to hooks, which
1785 do the locking. Think about this as signals passed to hooks what to do.
1796 do the locking. Think about this as signals passed to hooks what to do.
1786
1797
1787 """
1798 """
1788 # TODO: johbo: This is part of the business logic and should be moved
1799 # TODO: johbo: This is part of the business logic and should be moved
1789 # into the RepositoryModel.
1800 # into the RepositoryModel.
1790
1801
1791 if action not in ('push', 'pull'):
1802 if action not in ('push', 'pull'):
1792 raise ValueError("Invalid action value: %s" % repr(action))
1803 raise ValueError("Invalid action value: %s" % repr(action))
1793
1804
1794 # defines if locked error should be thrown to user
1805 # defines if locked error should be thrown to user
1795 currently_locked = False
1806 currently_locked = False
1796 # defines if new lock should be made, tri-state
1807 # defines if new lock should be made, tri-state
1797 make_lock = None
1808 make_lock = None
1798 repo = self
1809 repo = self
1799 user = User.get(user_id)
1810 user = User.get(user_id)
1800
1811
1801 lock_info = repo.locked
1812 lock_info = repo.locked
1802
1813
1803 if repo and (repo.enable_locking or not only_when_enabled):
1814 if repo and (repo.enable_locking or not only_when_enabled):
1804 if action == 'push':
1815 if action == 'push':
1805 # check if it's already locked !, if it is compare users
1816 # check if it's already locked !, if it is compare users
1806 locked_by_user_id = lock_info[0]
1817 locked_by_user_id = lock_info[0]
1807 if user.user_id == locked_by_user_id:
1818 if user.user_id == locked_by_user_id:
1808 log.debug(
1819 log.debug(
1809 'Got `push` action from user %s, now unlocking', user)
1820 'Got `push` action from user %s, now unlocking', user)
1810 # unlock if we have push from user who locked
1821 # unlock if we have push from user who locked
1811 make_lock = False
1822 make_lock = False
1812 else:
1823 else:
1813 # we're not the same user who locked, ban with
1824 # we're not the same user who locked, ban with
1814 # code defined in settings (default is 423 HTTP Locked) !
1825 # code defined in settings (default is 423 HTTP Locked) !
1815 log.debug('Repo %s is currently locked by %s', repo, user)
1826 log.debug('Repo %s is currently locked by %s', repo, user)
1816 currently_locked = True
1827 currently_locked = True
1817 elif action == 'pull':
1828 elif action == 'pull':
1818 # [0] user [1] date
1829 # [0] user [1] date
1819 if lock_info[0] and lock_info[1]:
1830 if lock_info[0] and lock_info[1]:
1820 log.debug('Repo %s is currently locked by %s', repo, user)
1831 log.debug('Repo %s is currently locked by %s', repo, user)
1821 currently_locked = True
1832 currently_locked = True
1822 else:
1833 else:
1823 log.debug('Setting lock on repo %s by %s', repo, user)
1834 log.debug('Setting lock on repo %s by %s', repo, user)
1824 make_lock = True
1835 make_lock = True
1825
1836
1826 else:
1837 else:
1827 log.debug('Repository %s do not have locking enabled', repo)
1838 log.debug('Repository %s do not have locking enabled', repo)
1828
1839
1829 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1840 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1830 make_lock, currently_locked, lock_info)
1841 make_lock, currently_locked, lock_info)
1831
1842
1832 from rhodecode.lib.auth import HasRepoPermissionAny
1843 from rhodecode.lib.auth import HasRepoPermissionAny
1833 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1844 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1834 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1845 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1835 # if we don't have at least write permission we cannot make a lock
1846 # if we don't have at least write permission we cannot make a lock
1836 log.debug('lock state reset back to FALSE due to lack '
1847 log.debug('lock state reset back to FALSE due to lack '
1837 'of at least read permission')
1848 'of at least read permission')
1838 make_lock = False
1849 make_lock = False
1839
1850
1840 return make_lock, currently_locked, lock_info
1851 return make_lock, currently_locked, lock_info
1841
1852
1842 @property
1853 @property
1843 def last_db_change(self):
1854 def last_db_change(self):
1844 return self.updated_on
1855 return self.updated_on
1845
1856
1846 @property
1857 @property
1847 def clone_uri_hidden(self):
1858 def clone_uri_hidden(self):
1848 clone_uri = self.clone_uri
1859 clone_uri = self.clone_uri
1849 if clone_uri:
1860 if clone_uri:
1850 import urlobject
1861 import urlobject
1851 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1862 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1852 if url_obj.password:
1863 if url_obj.password:
1853 clone_uri = url_obj.with_password('*****')
1864 clone_uri = url_obj.with_password('*****')
1854 return clone_uri
1865 return clone_uri
1855
1866
1856 def clone_url(self, **override):
1867 def clone_url(self, **override):
1857 qualified_home_url = url('home', qualified=True)
1868 qualified_home_url = url('home', qualified=True)
1858
1869
1859 uri_tmpl = None
1870 uri_tmpl = None
1860 if 'with_id' in override:
1871 if 'with_id' in override:
1861 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1872 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1862 del override['with_id']
1873 del override['with_id']
1863
1874
1864 if 'uri_tmpl' in override:
1875 if 'uri_tmpl' in override:
1865 uri_tmpl = override['uri_tmpl']
1876 uri_tmpl = override['uri_tmpl']
1866 del override['uri_tmpl']
1877 del override['uri_tmpl']
1867
1878
1868 # we didn't override our tmpl from **overrides
1879 # we didn't override our tmpl from **overrides
1869 if not uri_tmpl:
1880 if not uri_tmpl:
1870 uri_tmpl = self.DEFAULT_CLONE_URI
1881 uri_tmpl = self.DEFAULT_CLONE_URI
1871 try:
1882 try:
1872 from pylons import tmpl_context as c
1883 from pylons import tmpl_context as c
1873 uri_tmpl = c.clone_uri_tmpl
1884 uri_tmpl = c.clone_uri_tmpl
1874 except Exception:
1885 except Exception:
1875 # in any case if we call this outside of request context,
1886 # in any case if we call this outside of request context,
1876 # ie, not having tmpl_context set up
1887 # ie, not having tmpl_context set up
1877 pass
1888 pass
1878
1889
1879 return get_clone_url(uri_tmpl=uri_tmpl,
1890 return get_clone_url(uri_tmpl=uri_tmpl,
1880 qualifed_home_url=qualified_home_url,
1891 qualifed_home_url=qualified_home_url,
1881 repo_name=self.repo_name,
1892 repo_name=self.repo_name,
1882 repo_id=self.repo_id, **override)
1893 repo_id=self.repo_id, **override)
1883
1894
1884 def set_state(self, state):
1895 def set_state(self, state):
1885 self.repo_state = state
1896 self.repo_state = state
1886 Session().add(self)
1897 Session().add(self)
1887 #==========================================================================
1898 #==========================================================================
1888 # SCM PROPERTIES
1899 # SCM PROPERTIES
1889 #==========================================================================
1900 #==========================================================================
1890
1901
1891 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1902 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1892 return get_commit_safe(
1903 return get_commit_safe(
1893 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1904 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1894
1905
1895 def get_changeset(self, rev=None, pre_load=None):
1906 def get_changeset(self, rev=None, pre_load=None):
1896 warnings.warn("Use get_commit", DeprecationWarning)
1907 warnings.warn("Use get_commit", DeprecationWarning)
1897 commit_id = None
1908 commit_id = None
1898 commit_idx = None
1909 commit_idx = None
1899 if isinstance(rev, basestring):
1910 if isinstance(rev, basestring):
1900 commit_id = rev
1911 commit_id = rev
1901 else:
1912 else:
1902 commit_idx = rev
1913 commit_idx = rev
1903 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1914 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1904 pre_load=pre_load)
1915 pre_load=pre_load)
1905
1916
1906 def get_landing_commit(self):
1917 def get_landing_commit(self):
1907 """
1918 """
1908 Returns landing commit, or if that doesn't exist returns the tip
1919 Returns landing commit, or if that doesn't exist returns the tip
1909 """
1920 """
1910 _rev_type, _rev = self.landing_rev
1921 _rev_type, _rev = self.landing_rev
1911 commit = self.get_commit(_rev)
1922 commit = self.get_commit(_rev)
1912 if isinstance(commit, EmptyCommit):
1923 if isinstance(commit, EmptyCommit):
1913 return self.get_commit()
1924 return self.get_commit()
1914 return commit
1925 return commit
1915
1926
1916 def update_commit_cache(self, cs_cache=None, config=None):
1927 def update_commit_cache(self, cs_cache=None, config=None):
1917 """
1928 """
1918 Update cache of last changeset for repository, keys should be::
1929 Update cache of last changeset for repository, keys should be::
1919
1930
1920 short_id
1931 short_id
1921 raw_id
1932 raw_id
1922 revision
1933 revision
1923 parents
1934 parents
1924 message
1935 message
1925 date
1936 date
1926 author
1937 author
1927
1938
1928 :param cs_cache:
1939 :param cs_cache:
1929 """
1940 """
1930 from rhodecode.lib.vcs.backends.base import BaseChangeset
1941 from rhodecode.lib.vcs.backends.base import BaseChangeset
1931 if cs_cache is None:
1942 if cs_cache is None:
1932 # use no-cache version here
1943 # use no-cache version here
1933 scm_repo = self.scm_instance(cache=False, config=config)
1944 scm_repo = self.scm_instance(cache=False, config=config)
1934 if scm_repo:
1945 if scm_repo:
1935 cs_cache = scm_repo.get_commit(
1946 cs_cache = scm_repo.get_commit(
1936 pre_load=["author", "date", "message", "parents"])
1947 pre_load=["author", "date", "message", "parents"])
1937 else:
1948 else:
1938 cs_cache = EmptyCommit()
1949 cs_cache = EmptyCommit()
1939
1950
1940 if isinstance(cs_cache, BaseChangeset):
1951 if isinstance(cs_cache, BaseChangeset):
1941 cs_cache = cs_cache.__json__()
1952 cs_cache = cs_cache.__json__()
1942
1953
1943 def is_outdated(new_cs_cache):
1954 def is_outdated(new_cs_cache):
1944 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1955 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1945 new_cs_cache['revision'] != self.changeset_cache['revision']):
1956 new_cs_cache['revision'] != self.changeset_cache['revision']):
1946 return True
1957 return True
1947 return False
1958 return False
1948
1959
1949 # check if we have maybe already latest cached revision
1960 # check if we have maybe already latest cached revision
1950 if is_outdated(cs_cache) or not self.changeset_cache:
1961 if is_outdated(cs_cache) or not self.changeset_cache:
1951 _default = datetime.datetime.fromtimestamp(0)
1962 _default = datetime.datetime.fromtimestamp(0)
1952 last_change = cs_cache.get('date') or _default
1963 last_change = cs_cache.get('date') or _default
1953 log.debug('updated repo %s with new cs cache %s',
1964 log.debug('updated repo %s with new cs cache %s',
1954 self.repo_name, cs_cache)
1965 self.repo_name, cs_cache)
1955 self.updated_on = last_change
1966 self.updated_on = last_change
1956 self.changeset_cache = cs_cache
1967 self.changeset_cache = cs_cache
1957 Session().add(self)
1968 Session().add(self)
1958 Session().commit()
1969 Session().commit()
1959 else:
1970 else:
1960 log.debug('Skipping update_commit_cache for repo:`%s` '
1971 log.debug('Skipping update_commit_cache for repo:`%s` '
1961 'commit already with latest changes', self.repo_name)
1972 'commit already with latest changes', self.repo_name)
1962
1973
1963 @property
1974 @property
1964 def tip(self):
1975 def tip(self):
1965 return self.get_commit('tip')
1976 return self.get_commit('tip')
1966
1977
1967 @property
1978 @property
1968 def author(self):
1979 def author(self):
1969 return self.tip.author
1980 return self.tip.author
1970
1981
1971 @property
1982 @property
1972 def last_change(self):
1983 def last_change(self):
1973 return self.scm_instance().last_change
1984 return self.scm_instance().last_change
1974
1985
1975 def get_comments(self, revisions=None):
1986 def get_comments(self, revisions=None):
1976 """
1987 """
1977 Returns comments for this repository grouped by revisions
1988 Returns comments for this repository grouped by revisions
1978
1989
1979 :param revisions: filter query by revisions only
1990 :param revisions: filter query by revisions only
1980 """
1991 """
1981 cmts = ChangesetComment.query()\
1992 cmts = ChangesetComment.query()\
1982 .filter(ChangesetComment.repo == self)
1993 .filter(ChangesetComment.repo == self)
1983 if revisions:
1994 if revisions:
1984 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1995 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1985 grouped = collections.defaultdict(list)
1996 grouped = collections.defaultdict(list)
1986 for cmt in cmts.all():
1997 for cmt in cmts.all():
1987 grouped[cmt.revision].append(cmt)
1998 grouped[cmt.revision].append(cmt)
1988 return grouped
1999 return grouped
1989
2000
1990 def statuses(self, revisions=None):
2001 def statuses(self, revisions=None):
1991 """
2002 """
1992 Returns statuses for this repository
2003 Returns statuses for this repository
1993
2004
1994 :param revisions: list of revisions to get statuses for
2005 :param revisions: list of revisions to get statuses for
1995 """
2006 """
1996 statuses = ChangesetStatus.query()\
2007 statuses = ChangesetStatus.query()\
1997 .filter(ChangesetStatus.repo == self)\
2008 .filter(ChangesetStatus.repo == self)\
1998 .filter(ChangesetStatus.version == 0)
2009 .filter(ChangesetStatus.version == 0)
1999
2010
2000 if revisions:
2011 if revisions:
2001 # Try doing the filtering in chunks to avoid hitting limits
2012 # Try doing the filtering in chunks to avoid hitting limits
2002 size = 500
2013 size = 500
2003 status_results = []
2014 status_results = []
2004 for chunk in xrange(0, len(revisions), size):
2015 for chunk in xrange(0, len(revisions), size):
2005 status_results += statuses.filter(
2016 status_results += statuses.filter(
2006 ChangesetStatus.revision.in_(
2017 ChangesetStatus.revision.in_(
2007 revisions[chunk: chunk+size])
2018 revisions[chunk: chunk+size])
2008 ).all()
2019 ).all()
2009 else:
2020 else:
2010 status_results = statuses.all()
2021 status_results = statuses.all()
2011
2022
2012 grouped = {}
2023 grouped = {}
2013
2024
2014 # maybe we have open new pullrequest without a status?
2025 # maybe we have open new pullrequest without a status?
2015 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2026 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2016 status_lbl = ChangesetStatus.get_status_lbl(stat)
2027 status_lbl = ChangesetStatus.get_status_lbl(stat)
2017 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2028 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2018 for rev in pr.revisions:
2029 for rev in pr.revisions:
2019 pr_id = pr.pull_request_id
2030 pr_id = pr.pull_request_id
2020 pr_repo = pr.target_repo.repo_name
2031 pr_repo = pr.target_repo.repo_name
2021 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2032 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2022
2033
2023 for stat in status_results:
2034 for stat in status_results:
2024 pr_id = pr_repo = None
2035 pr_id = pr_repo = None
2025 if stat.pull_request:
2036 if stat.pull_request:
2026 pr_id = stat.pull_request.pull_request_id
2037 pr_id = stat.pull_request.pull_request_id
2027 pr_repo = stat.pull_request.target_repo.repo_name
2038 pr_repo = stat.pull_request.target_repo.repo_name
2028 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2039 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2029 pr_id, pr_repo]
2040 pr_id, pr_repo]
2030 return grouped
2041 return grouped
2031
2042
2032 # ==========================================================================
2043 # ==========================================================================
2033 # SCM CACHE INSTANCE
2044 # SCM CACHE INSTANCE
2034 # ==========================================================================
2045 # ==========================================================================
2035
2046
2036 def scm_instance(self, **kwargs):
2047 def scm_instance(self, **kwargs):
2037 import rhodecode
2048 import rhodecode
2038
2049
2039 # Passing a config will not hit the cache currently only used
2050 # Passing a config will not hit the cache currently only used
2040 # for repo2dbmapper
2051 # for repo2dbmapper
2041 config = kwargs.pop('config', None)
2052 config = kwargs.pop('config', None)
2042 cache = kwargs.pop('cache', None)
2053 cache = kwargs.pop('cache', None)
2043 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2054 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2044 # if cache is NOT defined use default global, else we have a full
2055 # if cache is NOT defined use default global, else we have a full
2045 # control over cache behaviour
2056 # control over cache behaviour
2046 if cache is None and full_cache and not config:
2057 if cache is None and full_cache and not config:
2047 return self._get_instance_cached()
2058 return self._get_instance_cached()
2048 return self._get_instance(cache=bool(cache), config=config)
2059 return self._get_instance(cache=bool(cache), config=config)
2049
2060
2050 def _get_instance_cached(self):
2061 def _get_instance_cached(self):
2051 @cache_region('long_term')
2062 @cache_region('long_term')
2052 def _get_repo(cache_key):
2063 def _get_repo(cache_key):
2053 return self._get_instance()
2064 return self._get_instance()
2054
2065
2055 invalidator_context = CacheKey.repo_context_cache(
2066 invalidator_context = CacheKey.repo_context_cache(
2056 _get_repo, self.repo_name, None, thread_scoped=True)
2067 _get_repo, self.repo_name, None, thread_scoped=True)
2057
2068
2058 with invalidator_context as context:
2069 with invalidator_context as context:
2059 context.invalidate()
2070 context.invalidate()
2060 repo = context.compute()
2071 repo = context.compute()
2061
2072
2062 return repo
2073 return repo
2063
2074
2064 def _get_instance(self, cache=True, config=None):
2075 def _get_instance(self, cache=True, config=None):
2065 config = config or self._config
2076 config = config or self._config
2066 custom_wire = {
2077 custom_wire = {
2067 'cache': cache # controls the vcs.remote cache
2078 'cache': cache # controls the vcs.remote cache
2068 }
2079 }
2069 repo = get_vcs_instance(
2080 repo = get_vcs_instance(
2070 repo_path=safe_str(self.repo_full_path),
2081 repo_path=safe_str(self.repo_full_path),
2071 config=config,
2082 config=config,
2072 with_wire=custom_wire,
2083 with_wire=custom_wire,
2073 create=False,
2084 create=False,
2074 _vcs_alias=self.repo_type)
2085 _vcs_alias=self.repo_type)
2075
2086
2076 return repo
2087 return repo
2077
2088
2078 def __json__(self):
2089 def __json__(self):
2079 return {'landing_rev': self.landing_rev}
2090 return {'landing_rev': self.landing_rev}
2080
2091
2081 def get_dict(self):
2092 def get_dict(self):
2082
2093
2083 # Since we transformed `repo_name` to a hybrid property, we need to
2094 # Since we transformed `repo_name` to a hybrid property, we need to
2084 # keep compatibility with the code which uses `repo_name` field.
2095 # keep compatibility with the code which uses `repo_name` field.
2085
2096
2086 result = super(Repository, self).get_dict()
2097 result = super(Repository, self).get_dict()
2087 result['repo_name'] = result.pop('_repo_name', None)
2098 result['repo_name'] = result.pop('_repo_name', None)
2088 return result
2099 return result
2089
2100
2090
2101
2091 class RepoGroup(Base, BaseModel):
2102 class RepoGroup(Base, BaseModel):
2092 __tablename__ = 'groups'
2103 __tablename__ = 'groups'
2093 __table_args__ = (
2104 __table_args__ = (
2094 UniqueConstraint('group_name', 'group_parent_id'),
2105 UniqueConstraint('group_name', 'group_parent_id'),
2095 CheckConstraint('group_id != group_parent_id'),
2106 CheckConstraint('group_id != group_parent_id'),
2096 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2107 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2097 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2108 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2098 )
2109 )
2099 __mapper_args__ = {'order_by': 'group_name'}
2110 __mapper_args__ = {'order_by': 'group_name'}
2100
2111
2101 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2112 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2102
2113
2103 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2114 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2104 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2115 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2105 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2116 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2106 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2117 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2107 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2118 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2108 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2119 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2109 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2120 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2110 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2121 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2111
2122
2112 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2123 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2113 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2124 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2114 parent_group = relationship('RepoGroup', remote_side=group_id)
2125 parent_group = relationship('RepoGroup', remote_side=group_id)
2115 user = relationship('User')
2126 user = relationship('User')
2116 integrations = relationship('Integration',
2127 integrations = relationship('Integration',
2117 cascade="all, delete, delete-orphan")
2128 cascade="all, delete, delete-orphan")
2118
2129
2119 def __init__(self, group_name='', parent_group=None):
2130 def __init__(self, group_name='', parent_group=None):
2120 self.group_name = group_name
2131 self.group_name = group_name
2121 self.parent_group = parent_group
2132 self.parent_group = parent_group
2122
2133
2123 def __unicode__(self):
2134 def __unicode__(self):
2124 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2135 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2125 self.group_name)
2136 self.group_name)
2126
2137
2127 @classmethod
2138 @classmethod
2128 def _generate_choice(cls, repo_group):
2139 def _generate_choice(cls, repo_group):
2129 from webhelpers.html import literal as _literal
2140 from webhelpers.html import literal as _literal
2130 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2141 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2131 return repo_group.group_id, _name(repo_group.full_path_splitted)
2142 return repo_group.group_id, _name(repo_group.full_path_splitted)
2132
2143
2133 @classmethod
2144 @classmethod
2134 def groups_choices(cls, groups=None, show_empty_group=True):
2145 def groups_choices(cls, groups=None, show_empty_group=True):
2135 if not groups:
2146 if not groups:
2136 groups = cls.query().all()
2147 groups = cls.query().all()
2137
2148
2138 repo_groups = []
2149 repo_groups = []
2139 if show_empty_group:
2150 if show_empty_group:
2140 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2151 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2141
2152
2142 repo_groups.extend([cls._generate_choice(x) for x in groups])
2153 repo_groups.extend([cls._generate_choice(x) for x in groups])
2143
2154
2144 repo_groups = sorted(
2155 repo_groups = sorted(
2145 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2156 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2146 return repo_groups
2157 return repo_groups
2147
2158
2148 @classmethod
2159 @classmethod
2149 def url_sep(cls):
2160 def url_sep(cls):
2150 return URL_SEP
2161 return URL_SEP
2151
2162
2152 @classmethod
2163 @classmethod
2153 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2164 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2154 if case_insensitive:
2165 if case_insensitive:
2155 gr = cls.query().filter(func.lower(cls.group_name)
2166 gr = cls.query().filter(func.lower(cls.group_name)
2156 == func.lower(group_name))
2167 == func.lower(group_name))
2157 else:
2168 else:
2158 gr = cls.query().filter(cls.group_name == group_name)
2169 gr = cls.query().filter(cls.group_name == group_name)
2159 if cache:
2170 if cache:
2160 gr = gr.options(FromCache(
2171 gr = gr.options(FromCache(
2161 "sql_cache_short",
2172 "sql_cache_short",
2162 "get_group_%s" % _hash_key(group_name)))
2173 "get_group_%s" % _hash_key(group_name)))
2163 return gr.scalar()
2174 return gr.scalar()
2164
2175
2165 @classmethod
2176 @classmethod
2166 def get_user_personal_repo_group(cls, user_id):
2177 def get_user_personal_repo_group(cls, user_id):
2167 user = User.get(user_id)
2178 user = User.get(user_id)
2168 return cls.query()\
2179 return cls.query()\
2169 .filter(cls.personal == true())\
2180 .filter(cls.personal == true())\
2170 .filter(cls.user == user).scalar()
2181 .filter(cls.user == user).scalar()
2171
2182
2172 @classmethod
2183 @classmethod
2173 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2184 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2174 case_insensitive=True):
2185 case_insensitive=True):
2175 q = RepoGroup.query()
2186 q = RepoGroup.query()
2176
2187
2177 if not isinstance(user_id, Optional):
2188 if not isinstance(user_id, Optional):
2178 q = q.filter(RepoGroup.user_id == user_id)
2189 q = q.filter(RepoGroup.user_id == user_id)
2179
2190
2180 if not isinstance(group_id, Optional):
2191 if not isinstance(group_id, Optional):
2181 q = q.filter(RepoGroup.group_parent_id == group_id)
2192 q = q.filter(RepoGroup.group_parent_id == group_id)
2182
2193
2183 if case_insensitive:
2194 if case_insensitive:
2184 q = q.order_by(func.lower(RepoGroup.group_name))
2195 q = q.order_by(func.lower(RepoGroup.group_name))
2185 else:
2196 else:
2186 q = q.order_by(RepoGroup.group_name)
2197 q = q.order_by(RepoGroup.group_name)
2187 return q.all()
2198 return q.all()
2188
2199
2189 @property
2200 @property
2190 def parents(self):
2201 def parents(self):
2191 parents_recursion_limit = 10
2202 parents_recursion_limit = 10
2192 groups = []
2203 groups = []
2193 if self.parent_group is None:
2204 if self.parent_group is None:
2194 return groups
2205 return groups
2195 cur_gr = self.parent_group
2206 cur_gr = self.parent_group
2196 groups.insert(0, cur_gr)
2207 groups.insert(0, cur_gr)
2197 cnt = 0
2208 cnt = 0
2198 while 1:
2209 while 1:
2199 cnt += 1
2210 cnt += 1
2200 gr = getattr(cur_gr, 'parent_group', None)
2211 gr = getattr(cur_gr, 'parent_group', None)
2201 cur_gr = cur_gr.parent_group
2212 cur_gr = cur_gr.parent_group
2202 if gr is None:
2213 if gr is None:
2203 break
2214 break
2204 if cnt == parents_recursion_limit:
2215 if cnt == parents_recursion_limit:
2205 # this will prevent accidental infinit loops
2216 # this will prevent accidental infinit loops
2206 log.error(('more than %s parents found for group %s, stopping '
2217 log.error(('more than %s parents found for group %s, stopping '
2207 'recursive parent fetching' % (parents_recursion_limit, self)))
2218 'recursive parent fetching' % (parents_recursion_limit, self)))
2208 break
2219 break
2209
2220
2210 groups.insert(0, gr)
2221 groups.insert(0, gr)
2211 return groups
2222 return groups
2212
2223
2213 @property
2224 @property
2214 def children(self):
2225 def children(self):
2215 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2226 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2216
2227
2217 @property
2228 @property
2218 def name(self):
2229 def name(self):
2219 return self.group_name.split(RepoGroup.url_sep())[-1]
2230 return self.group_name.split(RepoGroup.url_sep())[-1]
2220
2231
2221 @property
2232 @property
2222 def full_path(self):
2233 def full_path(self):
2223 return self.group_name
2234 return self.group_name
2224
2235
2225 @property
2236 @property
2226 def full_path_splitted(self):
2237 def full_path_splitted(self):
2227 return self.group_name.split(RepoGroup.url_sep())
2238 return self.group_name.split(RepoGroup.url_sep())
2228
2239
2229 @property
2240 @property
2230 def repositories(self):
2241 def repositories(self):
2231 return Repository.query()\
2242 return Repository.query()\
2232 .filter(Repository.group == self)\
2243 .filter(Repository.group == self)\
2233 .order_by(Repository.repo_name)
2244 .order_by(Repository.repo_name)
2234
2245
2235 @property
2246 @property
2236 def repositories_recursive_count(self):
2247 def repositories_recursive_count(self):
2237 cnt = self.repositories.count()
2248 cnt = self.repositories.count()
2238
2249
2239 def children_count(group):
2250 def children_count(group):
2240 cnt = 0
2251 cnt = 0
2241 for child in group.children:
2252 for child in group.children:
2242 cnt += child.repositories.count()
2253 cnt += child.repositories.count()
2243 cnt += children_count(child)
2254 cnt += children_count(child)
2244 return cnt
2255 return cnt
2245
2256
2246 return cnt + children_count(self)
2257 return cnt + children_count(self)
2247
2258
2248 def _recursive_objects(self, include_repos=True):
2259 def _recursive_objects(self, include_repos=True):
2249 all_ = []
2260 all_ = []
2250
2261
2251 def _get_members(root_gr):
2262 def _get_members(root_gr):
2252 if include_repos:
2263 if include_repos:
2253 for r in root_gr.repositories:
2264 for r in root_gr.repositories:
2254 all_.append(r)
2265 all_.append(r)
2255 childs = root_gr.children.all()
2266 childs = root_gr.children.all()
2256 if childs:
2267 if childs:
2257 for gr in childs:
2268 for gr in childs:
2258 all_.append(gr)
2269 all_.append(gr)
2259 _get_members(gr)
2270 _get_members(gr)
2260
2271
2261 _get_members(self)
2272 _get_members(self)
2262 return [self] + all_
2273 return [self] + all_
2263
2274
2264 def recursive_groups_and_repos(self):
2275 def recursive_groups_and_repos(self):
2265 """
2276 """
2266 Recursive return all groups, with repositories in those groups
2277 Recursive return all groups, with repositories in those groups
2267 """
2278 """
2268 return self._recursive_objects()
2279 return self._recursive_objects()
2269
2280
2270 def recursive_groups(self):
2281 def recursive_groups(self):
2271 """
2282 """
2272 Returns all children groups for this group including children of children
2283 Returns all children groups for this group including children of children
2273 """
2284 """
2274 return self._recursive_objects(include_repos=False)
2285 return self._recursive_objects(include_repos=False)
2275
2286
2276 def get_new_name(self, group_name):
2287 def get_new_name(self, group_name):
2277 """
2288 """
2278 returns new full group name based on parent and new name
2289 returns new full group name based on parent and new name
2279
2290
2280 :param group_name:
2291 :param group_name:
2281 """
2292 """
2282 path_prefix = (self.parent_group.full_path_splitted if
2293 path_prefix = (self.parent_group.full_path_splitted if
2283 self.parent_group else [])
2294 self.parent_group else [])
2284 return RepoGroup.url_sep().join(path_prefix + [group_name])
2295 return RepoGroup.url_sep().join(path_prefix + [group_name])
2285
2296
2286 def permissions(self, with_admins=True, with_owner=True):
2297 def permissions(self, with_admins=True, with_owner=True):
2287 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2298 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2288 q = q.options(joinedload(UserRepoGroupToPerm.group),
2299 q = q.options(joinedload(UserRepoGroupToPerm.group),
2289 joinedload(UserRepoGroupToPerm.user),
2300 joinedload(UserRepoGroupToPerm.user),
2290 joinedload(UserRepoGroupToPerm.permission),)
2301 joinedload(UserRepoGroupToPerm.permission),)
2291
2302
2292 # get owners and admins and permissions. We do a trick of re-writing
2303 # get owners and admins and permissions. We do a trick of re-writing
2293 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2304 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2294 # has a global reference and changing one object propagates to all
2305 # has a global reference and changing one object propagates to all
2295 # others. This means if admin is also an owner admin_row that change
2306 # others. This means if admin is also an owner admin_row that change
2296 # would propagate to both objects
2307 # would propagate to both objects
2297 perm_rows = []
2308 perm_rows = []
2298 for _usr in q.all():
2309 for _usr in q.all():
2299 usr = AttributeDict(_usr.user.get_dict())
2310 usr = AttributeDict(_usr.user.get_dict())
2300 usr.permission = _usr.permission.permission_name
2311 usr.permission = _usr.permission.permission_name
2301 perm_rows.append(usr)
2312 perm_rows.append(usr)
2302
2313
2303 # filter the perm rows by 'default' first and then sort them by
2314 # filter the perm rows by 'default' first and then sort them by
2304 # admin,write,read,none permissions sorted again alphabetically in
2315 # admin,write,read,none permissions sorted again alphabetically in
2305 # each group
2316 # each group
2306 perm_rows = sorted(perm_rows, key=display_sort)
2317 perm_rows = sorted(perm_rows, key=display_sort)
2307
2318
2308 _admin_perm = 'group.admin'
2319 _admin_perm = 'group.admin'
2309 owner_row = []
2320 owner_row = []
2310 if with_owner:
2321 if with_owner:
2311 usr = AttributeDict(self.user.get_dict())
2322 usr = AttributeDict(self.user.get_dict())
2312 usr.owner_row = True
2323 usr.owner_row = True
2313 usr.permission = _admin_perm
2324 usr.permission = _admin_perm
2314 owner_row.append(usr)
2325 owner_row.append(usr)
2315
2326
2316 super_admin_rows = []
2327 super_admin_rows = []
2317 if with_admins:
2328 if with_admins:
2318 for usr in User.get_all_super_admins():
2329 for usr in User.get_all_super_admins():
2319 # if this admin is also owner, don't double the record
2330 # if this admin is also owner, don't double the record
2320 if usr.user_id == owner_row[0].user_id:
2331 if usr.user_id == owner_row[0].user_id:
2321 owner_row[0].admin_row = True
2332 owner_row[0].admin_row = True
2322 else:
2333 else:
2323 usr = AttributeDict(usr.get_dict())
2334 usr = AttributeDict(usr.get_dict())
2324 usr.admin_row = True
2335 usr.admin_row = True
2325 usr.permission = _admin_perm
2336 usr.permission = _admin_perm
2326 super_admin_rows.append(usr)
2337 super_admin_rows.append(usr)
2327
2338
2328 return super_admin_rows + owner_row + perm_rows
2339 return super_admin_rows + owner_row + perm_rows
2329
2340
2330 def permission_user_groups(self):
2341 def permission_user_groups(self):
2331 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2342 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2332 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2343 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2333 joinedload(UserGroupRepoGroupToPerm.users_group),
2344 joinedload(UserGroupRepoGroupToPerm.users_group),
2334 joinedload(UserGroupRepoGroupToPerm.permission),)
2345 joinedload(UserGroupRepoGroupToPerm.permission),)
2335
2346
2336 perm_rows = []
2347 perm_rows = []
2337 for _user_group in q.all():
2348 for _user_group in q.all():
2338 usr = AttributeDict(_user_group.users_group.get_dict())
2349 usr = AttributeDict(_user_group.users_group.get_dict())
2339 usr.permission = _user_group.permission.permission_name
2350 usr.permission = _user_group.permission.permission_name
2340 perm_rows.append(usr)
2351 perm_rows.append(usr)
2341
2352
2342 return perm_rows
2353 return perm_rows
2343
2354
2344 def get_api_data(self):
2355 def get_api_data(self):
2345 """
2356 """
2346 Common function for generating api data
2357 Common function for generating api data
2347
2358
2348 """
2359 """
2349 group = self
2360 group = self
2350 data = {
2361 data = {
2351 'group_id': group.group_id,
2362 'group_id': group.group_id,
2352 'group_name': group.group_name,
2363 'group_name': group.group_name,
2353 'group_description': group.group_description,
2364 'group_description': group.group_description,
2354 'parent_group': group.parent_group.group_name if group.parent_group else None,
2365 'parent_group': group.parent_group.group_name if group.parent_group else None,
2355 'repositories': [x.repo_name for x in group.repositories],
2366 'repositories': [x.repo_name for x in group.repositories],
2356 'owner': group.user.username,
2367 'owner': group.user.username,
2357 }
2368 }
2358 return data
2369 return data
2359
2370
2360
2371
2361 class Permission(Base, BaseModel):
2372 class Permission(Base, BaseModel):
2362 __tablename__ = 'permissions'
2373 __tablename__ = 'permissions'
2363 __table_args__ = (
2374 __table_args__ = (
2364 Index('p_perm_name_idx', 'permission_name'),
2375 Index('p_perm_name_idx', 'permission_name'),
2365 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2376 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2366 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2377 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2367 )
2378 )
2368 PERMS = [
2379 PERMS = [
2369 ('hg.admin', _('RhodeCode Super Administrator')),
2380 ('hg.admin', _('RhodeCode Super Administrator')),
2370
2381
2371 ('repository.none', _('Repository no access')),
2382 ('repository.none', _('Repository no access')),
2372 ('repository.read', _('Repository read access')),
2383 ('repository.read', _('Repository read access')),
2373 ('repository.write', _('Repository write access')),
2384 ('repository.write', _('Repository write access')),
2374 ('repository.admin', _('Repository admin access')),
2385 ('repository.admin', _('Repository admin access')),
2375
2386
2376 ('group.none', _('Repository group no access')),
2387 ('group.none', _('Repository group no access')),
2377 ('group.read', _('Repository group read access')),
2388 ('group.read', _('Repository group read access')),
2378 ('group.write', _('Repository group write access')),
2389 ('group.write', _('Repository group write access')),
2379 ('group.admin', _('Repository group admin access')),
2390 ('group.admin', _('Repository group admin access')),
2380
2391
2381 ('usergroup.none', _('User group no access')),
2392 ('usergroup.none', _('User group no access')),
2382 ('usergroup.read', _('User group read access')),
2393 ('usergroup.read', _('User group read access')),
2383 ('usergroup.write', _('User group write access')),
2394 ('usergroup.write', _('User group write access')),
2384 ('usergroup.admin', _('User group admin access')),
2395 ('usergroup.admin', _('User group admin access')),
2385
2396
2386 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2397 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2387 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2398 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2388
2399
2389 ('hg.usergroup.create.false', _('User Group creation disabled')),
2400 ('hg.usergroup.create.false', _('User Group creation disabled')),
2390 ('hg.usergroup.create.true', _('User Group creation enabled')),
2401 ('hg.usergroup.create.true', _('User Group creation enabled')),
2391
2402
2392 ('hg.create.none', _('Repository creation disabled')),
2403 ('hg.create.none', _('Repository creation disabled')),
2393 ('hg.create.repository', _('Repository creation enabled')),
2404 ('hg.create.repository', _('Repository creation enabled')),
2394 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2405 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2395 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2406 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2396
2407
2397 ('hg.fork.none', _('Repository forking disabled')),
2408 ('hg.fork.none', _('Repository forking disabled')),
2398 ('hg.fork.repository', _('Repository forking enabled')),
2409 ('hg.fork.repository', _('Repository forking enabled')),
2399
2410
2400 ('hg.register.none', _('Registration disabled')),
2411 ('hg.register.none', _('Registration disabled')),
2401 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2412 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2402 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2413 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2403
2414
2404 ('hg.password_reset.enabled', _('Password reset enabled')),
2415 ('hg.password_reset.enabled', _('Password reset enabled')),
2405 ('hg.password_reset.hidden', _('Password reset hidden')),
2416 ('hg.password_reset.hidden', _('Password reset hidden')),
2406 ('hg.password_reset.disabled', _('Password reset disabled')),
2417 ('hg.password_reset.disabled', _('Password reset disabled')),
2407
2418
2408 ('hg.extern_activate.manual', _('Manual activation of external account')),
2419 ('hg.extern_activate.manual', _('Manual activation of external account')),
2409 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2420 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2410
2421
2411 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2422 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2412 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2423 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2413 ]
2424 ]
2414
2425
2415 # definition of system default permissions for DEFAULT user
2426 # definition of system default permissions for DEFAULT user
2416 DEFAULT_USER_PERMISSIONS = [
2427 DEFAULT_USER_PERMISSIONS = [
2417 'repository.read',
2428 'repository.read',
2418 'group.read',
2429 'group.read',
2419 'usergroup.read',
2430 'usergroup.read',
2420 'hg.create.repository',
2431 'hg.create.repository',
2421 'hg.repogroup.create.false',
2432 'hg.repogroup.create.false',
2422 'hg.usergroup.create.false',
2433 'hg.usergroup.create.false',
2423 'hg.create.write_on_repogroup.true',
2434 'hg.create.write_on_repogroup.true',
2424 'hg.fork.repository',
2435 'hg.fork.repository',
2425 'hg.register.manual_activate',
2436 'hg.register.manual_activate',
2426 'hg.password_reset.enabled',
2437 'hg.password_reset.enabled',
2427 'hg.extern_activate.auto',
2438 'hg.extern_activate.auto',
2428 'hg.inherit_default_perms.true',
2439 'hg.inherit_default_perms.true',
2429 ]
2440 ]
2430
2441
2431 # defines which permissions are more important higher the more important
2442 # defines which permissions are more important higher the more important
2432 # Weight defines which permissions are more important.
2443 # Weight defines which permissions are more important.
2433 # The higher number the more important.
2444 # The higher number the more important.
2434 PERM_WEIGHTS = {
2445 PERM_WEIGHTS = {
2435 'repository.none': 0,
2446 'repository.none': 0,
2436 'repository.read': 1,
2447 'repository.read': 1,
2437 'repository.write': 3,
2448 'repository.write': 3,
2438 'repository.admin': 4,
2449 'repository.admin': 4,
2439
2450
2440 'group.none': 0,
2451 'group.none': 0,
2441 'group.read': 1,
2452 'group.read': 1,
2442 'group.write': 3,
2453 'group.write': 3,
2443 'group.admin': 4,
2454 'group.admin': 4,
2444
2455
2445 'usergroup.none': 0,
2456 'usergroup.none': 0,
2446 'usergroup.read': 1,
2457 'usergroup.read': 1,
2447 'usergroup.write': 3,
2458 'usergroup.write': 3,
2448 'usergroup.admin': 4,
2459 'usergroup.admin': 4,
2449
2460
2450 'hg.repogroup.create.false': 0,
2461 'hg.repogroup.create.false': 0,
2451 'hg.repogroup.create.true': 1,
2462 'hg.repogroup.create.true': 1,
2452
2463
2453 'hg.usergroup.create.false': 0,
2464 'hg.usergroup.create.false': 0,
2454 'hg.usergroup.create.true': 1,
2465 'hg.usergroup.create.true': 1,
2455
2466
2456 'hg.fork.none': 0,
2467 'hg.fork.none': 0,
2457 'hg.fork.repository': 1,
2468 'hg.fork.repository': 1,
2458 'hg.create.none': 0,
2469 'hg.create.none': 0,
2459 'hg.create.repository': 1
2470 'hg.create.repository': 1
2460 }
2471 }
2461
2472
2462 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2473 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2463 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2474 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2464 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2475 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2465
2476
2466 def __unicode__(self):
2477 def __unicode__(self):
2467 return u"<%s('%s:%s')>" % (
2478 return u"<%s('%s:%s')>" % (
2468 self.__class__.__name__, self.permission_id, self.permission_name
2479 self.__class__.__name__, self.permission_id, self.permission_name
2469 )
2480 )
2470
2481
2471 @classmethod
2482 @classmethod
2472 def get_by_key(cls, key):
2483 def get_by_key(cls, key):
2473 return cls.query().filter(cls.permission_name == key).scalar()
2484 return cls.query().filter(cls.permission_name == key).scalar()
2474
2485
2475 @classmethod
2486 @classmethod
2476 def get_default_repo_perms(cls, user_id, repo_id=None):
2487 def get_default_repo_perms(cls, user_id, repo_id=None):
2477 q = Session().query(UserRepoToPerm, Repository, Permission)\
2488 q = Session().query(UserRepoToPerm, Repository, Permission)\
2478 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2489 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2479 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2490 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2480 .filter(UserRepoToPerm.user_id == user_id)
2491 .filter(UserRepoToPerm.user_id == user_id)
2481 if repo_id:
2492 if repo_id:
2482 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2493 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2483 return q.all()
2494 return q.all()
2484
2495
2485 @classmethod
2496 @classmethod
2486 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2497 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2487 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2498 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2488 .join(
2499 .join(
2489 Permission,
2500 Permission,
2490 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2501 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2491 .join(
2502 .join(
2492 Repository,
2503 Repository,
2493 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2504 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2494 .join(
2505 .join(
2495 UserGroup,
2506 UserGroup,
2496 UserGroupRepoToPerm.users_group_id ==
2507 UserGroupRepoToPerm.users_group_id ==
2497 UserGroup.users_group_id)\
2508 UserGroup.users_group_id)\
2498 .join(
2509 .join(
2499 UserGroupMember,
2510 UserGroupMember,
2500 UserGroupRepoToPerm.users_group_id ==
2511 UserGroupRepoToPerm.users_group_id ==
2501 UserGroupMember.users_group_id)\
2512 UserGroupMember.users_group_id)\
2502 .filter(
2513 .filter(
2503 UserGroupMember.user_id == user_id,
2514 UserGroupMember.user_id == user_id,
2504 UserGroup.users_group_active == true())
2515 UserGroup.users_group_active == true())
2505 if repo_id:
2516 if repo_id:
2506 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2517 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2507 return q.all()
2518 return q.all()
2508
2519
2509 @classmethod
2520 @classmethod
2510 def get_default_group_perms(cls, user_id, repo_group_id=None):
2521 def get_default_group_perms(cls, user_id, repo_group_id=None):
2511 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2522 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2512 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2523 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2513 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2524 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2514 .filter(UserRepoGroupToPerm.user_id == user_id)
2525 .filter(UserRepoGroupToPerm.user_id == user_id)
2515 if repo_group_id:
2526 if repo_group_id:
2516 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2527 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2517 return q.all()
2528 return q.all()
2518
2529
2519 @classmethod
2530 @classmethod
2520 def get_default_group_perms_from_user_group(
2531 def get_default_group_perms_from_user_group(
2521 cls, user_id, repo_group_id=None):
2532 cls, user_id, repo_group_id=None):
2522 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2533 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2523 .join(
2534 .join(
2524 Permission,
2535 Permission,
2525 UserGroupRepoGroupToPerm.permission_id ==
2536 UserGroupRepoGroupToPerm.permission_id ==
2526 Permission.permission_id)\
2537 Permission.permission_id)\
2527 .join(
2538 .join(
2528 RepoGroup,
2539 RepoGroup,
2529 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2540 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2530 .join(
2541 .join(
2531 UserGroup,
2542 UserGroup,
2532 UserGroupRepoGroupToPerm.users_group_id ==
2543 UserGroupRepoGroupToPerm.users_group_id ==
2533 UserGroup.users_group_id)\
2544 UserGroup.users_group_id)\
2534 .join(
2545 .join(
2535 UserGroupMember,
2546 UserGroupMember,
2536 UserGroupRepoGroupToPerm.users_group_id ==
2547 UserGroupRepoGroupToPerm.users_group_id ==
2537 UserGroupMember.users_group_id)\
2548 UserGroupMember.users_group_id)\
2538 .filter(
2549 .filter(
2539 UserGroupMember.user_id == user_id,
2550 UserGroupMember.user_id == user_id,
2540 UserGroup.users_group_active == true())
2551 UserGroup.users_group_active == true())
2541 if repo_group_id:
2552 if repo_group_id:
2542 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2553 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2543 return q.all()
2554 return q.all()
2544
2555
2545 @classmethod
2556 @classmethod
2546 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2557 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2547 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2558 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2548 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2559 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2549 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2560 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2550 .filter(UserUserGroupToPerm.user_id == user_id)
2561 .filter(UserUserGroupToPerm.user_id == user_id)
2551 if user_group_id:
2562 if user_group_id:
2552 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2563 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2553 return q.all()
2564 return q.all()
2554
2565
2555 @classmethod
2566 @classmethod
2556 def get_default_user_group_perms_from_user_group(
2567 def get_default_user_group_perms_from_user_group(
2557 cls, user_id, user_group_id=None):
2568 cls, user_id, user_group_id=None):
2558 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2569 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2559 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2570 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2560 .join(
2571 .join(
2561 Permission,
2572 Permission,
2562 UserGroupUserGroupToPerm.permission_id ==
2573 UserGroupUserGroupToPerm.permission_id ==
2563 Permission.permission_id)\
2574 Permission.permission_id)\
2564 .join(
2575 .join(
2565 TargetUserGroup,
2576 TargetUserGroup,
2566 UserGroupUserGroupToPerm.target_user_group_id ==
2577 UserGroupUserGroupToPerm.target_user_group_id ==
2567 TargetUserGroup.users_group_id)\
2578 TargetUserGroup.users_group_id)\
2568 .join(
2579 .join(
2569 UserGroup,
2580 UserGroup,
2570 UserGroupUserGroupToPerm.user_group_id ==
2581 UserGroupUserGroupToPerm.user_group_id ==
2571 UserGroup.users_group_id)\
2582 UserGroup.users_group_id)\
2572 .join(
2583 .join(
2573 UserGroupMember,
2584 UserGroupMember,
2574 UserGroupUserGroupToPerm.user_group_id ==
2585 UserGroupUserGroupToPerm.user_group_id ==
2575 UserGroupMember.users_group_id)\
2586 UserGroupMember.users_group_id)\
2576 .filter(
2587 .filter(
2577 UserGroupMember.user_id == user_id,
2588 UserGroupMember.user_id == user_id,
2578 UserGroup.users_group_active == true())
2589 UserGroup.users_group_active == true())
2579 if user_group_id:
2590 if user_group_id:
2580 q = q.filter(
2591 q = q.filter(
2581 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2592 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2582
2593
2583 return q.all()
2594 return q.all()
2584
2595
2585
2596
2586 class UserRepoToPerm(Base, BaseModel):
2597 class UserRepoToPerm(Base, BaseModel):
2587 __tablename__ = 'repo_to_perm'
2598 __tablename__ = 'repo_to_perm'
2588 __table_args__ = (
2599 __table_args__ = (
2589 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2600 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2590 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2601 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2591 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2602 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2592 )
2603 )
2593 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2604 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2594 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2605 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2595 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2606 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2596 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2607 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2597
2608
2598 user = relationship('User')
2609 user = relationship('User')
2599 repository = relationship('Repository')
2610 repository = relationship('Repository')
2600 permission = relationship('Permission')
2611 permission = relationship('Permission')
2601
2612
2602 @classmethod
2613 @classmethod
2603 def create(cls, user, repository, permission):
2614 def create(cls, user, repository, permission):
2604 n = cls()
2615 n = cls()
2605 n.user = user
2616 n.user = user
2606 n.repository = repository
2617 n.repository = repository
2607 n.permission = permission
2618 n.permission = permission
2608 Session().add(n)
2619 Session().add(n)
2609 return n
2620 return n
2610
2621
2611 def __unicode__(self):
2622 def __unicode__(self):
2612 return u'<%s => %s >' % (self.user, self.repository)
2623 return u'<%s => %s >' % (self.user, self.repository)
2613
2624
2614
2625
2615 class UserUserGroupToPerm(Base, BaseModel):
2626 class UserUserGroupToPerm(Base, BaseModel):
2616 __tablename__ = 'user_user_group_to_perm'
2627 __tablename__ = 'user_user_group_to_perm'
2617 __table_args__ = (
2628 __table_args__ = (
2618 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2629 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2619 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2620 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2631 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2621 )
2632 )
2622 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2633 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2623 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2634 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2624 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2635 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2625 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2636 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2626
2637
2627 user = relationship('User')
2638 user = relationship('User')
2628 user_group = relationship('UserGroup')
2639 user_group = relationship('UserGroup')
2629 permission = relationship('Permission')
2640 permission = relationship('Permission')
2630
2641
2631 @classmethod
2642 @classmethod
2632 def create(cls, user, user_group, permission):
2643 def create(cls, user, user_group, permission):
2633 n = cls()
2644 n = cls()
2634 n.user = user
2645 n.user = user
2635 n.user_group = user_group
2646 n.user_group = user_group
2636 n.permission = permission
2647 n.permission = permission
2637 Session().add(n)
2648 Session().add(n)
2638 return n
2649 return n
2639
2650
2640 def __unicode__(self):
2651 def __unicode__(self):
2641 return u'<%s => %s >' % (self.user, self.user_group)
2652 return u'<%s => %s >' % (self.user, self.user_group)
2642
2653
2643
2654
2644 class UserToPerm(Base, BaseModel):
2655 class UserToPerm(Base, BaseModel):
2645 __tablename__ = 'user_to_perm'
2656 __tablename__ = 'user_to_perm'
2646 __table_args__ = (
2657 __table_args__ = (
2647 UniqueConstraint('user_id', 'permission_id'),
2658 UniqueConstraint('user_id', 'permission_id'),
2648 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2659 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2649 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2660 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2650 )
2661 )
2651 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2662 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2652 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2663 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2653 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2664 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2654
2665
2655 user = relationship('User')
2666 user = relationship('User')
2656 permission = relationship('Permission', lazy='joined')
2667 permission = relationship('Permission', lazy='joined')
2657
2668
2658 def __unicode__(self):
2669 def __unicode__(self):
2659 return u'<%s => %s >' % (self.user, self.permission)
2670 return u'<%s => %s >' % (self.user, self.permission)
2660
2671
2661
2672
2662 class UserGroupRepoToPerm(Base, BaseModel):
2673 class UserGroupRepoToPerm(Base, BaseModel):
2663 __tablename__ = 'users_group_repo_to_perm'
2674 __tablename__ = 'users_group_repo_to_perm'
2664 __table_args__ = (
2675 __table_args__ = (
2665 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2676 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2666 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2667 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2668 )
2679 )
2669 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2680 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2681 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2671 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2682 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2672 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2683 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2673
2684
2674 users_group = relationship('UserGroup')
2685 users_group = relationship('UserGroup')
2675 permission = relationship('Permission')
2686 permission = relationship('Permission')
2676 repository = relationship('Repository')
2687 repository = relationship('Repository')
2677
2688
2678 @classmethod
2689 @classmethod
2679 def create(cls, users_group, repository, permission):
2690 def create(cls, users_group, repository, permission):
2680 n = cls()
2691 n = cls()
2681 n.users_group = users_group
2692 n.users_group = users_group
2682 n.repository = repository
2693 n.repository = repository
2683 n.permission = permission
2694 n.permission = permission
2684 Session().add(n)
2695 Session().add(n)
2685 return n
2696 return n
2686
2697
2687 def __unicode__(self):
2698 def __unicode__(self):
2688 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2699 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2689
2700
2690
2701
2691 class UserGroupUserGroupToPerm(Base, BaseModel):
2702 class UserGroupUserGroupToPerm(Base, BaseModel):
2692 __tablename__ = 'user_group_user_group_to_perm'
2703 __tablename__ = 'user_group_user_group_to_perm'
2693 __table_args__ = (
2704 __table_args__ = (
2694 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2705 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2695 CheckConstraint('target_user_group_id != user_group_id'),
2706 CheckConstraint('target_user_group_id != user_group_id'),
2696 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2697 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2698 )
2709 )
2699 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)
2710 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)
2700 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2711 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2712 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2713 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2703
2714
2704 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2715 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2705 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2716 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2706 permission = relationship('Permission')
2717 permission = relationship('Permission')
2707
2718
2708 @classmethod
2719 @classmethod
2709 def create(cls, target_user_group, user_group, permission):
2720 def create(cls, target_user_group, user_group, permission):
2710 n = cls()
2721 n = cls()
2711 n.target_user_group = target_user_group
2722 n.target_user_group = target_user_group
2712 n.user_group = user_group
2723 n.user_group = user_group
2713 n.permission = permission
2724 n.permission = permission
2714 Session().add(n)
2725 Session().add(n)
2715 return n
2726 return n
2716
2727
2717 def __unicode__(self):
2728 def __unicode__(self):
2718 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2729 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2719
2730
2720
2731
2721 class UserGroupToPerm(Base, BaseModel):
2732 class UserGroupToPerm(Base, BaseModel):
2722 __tablename__ = 'users_group_to_perm'
2733 __tablename__ = 'users_group_to_perm'
2723 __table_args__ = (
2734 __table_args__ = (
2724 UniqueConstraint('users_group_id', 'permission_id',),
2735 UniqueConstraint('users_group_id', 'permission_id',),
2725 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2726 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2727 )
2738 )
2728 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2739 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2729 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2740 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2730 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2741 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2731
2742
2732 users_group = relationship('UserGroup')
2743 users_group = relationship('UserGroup')
2733 permission = relationship('Permission')
2744 permission = relationship('Permission')
2734
2745
2735
2746
2736 class UserRepoGroupToPerm(Base, BaseModel):
2747 class UserRepoGroupToPerm(Base, BaseModel):
2737 __tablename__ = 'user_repo_group_to_perm'
2748 __tablename__ = 'user_repo_group_to_perm'
2738 __table_args__ = (
2749 __table_args__ = (
2739 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2750 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2740 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2751 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2741 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2752 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2742 )
2753 )
2743
2754
2744 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2755 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2745 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2756 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2746 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2757 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2747 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2758 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2748
2759
2749 user = relationship('User')
2760 user = relationship('User')
2750 group = relationship('RepoGroup')
2761 group = relationship('RepoGroup')
2751 permission = relationship('Permission')
2762 permission = relationship('Permission')
2752
2763
2753 @classmethod
2764 @classmethod
2754 def create(cls, user, repository_group, permission):
2765 def create(cls, user, repository_group, permission):
2755 n = cls()
2766 n = cls()
2756 n.user = user
2767 n.user = user
2757 n.group = repository_group
2768 n.group = repository_group
2758 n.permission = permission
2769 n.permission = permission
2759 Session().add(n)
2770 Session().add(n)
2760 return n
2771 return n
2761
2772
2762
2773
2763 class UserGroupRepoGroupToPerm(Base, BaseModel):
2774 class UserGroupRepoGroupToPerm(Base, BaseModel):
2764 __tablename__ = 'users_group_repo_group_to_perm'
2775 __tablename__ = 'users_group_repo_group_to_perm'
2765 __table_args__ = (
2776 __table_args__ = (
2766 UniqueConstraint('users_group_id', 'group_id'),
2777 UniqueConstraint('users_group_id', 'group_id'),
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2778 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2779 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2769 )
2780 )
2770
2781
2771 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)
2782 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)
2772 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2783 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2773 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2784 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2774 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2785 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2775
2786
2776 users_group = relationship('UserGroup')
2787 users_group = relationship('UserGroup')
2777 permission = relationship('Permission')
2788 permission = relationship('Permission')
2778 group = relationship('RepoGroup')
2789 group = relationship('RepoGroup')
2779
2790
2780 @classmethod
2791 @classmethod
2781 def create(cls, user_group, repository_group, permission):
2792 def create(cls, user_group, repository_group, permission):
2782 n = cls()
2793 n = cls()
2783 n.users_group = user_group
2794 n.users_group = user_group
2784 n.group = repository_group
2795 n.group = repository_group
2785 n.permission = permission
2796 n.permission = permission
2786 Session().add(n)
2797 Session().add(n)
2787 return n
2798 return n
2788
2799
2789 def __unicode__(self):
2800 def __unicode__(self):
2790 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2801 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2791
2802
2792
2803
2793 class Statistics(Base, BaseModel):
2804 class Statistics(Base, BaseModel):
2794 __tablename__ = 'statistics'
2805 __tablename__ = 'statistics'
2795 __table_args__ = (
2806 __table_args__ = (
2796 UniqueConstraint('repository_id'),
2807 UniqueConstraint('repository_id'),
2797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2808 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2809 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2799 )
2810 )
2800 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2811 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2801 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2812 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2802 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2813 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2803 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2814 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2804 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2815 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2805 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2816 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2806
2817
2807 repository = relationship('Repository', single_parent=True)
2818 repository = relationship('Repository', single_parent=True)
2808
2819
2809
2820
2810 class UserFollowing(Base, BaseModel):
2821 class UserFollowing(Base, BaseModel):
2811 __tablename__ = 'user_followings'
2822 __tablename__ = 'user_followings'
2812 __table_args__ = (
2823 __table_args__ = (
2813 UniqueConstraint('user_id', 'follows_repository_id'),
2824 UniqueConstraint('user_id', 'follows_repository_id'),
2814 UniqueConstraint('user_id', 'follows_user_id'),
2825 UniqueConstraint('user_id', 'follows_user_id'),
2815 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2826 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2816 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2827 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2817 )
2828 )
2818
2829
2819 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2830 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2820 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2831 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2821 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2832 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2822 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2833 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2823 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2834 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2824
2835
2825 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2836 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2826
2837
2827 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2838 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2828 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2839 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2829
2840
2830 @classmethod
2841 @classmethod
2831 def get_repo_followers(cls, repo_id):
2842 def get_repo_followers(cls, repo_id):
2832 return cls.query().filter(cls.follows_repo_id == repo_id)
2843 return cls.query().filter(cls.follows_repo_id == repo_id)
2833
2844
2834
2845
2835 class CacheKey(Base, BaseModel):
2846 class CacheKey(Base, BaseModel):
2836 __tablename__ = 'cache_invalidation'
2847 __tablename__ = 'cache_invalidation'
2837 __table_args__ = (
2848 __table_args__ = (
2838 UniqueConstraint('cache_key'),
2849 UniqueConstraint('cache_key'),
2839 Index('key_idx', 'cache_key'),
2850 Index('key_idx', 'cache_key'),
2840 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2851 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2841 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2852 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2842 )
2853 )
2843 CACHE_TYPE_ATOM = 'ATOM'
2854 CACHE_TYPE_ATOM = 'ATOM'
2844 CACHE_TYPE_RSS = 'RSS'
2855 CACHE_TYPE_RSS = 'RSS'
2845 CACHE_TYPE_README = 'README'
2856 CACHE_TYPE_README = 'README'
2846
2857
2847 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2858 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2848 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2859 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2849 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2860 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2850 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2861 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2851
2862
2852 def __init__(self, cache_key, cache_args=''):
2863 def __init__(self, cache_key, cache_args=''):
2853 self.cache_key = cache_key
2864 self.cache_key = cache_key
2854 self.cache_args = cache_args
2865 self.cache_args = cache_args
2855 self.cache_active = False
2866 self.cache_active = False
2856
2867
2857 def __unicode__(self):
2868 def __unicode__(self):
2858 return u"<%s('%s:%s[%s]')>" % (
2869 return u"<%s('%s:%s[%s]')>" % (
2859 self.__class__.__name__,
2870 self.__class__.__name__,
2860 self.cache_id, self.cache_key, self.cache_active)
2871 self.cache_id, self.cache_key, self.cache_active)
2861
2872
2862 def _cache_key_partition(self):
2873 def _cache_key_partition(self):
2863 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2874 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2864 return prefix, repo_name, suffix
2875 return prefix, repo_name, suffix
2865
2876
2866 def get_prefix(self):
2877 def get_prefix(self):
2867 """
2878 """
2868 Try to extract prefix from existing cache key. The key could consist
2879 Try to extract prefix from existing cache key. The key could consist
2869 of prefix, repo_name, suffix
2880 of prefix, repo_name, suffix
2870 """
2881 """
2871 # this returns prefix, repo_name, suffix
2882 # this returns prefix, repo_name, suffix
2872 return self._cache_key_partition()[0]
2883 return self._cache_key_partition()[0]
2873
2884
2874 def get_suffix(self):
2885 def get_suffix(self):
2875 """
2886 """
2876 get suffix that might have been used in _get_cache_key to
2887 get suffix that might have been used in _get_cache_key to
2877 generate self.cache_key. Only used for informational purposes
2888 generate self.cache_key. Only used for informational purposes
2878 in repo_edit.mako.
2889 in repo_edit.mako.
2879 """
2890 """
2880 # prefix, repo_name, suffix
2891 # prefix, repo_name, suffix
2881 return self._cache_key_partition()[2]
2892 return self._cache_key_partition()[2]
2882
2893
2883 @classmethod
2894 @classmethod
2884 def delete_all_cache(cls):
2895 def delete_all_cache(cls):
2885 """
2896 """
2886 Delete all cache keys from database.
2897 Delete all cache keys from database.
2887 Should only be run when all instances are down and all entries
2898 Should only be run when all instances are down and all entries
2888 thus stale.
2899 thus stale.
2889 """
2900 """
2890 cls.query().delete()
2901 cls.query().delete()
2891 Session().commit()
2902 Session().commit()
2892
2903
2893 @classmethod
2904 @classmethod
2894 def get_cache_key(cls, repo_name, cache_type):
2905 def get_cache_key(cls, repo_name, cache_type):
2895 """
2906 """
2896
2907
2897 Generate a cache key for this process of RhodeCode instance.
2908 Generate a cache key for this process of RhodeCode instance.
2898 Prefix most likely will be process id or maybe explicitly set
2909 Prefix most likely will be process id or maybe explicitly set
2899 instance_id from .ini file.
2910 instance_id from .ini file.
2900 """
2911 """
2901 import rhodecode
2912 import rhodecode
2902 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2913 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2903
2914
2904 repo_as_unicode = safe_unicode(repo_name)
2915 repo_as_unicode = safe_unicode(repo_name)
2905 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2916 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2906 if cache_type else repo_as_unicode
2917 if cache_type else repo_as_unicode
2907
2918
2908 return u'{}{}'.format(prefix, key)
2919 return u'{}{}'.format(prefix, key)
2909
2920
2910 @classmethod
2921 @classmethod
2911 def set_invalidate(cls, repo_name, delete=False):
2922 def set_invalidate(cls, repo_name, delete=False):
2912 """
2923 """
2913 Mark all caches of a repo as invalid in the database.
2924 Mark all caches of a repo as invalid in the database.
2914 """
2925 """
2915
2926
2916 try:
2927 try:
2917 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2928 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2918 if delete:
2929 if delete:
2919 log.debug('cache objects deleted for repo %s',
2930 log.debug('cache objects deleted for repo %s',
2920 safe_str(repo_name))
2931 safe_str(repo_name))
2921 qry.delete()
2932 qry.delete()
2922 else:
2933 else:
2923 log.debug('cache objects marked as invalid for repo %s',
2934 log.debug('cache objects marked as invalid for repo %s',
2924 safe_str(repo_name))
2935 safe_str(repo_name))
2925 qry.update({"cache_active": False})
2936 qry.update({"cache_active": False})
2926
2937
2927 Session().commit()
2938 Session().commit()
2928 except Exception:
2939 except Exception:
2929 log.exception(
2940 log.exception(
2930 'Cache key invalidation failed for repository %s',
2941 'Cache key invalidation failed for repository %s',
2931 safe_str(repo_name))
2942 safe_str(repo_name))
2932 Session().rollback()
2943 Session().rollback()
2933
2944
2934 @classmethod
2945 @classmethod
2935 def get_active_cache(cls, cache_key):
2946 def get_active_cache(cls, cache_key):
2936 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2947 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2937 if inv_obj:
2948 if inv_obj:
2938 return inv_obj
2949 return inv_obj
2939 return None
2950 return None
2940
2951
2941 @classmethod
2952 @classmethod
2942 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2953 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2943 thread_scoped=False):
2954 thread_scoped=False):
2944 """
2955 """
2945 @cache_region('long_term')
2956 @cache_region('long_term')
2946 def _heavy_calculation(cache_key):
2957 def _heavy_calculation(cache_key):
2947 return 'result'
2958 return 'result'
2948
2959
2949 cache_context = CacheKey.repo_context_cache(
2960 cache_context = CacheKey.repo_context_cache(
2950 _heavy_calculation, repo_name, cache_type)
2961 _heavy_calculation, repo_name, cache_type)
2951
2962
2952 with cache_context as context:
2963 with cache_context as context:
2953 context.invalidate()
2964 context.invalidate()
2954 computed = context.compute()
2965 computed = context.compute()
2955
2966
2956 assert computed == 'result'
2967 assert computed == 'result'
2957 """
2968 """
2958 from rhodecode.lib import caches
2969 from rhodecode.lib import caches
2959 return caches.InvalidationContext(
2970 return caches.InvalidationContext(
2960 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2971 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2961
2972
2962
2973
2963 class ChangesetComment(Base, BaseModel):
2974 class ChangesetComment(Base, BaseModel):
2964 __tablename__ = 'changeset_comments'
2975 __tablename__ = 'changeset_comments'
2965 __table_args__ = (
2976 __table_args__ = (
2966 Index('cc_revision_idx', 'revision'),
2977 Index('cc_revision_idx', 'revision'),
2967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2978 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2979 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2969 )
2980 )
2970
2981
2971 COMMENT_OUTDATED = u'comment_outdated'
2982 COMMENT_OUTDATED = u'comment_outdated'
2972 COMMENT_TYPE_NOTE = u'note'
2983 COMMENT_TYPE_NOTE = u'note'
2973 COMMENT_TYPE_TODO = u'todo'
2984 COMMENT_TYPE_TODO = u'todo'
2974 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2985 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2975
2986
2976 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2987 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2977 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2988 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2978 revision = Column('revision', String(40), nullable=True)
2989 revision = Column('revision', String(40), nullable=True)
2979 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2990 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2980 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2991 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2981 line_no = Column('line_no', Unicode(10), nullable=True)
2992 line_no = Column('line_no', Unicode(10), nullable=True)
2982 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2993 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2983 f_path = Column('f_path', Unicode(1000), nullable=True)
2994 f_path = Column('f_path', Unicode(1000), nullable=True)
2984 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2995 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2985 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2996 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2986 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2997 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2987 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2998 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2988 renderer = Column('renderer', Unicode(64), nullable=True)
2999 renderer = Column('renderer', Unicode(64), nullable=True)
2989 display_state = Column('display_state', Unicode(128), nullable=True)
3000 display_state = Column('display_state', Unicode(128), nullable=True)
2990
3001
2991 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3002 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2992 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3003 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2993 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3004 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2994 author = relationship('User', lazy='joined')
3005 author = relationship('User', lazy='joined')
2995 repo = relationship('Repository')
3006 repo = relationship('Repository')
2996 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3007 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2997 pull_request = relationship('PullRequest', lazy='joined')
3008 pull_request = relationship('PullRequest', lazy='joined')
2998 pull_request_version = relationship('PullRequestVersion')
3009 pull_request_version = relationship('PullRequestVersion')
2999
3010
3000 @classmethod
3011 @classmethod
3001 def get_users(cls, revision=None, pull_request_id=None):
3012 def get_users(cls, revision=None, pull_request_id=None):
3002 """
3013 """
3003 Returns user associated with this ChangesetComment. ie those
3014 Returns user associated with this ChangesetComment. ie those
3004 who actually commented
3015 who actually commented
3005
3016
3006 :param cls:
3017 :param cls:
3007 :param revision:
3018 :param revision:
3008 """
3019 """
3009 q = Session().query(User)\
3020 q = Session().query(User)\
3010 .join(ChangesetComment.author)
3021 .join(ChangesetComment.author)
3011 if revision:
3022 if revision:
3012 q = q.filter(cls.revision == revision)
3023 q = q.filter(cls.revision == revision)
3013 elif pull_request_id:
3024 elif pull_request_id:
3014 q = q.filter(cls.pull_request_id == pull_request_id)
3025 q = q.filter(cls.pull_request_id == pull_request_id)
3015 return q.all()
3026 return q.all()
3016
3027
3017 @classmethod
3028 @classmethod
3018 def get_index_from_version(cls, pr_version, versions):
3029 def get_index_from_version(cls, pr_version, versions):
3019 num_versions = [x.pull_request_version_id for x in versions]
3030 num_versions = [x.pull_request_version_id for x in versions]
3020 try:
3031 try:
3021 return num_versions.index(pr_version) +1
3032 return num_versions.index(pr_version) +1
3022 except (IndexError, ValueError):
3033 except (IndexError, ValueError):
3023 return
3034 return
3024
3035
3025 @property
3036 @property
3026 def outdated(self):
3037 def outdated(self):
3027 return self.display_state == self.COMMENT_OUTDATED
3038 return self.display_state == self.COMMENT_OUTDATED
3028
3039
3029 def outdated_at_version(self, version):
3040 def outdated_at_version(self, version):
3030 """
3041 """
3031 Checks if comment is outdated for given pull request version
3042 Checks if comment is outdated for given pull request version
3032 """
3043 """
3033 return self.outdated and self.pull_request_version_id != version
3044 return self.outdated and self.pull_request_version_id != version
3034
3045
3035 def older_than_version(self, version):
3046 def older_than_version(self, version):
3036 """
3047 """
3037 Checks if comment is made from previous version than given
3048 Checks if comment is made from previous version than given
3038 """
3049 """
3039 if version is None:
3050 if version is None:
3040 return self.pull_request_version_id is not None
3051 return self.pull_request_version_id is not None
3041
3052
3042 return self.pull_request_version_id < version
3053 return self.pull_request_version_id < version
3043
3054
3044 @property
3055 @property
3045 def resolved(self):
3056 def resolved(self):
3046 return self.resolved_by[0] if self.resolved_by else None
3057 return self.resolved_by[0] if self.resolved_by else None
3047
3058
3048 @property
3059 @property
3049 def is_todo(self):
3060 def is_todo(self):
3050 return self.comment_type == self.COMMENT_TYPE_TODO
3061 return self.comment_type == self.COMMENT_TYPE_TODO
3051
3062
3052 def get_index_version(self, versions):
3063 def get_index_version(self, versions):
3053 return self.get_index_from_version(
3064 return self.get_index_from_version(
3054 self.pull_request_version_id, versions)
3065 self.pull_request_version_id, versions)
3055
3066
3056 def render(self, mentions=False):
3067 def render(self, mentions=False):
3057 from rhodecode.lib import helpers as h
3068 from rhodecode.lib import helpers as h
3058 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3069 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3059
3070
3060 def __repr__(self):
3071 def __repr__(self):
3061 if self.comment_id:
3072 if self.comment_id:
3062 return '<DB:Comment #%s>' % self.comment_id
3073 return '<DB:Comment #%s>' % self.comment_id
3063 else:
3074 else:
3064 return '<DB:Comment at %#x>' % id(self)
3075 return '<DB:Comment at %#x>' % id(self)
3065
3076
3066
3077
3067 class ChangesetStatus(Base, BaseModel):
3078 class ChangesetStatus(Base, BaseModel):
3068 __tablename__ = 'changeset_statuses'
3079 __tablename__ = 'changeset_statuses'
3069 __table_args__ = (
3080 __table_args__ = (
3070 Index('cs_revision_idx', 'revision'),
3081 Index('cs_revision_idx', 'revision'),
3071 Index('cs_version_idx', 'version'),
3082 Index('cs_version_idx', 'version'),
3072 UniqueConstraint('repo_id', 'revision', 'version'),
3083 UniqueConstraint('repo_id', 'revision', 'version'),
3073 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3074 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3075 )
3086 )
3076 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3087 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3077 STATUS_APPROVED = 'approved'
3088 STATUS_APPROVED = 'approved'
3078 STATUS_REJECTED = 'rejected'
3089 STATUS_REJECTED = 'rejected'
3079 STATUS_UNDER_REVIEW = 'under_review'
3090 STATUS_UNDER_REVIEW = 'under_review'
3080
3091
3081 STATUSES = [
3092 STATUSES = [
3082 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3093 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3083 (STATUS_APPROVED, _("Approved")),
3094 (STATUS_APPROVED, _("Approved")),
3084 (STATUS_REJECTED, _("Rejected")),
3095 (STATUS_REJECTED, _("Rejected")),
3085 (STATUS_UNDER_REVIEW, _("Under Review")),
3096 (STATUS_UNDER_REVIEW, _("Under Review")),
3086 ]
3097 ]
3087
3098
3088 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3099 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3089 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3100 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3090 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3101 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3091 revision = Column('revision', String(40), nullable=False)
3102 revision = Column('revision', String(40), nullable=False)
3092 status = Column('status', String(128), nullable=False, default=DEFAULT)
3103 status = Column('status', String(128), nullable=False, default=DEFAULT)
3093 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3104 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3094 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3105 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3095 version = Column('version', Integer(), nullable=False, default=0)
3106 version = Column('version', Integer(), nullable=False, default=0)
3096 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3107 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3097
3108
3098 author = relationship('User', lazy='joined')
3109 author = relationship('User', lazy='joined')
3099 repo = relationship('Repository')
3110 repo = relationship('Repository')
3100 comment = relationship('ChangesetComment', lazy='joined')
3111 comment = relationship('ChangesetComment', lazy='joined')
3101 pull_request = relationship('PullRequest', lazy='joined')
3112 pull_request = relationship('PullRequest', lazy='joined')
3102
3113
3103 def __unicode__(self):
3114 def __unicode__(self):
3104 return u"<%s('%s[v%s]:%s')>" % (
3115 return u"<%s('%s[v%s]:%s')>" % (
3105 self.__class__.__name__,
3116 self.__class__.__name__,
3106 self.status, self.version, self.author
3117 self.status, self.version, self.author
3107 )
3118 )
3108
3119
3109 @classmethod
3120 @classmethod
3110 def get_status_lbl(cls, value):
3121 def get_status_lbl(cls, value):
3111 return dict(cls.STATUSES).get(value)
3122 return dict(cls.STATUSES).get(value)
3112
3123
3113 @property
3124 @property
3114 def status_lbl(self):
3125 def status_lbl(self):
3115 return ChangesetStatus.get_status_lbl(self.status)
3126 return ChangesetStatus.get_status_lbl(self.status)
3116
3127
3117
3128
3118 class _PullRequestBase(BaseModel):
3129 class _PullRequestBase(BaseModel):
3119 """
3130 """
3120 Common attributes of pull request and version entries.
3131 Common attributes of pull request and version entries.
3121 """
3132 """
3122
3133
3123 # .status values
3134 # .status values
3124 STATUS_NEW = u'new'
3135 STATUS_NEW = u'new'
3125 STATUS_OPEN = u'open'
3136 STATUS_OPEN = u'open'
3126 STATUS_CLOSED = u'closed'
3137 STATUS_CLOSED = u'closed'
3127
3138
3128 title = Column('title', Unicode(255), nullable=True)
3139 title = Column('title', Unicode(255), nullable=True)
3129 description = Column(
3140 description = Column(
3130 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3141 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3131 nullable=True)
3142 nullable=True)
3132 # new/open/closed status of pull request (not approve/reject/etc)
3143 # new/open/closed status of pull request (not approve/reject/etc)
3133 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3144 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3134 created_on = Column(
3145 created_on = Column(
3135 'created_on', DateTime(timezone=False), nullable=False,
3146 'created_on', DateTime(timezone=False), nullable=False,
3136 default=datetime.datetime.now)
3147 default=datetime.datetime.now)
3137 updated_on = Column(
3148 updated_on = Column(
3138 'updated_on', DateTime(timezone=False), nullable=False,
3149 'updated_on', DateTime(timezone=False), nullable=False,
3139 default=datetime.datetime.now)
3150 default=datetime.datetime.now)
3140
3151
3141 @declared_attr
3152 @declared_attr
3142 def user_id(cls):
3153 def user_id(cls):
3143 return Column(
3154 return Column(
3144 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3155 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3145 unique=None)
3156 unique=None)
3146
3157
3147 # 500 revisions max
3158 # 500 revisions max
3148 _revisions = Column(
3159 _revisions = Column(
3149 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3160 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3150
3161
3151 @declared_attr
3162 @declared_attr
3152 def source_repo_id(cls):
3163 def source_repo_id(cls):
3153 # TODO: dan: rename column to source_repo_id
3164 # TODO: dan: rename column to source_repo_id
3154 return Column(
3165 return Column(
3155 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3166 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3156 nullable=False)
3167 nullable=False)
3157
3168
3158 source_ref = Column('org_ref', Unicode(255), nullable=False)
3169 source_ref = Column('org_ref', Unicode(255), nullable=False)
3159
3170
3160 @declared_attr
3171 @declared_attr
3161 def target_repo_id(cls):
3172 def target_repo_id(cls):
3162 # TODO: dan: rename column to target_repo_id
3173 # TODO: dan: rename column to target_repo_id
3163 return Column(
3174 return Column(
3164 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3175 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3165 nullable=False)
3176 nullable=False)
3166
3177
3167 target_ref = Column('other_ref', Unicode(255), nullable=False)
3178 target_ref = Column('other_ref', Unicode(255), nullable=False)
3168 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3179 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3169
3180
3170 # TODO: dan: rename column to last_merge_source_rev
3181 # TODO: dan: rename column to last_merge_source_rev
3171 _last_merge_source_rev = Column(
3182 _last_merge_source_rev = Column(
3172 'last_merge_org_rev', String(40), nullable=True)
3183 'last_merge_org_rev', String(40), nullable=True)
3173 # TODO: dan: rename column to last_merge_target_rev
3184 # TODO: dan: rename column to last_merge_target_rev
3174 _last_merge_target_rev = Column(
3185 _last_merge_target_rev = Column(
3175 'last_merge_other_rev', String(40), nullable=True)
3186 'last_merge_other_rev', String(40), nullable=True)
3176 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3187 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3177 merge_rev = Column('merge_rev', String(40), nullable=True)
3188 merge_rev = Column('merge_rev', String(40), nullable=True)
3178
3189
3179 @hybrid_property
3190 @hybrid_property
3180 def revisions(self):
3191 def revisions(self):
3181 return self._revisions.split(':') if self._revisions else []
3192 return self._revisions.split(':') if self._revisions else []
3182
3193
3183 @revisions.setter
3194 @revisions.setter
3184 def revisions(self, val):
3195 def revisions(self, val):
3185 self._revisions = ':'.join(val)
3196 self._revisions = ':'.join(val)
3186
3197
3187 @declared_attr
3198 @declared_attr
3188 def author(cls):
3199 def author(cls):
3189 return relationship('User', lazy='joined')
3200 return relationship('User', lazy='joined')
3190
3201
3191 @declared_attr
3202 @declared_attr
3192 def source_repo(cls):
3203 def source_repo(cls):
3193 return relationship(
3204 return relationship(
3194 'Repository',
3205 'Repository',
3195 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3206 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3196
3207
3197 @property
3208 @property
3198 def source_ref_parts(self):
3209 def source_ref_parts(self):
3199 return self.unicode_to_reference(self.source_ref)
3210 return self.unicode_to_reference(self.source_ref)
3200
3211
3201 @declared_attr
3212 @declared_attr
3202 def target_repo(cls):
3213 def target_repo(cls):
3203 return relationship(
3214 return relationship(
3204 'Repository',
3215 'Repository',
3205 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3216 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3206
3217
3207 @property
3218 @property
3208 def target_ref_parts(self):
3219 def target_ref_parts(self):
3209 return self.unicode_to_reference(self.target_ref)
3220 return self.unicode_to_reference(self.target_ref)
3210
3221
3211 @property
3222 @property
3212 def shadow_merge_ref(self):
3223 def shadow_merge_ref(self):
3213 return self.unicode_to_reference(self._shadow_merge_ref)
3224 return self.unicode_to_reference(self._shadow_merge_ref)
3214
3225
3215 @shadow_merge_ref.setter
3226 @shadow_merge_ref.setter
3216 def shadow_merge_ref(self, ref):
3227 def shadow_merge_ref(self, ref):
3217 self._shadow_merge_ref = self.reference_to_unicode(ref)
3228 self._shadow_merge_ref = self.reference_to_unicode(ref)
3218
3229
3219 def unicode_to_reference(self, raw):
3230 def unicode_to_reference(self, raw):
3220 """
3231 """
3221 Convert a unicode (or string) to a reference object.
3232 Convert a unicode (or string) to a reference object.
3222 If unicode evaluates to False it returns None.
3233 If unicode evaluates to False it returns None.
3223 """
3234 """
3224 if raw:
3235 if raw:
3225 refs = raw.split(':')
3236 refs = raw.split(':')
3226 return Reference(*refs)
3237 return Reference(*refs)
3227 else:
3238 else:
3228 return None
3239 return None
3229
3240
3230 def reference_to_unicode(self, ref):
3241 def reference_to_unicode(self, ref):
3231 """
3242 """
3232 Convert a reference object to unicode.
3243 Convert a reference object to unicode.
3233 If reference is None it returns None.
3244 If reference is None it returns None.
3234 """
3245 """
3235 if ref:
3246 if ref:
3236 return u':'.join(ref)
3247 return u':'.join(ref)
3237 else:
3248 else:
3238 return None
3249 return None
3239
3250
3240 def get_api_data(self):
3251 def get_api_data(self):
3241 from rhodecode.model.pull_request import PullRequestModel
3252 from rhodecode.model.pull_request import PullRequestModel
3242 pull_request = self
3253 pull_request = self
3243 merge_status = PullRequestModel().merge_status(pull_request)
3254 merge_status = PullRequestModel().merge_status(pull_request)
3244
3255
3245 pull_request_url = url(
3256 pull_request_url = url(
3246 'pullrequest_show', repo_name=self.target_repo.repo_name,
3257 'pullrequest_show', repo_name=self.target_repo.repo_name,
3247 pull_request_id=self.pull_request_id, qualified=True)
3258 pull_request_id=self.pull_request_id, qualified=True)
3248
3259
3249 merge_data = {
3260 merge_data = {
3250 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3261 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3251 'reference': (
3262 'reference': (
3252 pull_request.shadow_merge_ref._asdict()
3263 pull_request.shadow_merge_ref._asdict()
3253 if pull_request.shadow_merge_ref else None),
3264 if pull_request.shadow_merge_ref else None),
3254 }
3265 }
3255
3266
3256 data = {
3267 data = {
3257 'pull_request_id': pull_request.pull_request_id,
3268 'pull_request_id': pull_request.pull_request_id,
3258 'url': pull_request_url,
3269 'url': pull_request_url,
3259 'title': pull_request.title,
3270 'title': pull_request.title,
3260 'description': pull_request.description,
3271 'description': pull_request.description,
3261 'status': pull_request.status,
3272 'status': pull_request.status,
3262 'created_on': pull_request.created_on,
3273 'created_on': pull_request.created_on,
3263 'updated_on': pull_request.updated_on,
3274 'updated_on': pull_request.updated_on,
3264 'commit_ids': pull_request.revisions,
3275 'commit_ids': pull_request.revisions,
3265 'review_status': pull_request.calculated_review_status(),
3276 'review_status': pull_request.calculated_review_status(),
3266 'mergeable': {
3277 'mergeable': {
3267 'status': merge_status[0],
3278 'status': merge_status[0],
3268 'message': unicode(merge_status[1]),
3279 'message': unicode(merge_status[1]),
3269 },
3280 },
3270 'source': {
3281 'source': {
3271 'clone_url': pull_request.source_repo.clone_url(),
3282 'clone_url': pull_request.source_repo.clone_url(),
3272 'repository': pull_request.source_repo.repo_name,
3283 'repository': pull_request.source_repo.repo_name,
3273 'reference': {
3284 'reference': {
3274 'name': pull_request.source_ref_parts.name,
3285 'name': pull_request.source_ref_parts.name,
3275 'type': pull_request.source_ref_parts.type,
3286 'type': pull_request.source_ref_parts.type,
3276 'commit_id': pull_request.source_ref_parts.commit_id,
3287 'commit_id': pull_request.source_ref_parts.commit_id,
3277 },
3288 },
3278 },
3289 },
3279 'target': {
3290 'target': {
3280 'clone_url': pull_request.target_repo.clone_url(),
3291 'clone_url': pull_request.target_repo.clone_url(),
3281 'repository': pull_request.target_repo.repo_name,
3292 'repository': pull_request.target_repo.repo_name,
3282 'reference': {
3293 'reference': {
3283 'name': pull_request.target_ref_parts.name,
3294 'name': pull_request.target_ref_parts.name,
3284 'type': pull_request.target_ref_parts.type,
3295 'type': pull_request.target_ref_parts.type,
3285 'commit_id': pull_request.target_ref_parts.commit_id,
3296 'commit_id': pull_request.target_ref_parts.commit_id,
3286 },
3297 },
3287 },
3298 },
3288 'merge': merge_data,
3299 'merge': merge_data,
3289 'author': pull_request.author.get_api_data(include_secrets=False,
3300 'author': pull_request.author.get_api_data(include_secrets=False,
3290 details='basic'),
3301 details='basic'),
3291 'reviewers': [
3302 'reviewers': [
3292 {
3303 {
3293 'user': reviewer.get_api_data(include_secrets=False,
3304 'user': reviewer.get_api_data(include_secrets=False,
3294 details='basic'),
3305 details='basic'),
3295 'reasons': reasons,
3306 'reasons': reasons,
3296 'review_status': st[0][1].status if st else 'not_reviewed',
3307 'review_status': st[0][1].status if st else 'not_reviewed',
3297 }
3308 }
3298 for reviewer, reasons, st in pull_request.reviewers_statuses()
3309 for reviewer, reasons, st in pull_request.reviewers_statuses()
3299 ]
3310 ]
3300 }
3311 }
3301
3312
3302 return data
3313 return data
3303
3314
3304
3315
3305 class PullRequest(Base, _PullRequestBase):
3316 class PullRequest(Base, _PullRequestBase):
3306 __tablename__ = 'pull_requests'
3317 __tablename__ = 'pull_requests'
3307 __table_args__ = (
3318 __table_args__ = (
3308 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3319 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3309 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3320 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3310 )
3321 )
3311
3322
3312 pull_request_id = Column(
3323 pull_request_id = Column(
3313 'pull_request_id', Integer(), nullable=False, primary_key=True)
3324 'pull_request_id', Integer(), nullable=False, primary_key=True)
3314
3325
3315 def __repr__(self):
3326 def __repr__(self):
3316 if self.pull_request_id:
3327 if self.pull_request_id:
3317 return '<DB:PullRequest #%s>' % self.pull_request_id
3328 return '<DB:PullRequest #%s>' % self.pull_request_id
3318 else:
3329 else:
3319 return '<DB:PullRequest at %#x>' % id(self)
3330 return '<DB:PullRequest at %#x>' % id(self)
3320
3331
3321 reviewers = relationship('PullRequestReviewers',
3332 reviewers = relationship('PullRequestReviewers',
3322 cascade="all, delete, delete-orphan")
3333 cascade="all, delete, delete-orphan")
3323 statuses = relationship('ChangesetStatus')
3334 statuses = relationship('ChangesetStatus')
3324 comments = relationship('ChangesetComment',
3335 comments = relationship('ChangesetComment',
3325 cascade="all, delete, delete-orphan")
3336 cascade="all, delete, delete-orphan")
3326 versions = relationship('PullRequestVersion',
3337 versions = relationship('PullRequestVersion',
3327 cascade="all, delete, delete-orphan",
3338 cascade="all, delete, delete-orphan",
3328 lazy='dynamic')
3339 lazy='dynamic')
3329
3340
3330 @classmethod
3341 @classmethod
3331 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3342 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3332 internal_methods=None):
3343 internal_methods=None):
3333
3344
3334 class PullRequestDisplay(object):
3345 class PullRequestDisplay(object):
3335 """
3346 """
3336 Special object wrapper for showing PullRequest data via Versions
3347 Special object wrapper for showing PullRequest data via Versions
3337 It mimics PR object as close as possible. This is read only object
3348 It mimics PR object as close as possible. This is read only object
3338 just for display
3349 just for display
3339 """
3350 """
3340
3351
3341 def __init__(self, attrs, internal=None):
3352 def __init__(self, attrs, internal=None):
3342 self.attrs = attrs
3353 self.attrs = attrs
3343 # internal have priority over the given ones via attrs
3354 # internal have priority over the given ones via attrs
3344 self.internal = internal or ['versions']
3355 self.internal = internal or ['versions']
3345
3356
3346 def __getattr__(self, item):
3357 def __getattr__(self, item):
3347 if item in self.internal:
3358 if item in self.internal:
3348 return getattr(self, item)
3359 return getattr(self, item)
3349 try:
3360 try:
3350 return self.attrs[item]
3361 return self.attrs[item]
3351 except KeyError:
3362 except KeyError:
3352 raise AttributeError(
3363 raise AttributeError(
3353 '%s object has no attribute %s' % (self, item))
3364 '%s object has no attribute %s' % (self, item))
3354
3365
3355 def __repr__(self):
3366 def __repr__(self):
3356 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3367 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3357
3368
3358 def versions(self):
3369 def versions(self):
3359 return pull_request_obj.versions.order_by(
3370 return pull_request_obj.versions.order_by(
3360 PullRequestVersion.pull_request_version_id).all()
3371 PullRequestVersion.pull_request_version_id).all()
3361
3372
3362 def is_closed(self):
3373 def is_closed(self):
3363 return pull_request_obj.is_closed()
3374 return pull_request_obj.is_closed()
3364
3375
3365 @property
3376 @property
3366 def pull_request_version_id(self):
3377 def pull_request_version_id(self):
3367 return getattr(pull_request_obj, 'pull_request_version_id', None)
3378 return getattr(pull_request_obj, 'pull_request_version_id', None)
3368
3379
3369 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3380 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3370
3381
3371 attrs.author = StrictAttributeDict(
3382 attrs.author = StrictAttributeDict(
3372 pull_request_obj.author.get_api_data())
3383 pull_request_obj.author.get_api_data())
3373 if pull_request_obj.target_repo:
3384 if pull_request_obj.target_repo:
3374 attrs.target_repo = StrictAttributeDict(
3385 attrs.target_repo = StrictAttributeDict(
3375 pull_request_obj.target_repo.get_api_data())
3386 pull_request_obj.target_repo.get_api_data())
3376 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3387 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3377
3388
3378 if pull_request_obj.source_repo:
3389 if pull_request_obj.source_repo:
3379 attrs.source_repo = StrictAttributeDict(
3390 attrs.source_repo = StrictAttributeDict(
3380 pull_request_obj.source_repo.get_api_data())
3391 pull_request_obj.source_repo.get_api_data())
3381 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3392 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3382
3393
3383 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3394 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3384 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3395 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3385 attrs.revisions = pull_request_obj.revisions
3396 attrs.revisions = pull_request_obj.revisions
3386
3397
3387 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3398 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3388
3399
3389 return PullRequestDisplay(attrs, internal=internal_methods)
3400 return PullRequestDisplay(attrs, internal=internal_methods)
3390
3401
3391 def is_closed(self):
3402 def is_closed(self):
3392 return self.status == self.STATUS_CLOSED
3403 return self.status == self.STATUS_CLOSED
3393
3404
3394 def __json__(self):
3405 def __json__(self):
3395 return {
3406 return {
3396 'revisions': self.revisions,
3407 'revisions': self.revisions,
3397 }
3408 }
3398
3409
3399 def calculated_review_status(self):
3410 def calculated_review_status(self):
3400 from rhodecode.model.changeset_status import ChangesetStatusModel
3411 from rhodecode.model.changeset_status import ChangesetStatusModel
3401 return ChangesetStatusModel().calculated_review_status(self)
3412 return ChangesetStatusModel().calculated_review_status(self)
3402
3413
3403 def reviewers_statuses(self):
3414 def reviewers_statuses(self):
3404 from rhodecode.model.changeset_status import ChangesetStatusModel
3415 from rhodecode.model.changeset_status import ChangesetStatusModel
3405 return ChangesetStatusModel().reviewers_statuses(self)
3416 return ChangesetStatusModel().reviewers_statuses(self)
3406
3417
3407 @property
3418 @property
3408 def workspace_id(self):
3419 def workspace_id(self):
3409 from rhodecode.model.pull_request import PullRequestModel
3420 from rhodecode.model.pull_request import PullRequestModel
3410 return PullRequestModel()._workspace_id(self)
3421 return PullRequestModel()._workspace_id(self)
3411
3422
3412 def get_shadow_repo(self):
3423 def get_shadow_repo(self):
3413 workspace_id = self.workspace_id
3424 workspace_id = self.workspace_id
3414 vcs_obj = self.target_repo.scm_instance()
3425 vcs_obj = self.target_repo.scm_instance()
3415 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3426 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3416 workspace_id)
3427 workspace_id)
3417 return vcs_obj._get_shadow_instance(shadow_repository_path)
3428 return vcs_obj._get_shadow_instance(shadow_repository_path)
3418
3429
3419
3430
3420 class PullRequestVersion(Base, _PullRequestBase):
3431 class PullRequestVersion(Base, _PullRequestBase):
3421 __tablename__ = 'pull_request_versions'
3432 __tablename__ = 'pull_request_versions'
3422 __table_args__ = (
3433 __table_args__ = (
3423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3434 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3435 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3425 )
3436 )
3426
3437
3427 pull_request_version_id = Column(
3438 pull_request_version_id = Column(
3428 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3439 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3429 pull_request_id = Column(
3440 pull_request_id = Column(
3430 'pull_request_id', Integer(),
3441 'pull_request_id', Integer(),
3431 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3442 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3432 pull_request = relationship('PullRequest')
3443 pull_request = relationship('PullRequest')
3433
3444
3434 def __repr__(self):
3445 def __repr__(self):
3435 if self.pull_request_version_id:
3446 if self.pull_request_version_id:
3436 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3447 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3437 else:
3448 else:
3438 return '<DB:PullRequestVersion at %#x>' % id(self)
3449 return '<DB:PullRequestVersion at %#x>' % id(self)
3439
3450
3440 @property
3451 @property
3441 def reviewers(self):
3452 def reviewers(self):
3442 return self.pull_request.reviewers
3453 return self.pull_request.reviewers
3443
3454
3444 @property
3455 @property
3445 def versions(self):
3456 def versions(self):
3446 return self.pull_request.versions
3457 return self.pull_request.versions
3447
3458
3448 def is_closed(self):
3459 def is_closed(self):
3449 # calculate from original
3460 # calculate from original
3450 return self.pull_request.status == self.STATUS_CLOSED
3461 return self.pull_request.status == self.STATUS_CLOSED
3451
3462
3452 def calculated_review_status(self):
3463 def calculated_review_status(self):
3453 return self.pull_request.calculated_review_status()
3464 return self.pull_request.calculated_review_status()
3454
3465
3455 def reviewers_statuses(self):
3466 def reviewers_statuses(self):
3456 return self.pull_request.reviewers_statuses()
3467 return self.pull_request.reviewers_statuses()
3457
3468
3458
3469
3459 class PullRequestReviewers(Base, BaseModel):
3470 class PullRequestReviewers(Base, BaseModel):
3460 __tablename__ = 'pull_request_reviewers'
3471 __tablename__ = 'pull_request_reviewers'
3461 __table_args__ = (
3472 __table_args__ = (
3462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3474 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3464 )
3475 )
3465
3476
3466 def __init__(self, user=None, pull_request=None, reasons=None):
3477 def __init__(self, user=None, pull_request=None, reasons=None):
3467 self.user = user
3478 self.user = user
3468 self.pull_request = pull_request
3479 self.pull_request = pull_request
3469 self.reasons = reasons or []
3480 self.reasons = reasons or []
3470
3481
3471 @hybrid_property
3482 @hybrid_property
3472 def reasons(self):
3483 def reasons(self):
3473 if not self._reasons:
3484 if not self._reasons:
3474 return []
3485 return []
3475 return self._reasons
3486 return self._reasons
3476
3487
3477 @reasons.setter
3488 @reasons.setter
3478 def reasons(self, val):
3489 def reasons(self, val):
3479 val = val or []
3490 val = val or []
3480 if any(not isinstance(x, basestring) for x in val):
3491 if any(not isinstance(x, basestring) for x in val):
3481 raise Exception('invalid reasons type, must be list of strings')
3492 raise Exception('invalid reasons type, must be list of strings')
3482 self._reasons = val
3493 self._reasons = val
3483
3494
3484 pull_requests_reviewers_id = Column(
3495 pull_requests_reviewers_id = Column(
3485 'pull_requests_reviewers_id', Integer(), nullable=False,
3496 'pull_requests_reviewers_id', Integer(), nullable=False,
3486 primary_key=True)
3497 primary_key=True)
3487 pull_request_id = Column(
3498 pull_request_id = Column(
3488 "pull_request_id", Integer(),
3499 "pull_request_id", Integer(),
3489 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3500 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3490 user_id = Column(
3501 user_id = Column(
3491 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3502 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3492 _reasons = Column(
3503 _reasons = Column(
3493 'reason', MutationList.as_mutable(
3504 'reason', MutationList.as_mutable(
3494 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3505 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3495
3506
3496 user = relationship('User')
3507 user = relationship('User')
3497 pull_request = relationship('PullRequest')
3508 pull_request = relationship('PullRequest')
3498
3509
3499
3510
3500 class Notification(Base, BaseModel):
3511 class Notification(Base, BaseModel):
3501 __tablename__ = 'notifications'
3512 __tablename__ = 'notifications'
3502 __table_args__ = (
3513 __table_args__ = (
3503 Index('notification_type_idx', 'type'),
3514 Index('notification_type_idx', 'type'),
3504 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3515 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3505 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3516 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3506 )
3517 )
3507
3518
3508 TYPE_CHANGESET_COMMENT = u'cs_comment'
3519 TYPE_CHANGESET_COMMENT = u'cs_comment'
3509 TYPE_MESSAGE = u'message'
3520 TYPE_MESSAGE = u'message'
3510 TYPE_MENTION = u'mention'
3521 TYPE_MENTION = u'mention'
3511 TYPE_REGISTRATION = u'registration'
3522 TYPE_REGISTRATION = u'registration'
3512 TYPE_PULL_REQUEST = u'pull_request'
3523 TYPE_PULL_REQUEST = u'pull_request'
3513 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3524 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3514
3525
3515 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3526 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3516 subject = Column('subject', Unicode(512), nullable=True)
3527 subject = Column('subject', Unicode(512), nullable=True)
3517 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3528 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3518 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3529 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3530 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3520 type_ = Column('type', Unicode(255))
3531 type_ = Column('type', Unicode(255))
3521
3532
3522 created_by_user = relationship('User')
3533 created_by_user = relationship('User')
3523 notifications_to_users = relationship('UserNotification', lazy='joined',
3534 notifications_to_users = relationship('UserNotification', lazy='joined',
3524 cascade="all, delete, delete-orphan")
3535 cascade="all, delete, delete-orphan")
3525
3536
3526 @property
3537 @property
3527 def recipients(self):
3538 def recipients(self):
3528 return [x.user for x in UserNotification.query()\
3539 return [x.user for x in UserNotification.query()\
3529 .filter(UserNotification.notification == self)\
3540 .filter(UserNotification.notification == self)\
3530 .order_by(UserNotification.user_id.asc()).all()]
3541 .order_by(UserNotification.user_id.asc()).all()]
3531
3542
3532 @classmethod
3543 @classmethod
3533 def create(cls, created_by, subject, body, recipients, type_=None):
3544 def create(cls, created_by, subject, body, recipients, type_=None):
3534 if type_ is None:
3545 if type_ is None:
3535 type_ = Notification.TYPE_MESSAGE
3546 type_ = Notification.TYPE_MESSAGE
3536
3547
3537 notification = cls()
3548 notification = cls()
3538 notification.created_by_user = created_by
3549 notification.created_by_user = created_by
3539 notification.subject = subject
3550 notification.subject = subject
3540 notification.body = body
3551 notification.body = body
3541 notification.type_ = type_
3552 notification.type_ = type_
3542 notification.created_on = datetime.datetime.now()
3553 notification.created_on = datetime.datetime.now()
3543
3554
3544 for u in recipients:
3555 for u in recipients:
3545 assoc = UserNotification()
3556 assoc = UserNotification()
3546 assoc.notification = notification
3557 assoc.notification = notification
3547
3558
3548 # if created_by is inside recipients mark his notification
3559 # if created_by is inside recipients mark his notification
3549 # as read
3560 # as read
3550 if u.user_id == created_by.user_id:
3561 if u.user_id == created_by.user_id:
3551 assoc.read = True
3562 assoc.read = True
3552
3563
3553 u.notifications.append(assoc)
3564 u.notifications.append(assoc)
3554 Session().add(notification)
3565 Session().add(notification)
3555
3566
3556 return notification
3567 return notification
3557
3568
3558 @property
3569 @property
3559 def description(self):
3570 def description(self):
3560 from rhodecode.model.notification import NotificationModel
3571 from rhodecode.model.notification import NotificationModel
3561 return NotificationModel().make_description(self)
3572 return NotificationModel().make_description(self)
3562
3573
3563
3574
3564 class UserNotification(Base, BaseModel):
3575 class UserNotification(Base, BaseModel):
3565 __tablename__ = 'user_to_notification'
3576 __tablename__ = 'user_to_notification'
3566 __table_args__ = (
3577 __table_args__ = (
3567 UniqueConstraint('user_id', 'notification_id'),
3578 UniqueConstraint('user_id', 'notification_id'),
3568 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3569 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3570 )
3581 )
3571 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3582 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3572 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3583 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3573 read = Column('read', Boolean, default=False)
3584 read = Column('read', Boolean, default=False)
3574 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3585 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3575
3586
3576 user = relationship('User', lazy="joined")
3587 user = relationship('User', lazy="joined")
3577 notification = relationship('Notification', lazy="joined",
3588 notification = relationship('Notification', lazy="joined",
3578 order_by=lambda: Notification.created_on.desc(),)
3589 order_by=lambda: Notification.created_on.desc(),)
3579
3590
3580 def mark_as_read(self):
3591 def mark_as_read(self):
3581 self.read = True
3592 self.read = True
3582 Session().add(self)
3593 Session().add(self)
3583
3594
3584
3595
3585 class Gist(Base, BaseModel):
3596 class Gist(Base, BaseModel):
3586 __tablename__ = 'gists'
3597 __tablename__ = 'gists'
3587 __table_args__ = (
3598 __table_args__ = (
3588 Index('g_gist_access_id_idx', 'gist_access_id'),
3599 Index('g_gist_access_id_idx', 'gist_access_id'),
3589 Index('g_created_on_idx', 'created_on'),
3600 Index('g_created_on_idx', 'created_on'),
3590 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3601 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3591 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3602 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3592 )
3603 )
3593 GIST_PUBLIC = u'public'
3604 GIST_PUBLIC = u'public'
3594 GIST_PRIVATE = u'private'
3605 GIST_PRIVATE = u'private'
3595 DEFAULT_FILENAME = u'gistfile1.txt'
3606 DEFAULT_FILENAME = u'gistfile1.txt'
3596
3607
3597 ACL_LEVEL_PUBLIC = u'acl_public'
3608 ACL_LEVEL_PUBLIC = u'acl_public'
3598 ACL_LEVEL_PRIVATE = u'acl_private'
3609 ACL_LEVEL_PRIVATE = u'acl_private'
3599
3610
3600 gist_id = Column('gist_id', Integer(), primary_key=True)
3611 gist_id = Column('gist_id', Integer(), primary_key=True)
3601 gist_access_id = Column('gist_access_id', Unicode(250))
3612 gist_access_id = Column('gist_access_id', Unicode(250))
3602 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3613 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3603 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3614 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3604 gist_expires = Column('gist_expires', Float(53), nullable=False)
3615 gist_expires = Column('gist_expires', Float(53), nullable=False)
3605 gist_type = Column('gist_type', Unicode(128), nullable=False)
3616 gist_type = Column('gist_type', Unicode(128), nullable=False)
3606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3617 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3607 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3618 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3608 acl_level = Column('acl_level', Unicode(128), nullable=True)
3619 acl_level = Column('acl_level', Unicode(128), nullable=True)
3609
3620
3610 owner = relationship('User')
3621 owner = relationship('User')
3611
3622
3612 def __repr__(self):
3623 def __repr__(self):
3613 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3624 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3614
3625
3615 @classmethod
3626 @classmethod
3616 def get_or_404(cls, id_):
3627 def get_or_404(cls, id_):
3617 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3628 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3618 if not res:
3629 if not res:
3619 raise HTTPNotFound
3630 raise HTTPNotFound
3620 return res
3631 return res
3621
3632
3622 @classmethod
3633 @classmethod
3623 def get_by_access_id(cls, gist_access_id):
3634 def get_by_access_id(cls, gist_access_id):
3624 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3635 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3625
3636
3626 def gist_url(self):
3637 def gist_url(self):
3627 import rhodecode
3638 import rhodecode
3628 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3639 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3629 if alias_url:
3640 if alias_url:
3630 return alias_url.replace('{gistid}', self.gist_access_id)
3641 return alias_url.replace('{gistid}', self.gist_access_id)
3631
3642
3632 return url('gist', gist_id=self.gist_access_id, qualified=True)
3643 return url('gist', gist_id=self.gist_access_id, qualified=True)
3633
3644
3634 @classmethod
3645 @classmethod
3635 def base_path(cls):
3646 def base_path(cls):
3636 """
3647 """
3637 Returns base path when all gists are stored
3648 Returns base path when all gists are stored
3638
3649
3639 :param cls:
3650 :param cls:
3640 """
3651 """
3641 from rhodecode.model.gist import GIST_STORE_LOC
3652 from rhodecode.model.gist import GIST_STORE_LOC
3642 q = Session().query(RhodeCodeUi)\
3653 q = Session().query(RhodeCodeUi)\
3643 .filter(RhodeCodeUi.ui_key == URL_SEP)
3654 .filter(RhodeCodeUi.ui_key == URL_SEP)
3644 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3655 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3645 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3656 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3646
3657
3647 def get_api_data(self):
3658 def get_api_data(self):
3648 """
3659 """
3649 Common function for generating gist related data for API
3660 Common function for generating gist related data for API
3650 """
3661 """
3651 gist = self
3662 gist = self
3652 data = {
3663 data = {
3653 'gist_id': gist.gist_id,
3664 'gist_id': gist.gist_id,
3654 'type': gist.gist_type,
3665 'type': gist.gist_type,
3655 'access_id': gist.gist_access_id,
3666 'access_id': gist.gist_access_id,
3656 'description': gist.gist_description,
3667 'description': gist.gist_description,
3657 'url': gist.gist_url(),
3668 'url': gist.gist_url(),
3658 'expires': gist.gist_expires,
3669 'expires': gist.gist_expires,
3659 'created_on': gist.created_on,
3670 'created_on': gist.created_on,
3660 'modified_at': gist.modified_at,
3671 'modified_at': gist.modified_at,
3661 'content': None,
3672 'content': None,
3662 'acl_level': gist.acl_level,
3673 'acl_level': gist.acl_level,
3663 }
3674 }
3664 return data
3675 return data
3665
3676
3666 def __json__(self):
3677 def __json__(self):
3667 data = dict(
3678 data = dict(
3668 )
3679 )
3669 data.update(self.get_api_data())
3680 data.update(self.get_api_data())
3670 return data
3681 return data
3671 # SCM functions
3682 # SCM functions
3672
3683
3673 def scm_instance(self, **kwargs):
3684 def scm_instance(self, **kwargs):
3674 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3685 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3675 return get_vcs_instance(
3686 return get_vcs_instance(
3676 repo_path=safe_str(full_repo_path), create=False)
3687 repo_path=safe_str(full_repo_path), create=False)
3677
3688
3678
3689
3679 class ExternalIdentity(Base, BaseModel):
3690 class ExternalIdentity(Base, BaseModel):
3680 __tablename__ = 'external_identities'
3691 __tablename__ = 'external_identities'
3681 __table_args__ = (
3692 __table_args__ = (
3682 Index('local_user_id_idx', 'local_user_id'),
3693 Index('local_user_id_idx', 'local_user_id'),
3683 Index('external_id_idx', 'external_id'),
3694 Index('external_id_idx', 'external_id'),
3684 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3695 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3685 'mysql_charset': 'utf8'})
3696 'mysql_charset': 'utf8'})
3686
3697
3687 external_id = Column('external_id', Unicode(255), default=u'',
3698 external_id = Column('external_id', Unicode(255), default=u'',
3688 primary_key=True)
3699 primary_key=True)
3689 external_username = Column('external_username', Unicode(1024), default=u'')
3700 external_username = Column('external_username', Unicode(1024), default=u'')
3690 local_user_id = Column('local_user_id', Integer(),
3701 local_user_id = Column('local_user_id', Integer(),
3691 ForeignKey('users.user_id'), primary_key=True)
3702 ForeignKey('users.user_id'), primary_key=True)
3692 provider_name = Column('provider_name', Unicode(255), default=u'',
3703 provider_name = Column('provider_name', Unicode(255), default=u'',
3693 primary_key=True)
3704 primary_key=True)
3694 access_token = Column('access_token', String(1024), default=u'')
3705 access_token = Column('access_token', String(1024), default=u'')
3695 alt_token = Column('alt_token', String(1024), default=u'')
3706 alt_token = Column('alt_token', String(1024), default=u'')
3696 token_secret = Column('token_secret', String(1024), default=u'')
3707 token_secret = Column('token_secret', String(1024), default=u'')
3697
3708
3698 @classmethod
3709 @classmethod
3699 def by_external_id_and_provider(cls, external_id, provider_name,
3710 def by_external_id_and_provider(cls, external_id, provider_name,
3700 local_user_id=None):
3711 local_user_id=None):
3701 """
3712 """
3702 Returns ExternalIdentity instance based on search params
3713 Returns ExternalIdentity instance based on search params
3703
3714
3704 :param external_id:
3715 :param external_id:
3705 :param provider_name:
3716 :param provider_name:
3706 :return: ExternalIdentity
3717 :return: ExternalIdentity
3707 """
3718 """
3708 query = cls.query()
3719 query = cls.query()
3709 query = query.filter(cls.external_id == external_id)
3720 query = query.filter(cls.external_id == external_id)
3710 query = query.filter(cls.provider_name == provider_name)
3721 query = query.filter(cls.provider_name == provider_name)
3711 if local_user_id:
3722 if local_user_id:
3712 query = query.filter(cls.local_user_id == local_user_id)
3723 query = query.filter(cls.local_user_id == local_user_id)
3713 return query.first()
3724 return query.first()
3714
3725
3715 @classmethod
3726 @classmethod
3716 def user_by_external_id_and_provider(cls, external_id, provider_name):
3727 def user_by_external_id_and_provider(cls, external_id, provider_name):
3717 """
3728 """
3718 Returns User instance based on search params
3729 Returns User instance based on search params
3719
3730
3720 :param external_id:
3731 :param external_id:
3721 :param provider_name:
3732 :param provider_name:
3722 :return: User
3733 :return: User
3723 """
3734 """
3724 query = User.query()
3735 query = User.query()
3725 query = query.filter(cls.external_id == external_id)
3736 query = query.filter(cls.external_id == external_id)
3726 query = query.filter(cls.provider_name == provider_name)
3737 query = query.filter(cls.provider_name == provider_name)
3727 query = query.filter(User.user_id == cls.local_user_id)
3738 query = query.filter(User.user_id == cls.local_user_id)
3728 return query.first()
3739 return query.first()
3729
3740
3730 @classmethod
3741 @classmethod
3731 def by_local_user_id(cls, local_user_id):
3742 def by_local_user_id(cls, local_user_id):
3732 """
3743 """
3733 Returns all tokens for user
3744 Returns all tokens for user
3734
3745
3735 :param local_user_id:
3746 :param local_user_id:
3736 :return: ExternalIdentity
3747 :return: ExternalIdentity
3737 """
3748 """
3738 query = cls.query()
3749 query = cls.query()
3739 query = query.filter(cls.local_user_id == local_user_id)
3750 query = query.filter(cls.local_user_id == local_user_id)
3740 return query
3751 return query
3741
3752
3742
3753
3743 class Integration(Base, BaseModel):
3754 class Integration(Base, BaseModel):
3744 __tablename__ = 'integrations'
3755 __tablename__ = 'integrations'
3745 __table_args__ = (
3756 __table_args__ = (
3746 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3757 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3747 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3758 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3748 )
3759 )
3749
3760
3750 integration_id = Column('integration_id', Integer(), primary_key=True)
3761 integration_id = Column('integration_id', Integer(), primary_key=True)
3751 integration_type = Column('integration_type', String(255))
3762 integration_type = Column('integration_type', String(255))
3752 enabled = Column('enabled', Boolean(), nullable=False)
3763 enabled = Column('enabled', Boolean(), nullable=False)
3753 name = Column('name', String(255), nullable=False)
3764 name = Column('name', String(255), nullable=False)
3754 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3765 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3755 default=False)
3766 default=False)
3756
3767
3757 settings = Column(
3768 settings = Column(
3758 'settings_json', MutationObj.as_mutable(
3769 'settings_json', MutationObj.as_mutable(
3759 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3770 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3760 repo_id = Column(
3771 repo_id = Column(
3761 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3772 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3762 nullable=True, unique=None, default=None)
3773 nullable=True, unique=None, default=None)
3763 repo = relationship('Repository', lazy='joined')
3774 repo = relationship('Repository', lazy='joined')
3764
3775
3765 repo_group_id = Column(
3776 repo_group_id = Column(
3766 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3777 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3767 nullable=True, unique=None, default=None)
3778 nullable=True, unique=None, default=None)
3768 repo_group = relationship('RepoGroup', lazy='joined')
3779 repo_group = relationship('RepoGroup', lazy='joined')
3769
3780
3770 @property
3781 @property
3771 def scope(self):
3782 def scope(self):
3772 if self.repo:
3783 if self.repo:
3773 return repr(self.repo)
3784 return repr(self.repo)
3774 if self.repo_group:
3785 if self.repo_group:
3775 if self.child_repos_only:
3786 if self.child_repos_only:
3776 return repr(self.repo_group) + ' (child repos only)'
3787 return repr(self.repo_group) + ' (child repos only)'
3777 else:
3788 else:
3778 return repr(self.repo_group) + ' (recursive)'
3789 return repr(self.repo_group) + ' (recursive)'
3779 if self.child_repos_only:
3790 if self.child_repos_only:
3780 return 'root_repos'
3791 return 'root_repos'
3781 return 'global'
3792 return 'global'
3782
3793
3783 def __repr__(self):
3794 def __repr__(self):
3784 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3795 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3785
3796
3786
3797
3787 class RepoReviewRuleUser(Base, BaseModel):
3798 class RepoReviewRuleUser(Base, BaseModel):
3788 __tablename__ = 'repo_review_rules_users'
3799 __tablename__ = 'repo_review_rules_users'
3789 __table_args__ = (
3800 __table_args__ = (
3790 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3801 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3791 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3802 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3792 )
3803 )
3793 repo_review_rule_user_id = Column(
3804 repo_review_rule_user_id = Column(
3794 'repo_review_rule_user_id', Integer(), primary_key=True)
3805 'repo_review_rule_user_id', Integer(), primary_key=True)
3795 repo_review_rule_id = Column("repo_review_rule_id",
3806 repo_review_rule_id = Column("repo_review_rule_id",
3796 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3807 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3797 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3808 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3798 nullable=False)
3809 nullable=False)
3799 user = relationship('User')
3810 user = relationship('User')
3800
3811
3801
3812
3802 class RepoReviewRuleUserGroup(Base, BaseModel):
3813 class RepoReviewRuleUserGroup(Base, BaseModel):
3803 __tablename__ = 'repo_review_rules_users_groups'
3814 __tablename__ = 'repo_review_rules_users_groups'
3804 __table_args__ = (
3815 __table_args__ = (
3805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3816 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3817 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3807 )
3818 )
3808 repo_review_rule_users_group_id = Column(
3819 repo_review_rule_users_group_id = Column(
3809 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3820 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3810 repo_review_rule_id = Column("repo_review_rule_id",
3821 repo_review_rule_id = Column("repo_review_rule_id",
3811 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3822 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3812 users_group_id = Column("users_group_id", Integer(),
3823 users_group_id = Column("users_group_id", Integer(),
3813 ForeignKey('users_groups.users_group_id'), nullable=False)
3824 ForeignKey('users_groups.users_group_id'), nullable=False)
3814 users_group = relationship('UserGroup')
3825 users_group = relationship('UserGroup')
3815
3826
3816
3827
3817 class RepoReviewRule(Base, BaseModel):
3828 class RepoReviewRule(Base, BaseModel):
3818 __tablename__ = 'repo_review_rules'
3829 __tablename__ = 'repo_review_rules'
3819 __table_args__ = (
3830 __table_args__ = (
3820 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3831 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3821 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3832 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3822 )
3833 )
3823
3834
3824 repo_review_rule_id = Column(
3835 repo_review_rule_id = Column(
3825 'repo_review_rule_id', Integer(), primary_key=True)
3836 'repo_review_rule_id', Integer(), primary_key=True)
3826 repo_id = Column(
3837 repo_id = Column(
3827 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3838 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3828 repo = relationship('Repository', backref='review_rules')
3839 repo = relationship('Repository', backref='review_rules')
3829
3840
3830 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3841 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3831 default=u'*') # glob
3842 default=u'*') # glob
3832 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3843 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3833 default=u'*') # glob
3844 default=u'*') # glob
3834
3845
3835 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3846 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3836 nullable=False, default=False)
3847 nullable=False, default=False)
3837 rule_users = relationship('RepoReviewRuleUser')
3848 rule_users = relationship('RepoReviewRuleUser')
3838 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3849 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3839
3850
3840 @hybrid_property
3851 @hybrid_property
3841 def branch_pattern(self):
3852 def branch_pattern(self):
3842 return self._branch_pattern or '*'
3853 return self._branch_pattern or '*'
3843
3854
3844 def _validate_glob(self, value):
3855 def _validate_glob(self, value):
3845 re.compile('^' + glob2re(value) + '$')
3856 re.compile('^' + glob2re(value) + '$')
3846
3857
3847 @branch_pattern.setter
3858 @branch_pattern.setter
3848 def branch_pattern(self, value):
3859 def branch_pattern(self, value):
3849 self._validate_glob(value)
3860 self._validate_glob(value)
3850 self._branch_pattern = value or '*'
3861 self._branch_pattern = value or '*'
3851
3862
3852 @hybrid_property
3863 @hybrid_property
3853 def file_pattern(self):
3864 def file_pattern(self):
3854 return self._file_pattern or '*'
3865 return self._file_pattern or '*'
3855
3866
3856 @file_pattern.setter
3867 @file_pattern.setter
3857 def file_pattern(self, value):
3868 def file_pattern(self, value):
3858 self._validate_glob(value)
3869 self._validate_glob(value)
3859 self._file_pattern = value or '*'
3870 self._file_pattern = value or '*'
3860
3871
3861 def matches(self, branch, files_changed):
3872 def matches(self, branch, files_changed):
3862 """
3873 """
3863 Check if this review rule matches a branch/files in a pull request
3874 Check if this review rule matches a branch/files in a pull request
3864
3875
3865 :param branch: branch name for the commit
3876 :param branch: branch name for the commit
3866 :param files_changed: list of file paths changed in the pull request
3877 :param files_changed: list of file paths changed in the pull request
3867 """
3878 """
3868
3879
3869 branch = branch or ''
3880 branch = branch or ''
3870 files_changed = files_changed or []
3881 files_changed = files_changed or []
3871
3882
3872 branch_matches = True
3883 branch_matches = True
3873 if branch:
3884 if branch:
3874 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3885 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3875 branch_matches = bool(branch_regex.search(branch))
3886 branch_matches = bool(branch_regex.search(branch))
3876
3887
3877 files_matches = True
3888 files_matches = True
3878 if self.file_pattern != '*':
3889 if self.file_pattern != '*':
3879 files_matches = False
3890 files_matches = False
3880 file_regex = re.compile(glob2re(self.file_pattern))
3891 file_regex = re.compile(glob2re(self.file_pattern))
3881 for filename in files_changed:
3892 for filename in files_changed:
3882 if file_regex.search(filename):
3893 if file_regex.search(filename):
3883 files_matches = True
3894 files_matches = True
3884 break
3895 break
3885
3896
3886 return branch_matches and files_matches
3897 return branch_matches and files_matches
3887
3898
3888 @property
3899 @property
3889 def review_users(self):
3900 def review_users(self):
3890 """ Returns the users which this rule applies to """
3901 """ Returns the users which this rule applies to """
3891
3902
3892 users = set()
3903 users = set()
3893 users |= set([
3904 users |= set([
3894 rule_user.user for rule_user in self.rule_users
3905 rule_user.user for rule_user in self.rule_users
3895 if rule_user.user.active])
3906 if rule_user.user.active])
3896 users |= set(
3907 users |= set(
3897 member.user
3908 member.user
3898 for rule_user_group in self.rule_user_groups
3909 for rule_user_group in self.rule_user_groups
3899 for member in rule_user_group.users_group.members
3910 for member in rule_user_group.users_group.members
3900 if member.user.active
3911 if member.user.active
3901 )
3912 )
3902 return users
3913 return users
3903
3914
3904 def __repr__(self):
3915 def __repr__(self):
3905 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3916 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3906 self.repo_review_rule_id, self.repo)
3917 self.repo_review_rule_id, self.repo)
3907
3918
3908
3919
3909 class DbMigrateVersion(Base, BaseModel):
3920 class DbMigrateVersion(Base, BaseModel):
3910 __tablename__ = 'db_migrate_version'
3921 __tablename__ = 'db_migrate_version'
3911 __table_args__ = (
3922 __table_args__ = (
3912 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3923 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3913 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3924 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3914 )
3925 )
3915 repository_id = Column('repository_id', String(250), primary_key=True)
3926 repository_id = Column('repository_id', String(250), primary_key=True)
3916 repository_path = Column('repository_path', Text)
3927 repository_path = Column('repository_path', Text)
3917 version = Column('version', Integer)
3928 version = Column('version', Integer)
3918
3929
3919
3930
3920 class DbSession(Base, BaseModel):
3931 class DbSession(Base, BaseModel):
3921 __tablename__ = 'db_session'
3932 __tablename__ = 'db_session'
3922 __table_args__ = (
3933 __table_args__ = (
3923 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3934 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3924 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3935 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3925 )
3936 )
3926
3937
3927 def __repr__(self):
3938 def __repr__(self):
3928 return '<DB:DbSession({})>'.format(self.id)
3939 return '<DB:DbSession({})>'.format(self.id)
3929
3940
3930 id = Column('id', Integer())
3941 id = Column('id', Integer())
3931 namespace = Column('namespace', String(255), primary_key=True)
3942 namespace = Column('namespace', String(255), primary_key=True)
3932 accessed = Column('accessed', DateTime, nullable=False)
3943 accessed = Column('accessed', DateTime, nullable=False)
3933 created = Column('created', DateTime, nullable=False)
3944 created = Column('created', DateTime, nullable=False)
3934 data = Column('data', PickleType, nullable=False)
3945 data = Column('data', PickleType, nullable=False)
@@ -1,257 +1,267 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 py.test config for test suite for making push/pull operations.
22 py.test config for test suite for making push/pull operations.
23
23
24 .. important::
24 .. important::
25
25
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 to redirect things to stderr instead of stdout.
27 to redirect things to stderr instead of stdout.
28 """
28 """
29
29
30 import ConfigParser
30 import ConfigParser
31 import os
31 import os
32 import subprocess32
32 import subprocess32
33 import tempfile
33 import tempfile
34 import textwrap
34 import textwrap
35 import pytest
35 import pytest
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.model.db import Repository
38 from rhodecode.model.db import Repository
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.settings import SettingsModel
41 from rhodecode.tests import (
41 from rhodecode.tests import (
42 GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,)
42 GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,)
43 from rhodecode.tests.fixture import Fixture
43 from rhodecode.tests.fixture import Fixture
44 from rhodecode.tests.utils import (
44 from rhodecode.tests.utils import (
45 set_anonymous_access, is_url_reachable, wait_for_url)
45 set_anonymous_access, is_url_reachable, wait_for_url)
46
46
47 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
47 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
48 REPO_GROUP = 'a_repo_group'
48 REPO_GROUP = 'a_repo_group'
49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
51
51
52
52
53 def assert_no_running_instance(url):
53 def assert_no_running_instance(url):
54 if is_url_reachable(url):
54 if is_url_reachable(url):
55 print("Hint: Usually this means another instance of Enterprise "
55 print("Hint: Usually this means another instance of Enterprise "
56 "is running in the background.")
56 "is running in the background.")
57 pytest.fail(
57 pytest.fail(
58 "Port is not free at %s, cannot start web interface" % url)
58 "Port is not free at %s, cannot start web interface" % url)
59
59
60
60
61 def get_host_url(pylons_config):
61 def get_host_url(pylons_config):
62 """Construct the host url using the port in the test configuration."""
62 """Construct the host url using the port in the test configuration."""
63 config = ConfigParser.ConfigParser()
63 config = ConfigParser.ConfigParser()
64 config.read(pylons_config)
64 config.read(pylons_config)
65
65
66 return '127.0.0.1:%s' % config.get('server:main', 'port')
66 return '127.0.0.1:%s' % config.get('server:main', 'port')
67
67
68
68
69 class RcWebServer(object):
69 class RcWebServer(object):
70 """
70 """
71 Represents a running RCE web server used as a test fixture.
71 Represents a running RCE web server used as a test fixture.
72 """
72 """
73 def __init__(self, pylons_config):
73 def __init__(self, pylons_config):
74 self.pylons_config = pylons_config
74 self.pylons_config = pylons_config
75
75
76 def repo_clone_url(self, repo_name, **kwargs):
76 def repo_clone_url(self, repo_name, **kwargs):
77 params = {
77 params = {
78 'user': TEST_USER_ADMIN_LOGIN,
78 'user': TEST_USER_ADMIN_LOGIN,
79 'passwd': TEST_USER_ADMIN_PASS,
79 'passwd': TEST_USER_ADMIN_PASS,
80 'host': get_host_url(self.pylons_config),
80 'host': get_host_url(self.pylons_config),
81 'cloned_repo': repo_name,
81 'cloned_repo': repo_name,
82 }
82 }
83 params.update(**kwargs)
83 params.update(**kwargs)
84 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
84 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
85 return _url
85 return _url
86
86
87
87
88 @pytest.fixture(scope="module")
88 @pytest.fixture(scope="module")
89 def rcextensions(request, pylonsapp, tmpdir_factory):
89 def rcextensions(request, pylonsapp, tmpdir_factory):
90 """
90 """
91 Installs a testing rcextensions pack to ensure they work as expected.
91 Installs a testing rcextensions pack to ensure they work as expected.
92 """
92 """
93 init_content = textwrap.dedent("""
93 init_content = textwrap.dedent("""
94 # Forward import the example rcextensions to make it
94 # Forward import the example rcextensions to make it
95 # active for our tests.
95 # active for our tests.
96 from rhodecode.tests.other.example_rcextensions import *
96 from rhodecode.tests.other.example_rcextensions import *
97 """)
97 """)
98
98
99 # Note: rcextensions are looked up based on the path of the ini file
99 # Note: rcextensions are looked up based on the path of the ini file
100 root_path = tmpdir_factory.getbasetemp()
100 root_path = tmpdir_factory.getbasetemp()
101 rcextensions_path = root_path.join('rcextensions')
101 rcextensions_path = root_path.join('rcextensions')
102 init_path = rcextensions_path.join('__init__.py')
102 init_path = rcextensions_path.join('__init__.py')
103
103
104 if rcextensions_path.check():
104 if rcextensions_path.check():
105 pytest.fail(
105 pytest.fail(
106 "Path for rcextensions already exists, please clean up before "
106 "Path for rcextensions already exists, please clean up before "
107 "test run this path: %s" % (rcextensions_path, ))
107 "test run this path: %s" % (rcextensions_path, ))
108 return
108 return
109
109
110 request.addfinalizer(rcextensions_path.remove)
110 request.addfinalizer(rcextensions_path.remove)
111 init_path.write_binary(init_content, ensure=True)
111 init_path.write_binary(init_content, ensure=True)
112
112
113
113
114 @pytest.fixture(scope="module")
114 @pytest.fixture(scope="module")
115 def repos(request, pylonsapp):
115 def repos(request, pylonsapp):
116 """Create a copy of each test repo in a repo group."""
116 """Create a copy of each test repo in a repo group."""
117 fixture = Fixture()
117 fixture = Fixture()
118 repo_group = fixture.create_repo_group(REPO_GROUP)
118 repo_group = fixture.create_repo_group(REPO_GROUP)
119 repo_group_id = repo_group.group_id
119 repo_group_id = repo_group.group_id
120 fixture.create_fork(HG_REPO, HG_REPO,
120 fixture.create_fork(HG_REPO, HG_REPO,
121 repo_name_full=HG_REPO_WITH_GROUP,
121 repo_name_full=HG_REPO_WITH_GROUP,
122 repo_group=repo_group_id)
122 repo_group=repo_group_id)
123 fixture.create_fork(GIT_REPO, GIT_REPO,
123 fixture.create_fork(GIT_REPO, GIT_REPO,
124 repo_name_full=GIT_REPO_WITH_GROUP,
124 repo_name_full=GIT_REPO_WITH_GROUP,
125 repo_group=repo_group_id)
125 repo_group=repo_group_id)
126
126
127 @request.addfinalizer
127 @request.addfinalizer
128 def cleanup():
128 def cleanup():
129 fixture.destroy_repo(HG_REPO_WITH_GROUP)
129 fixture.destroy_repo(HG_REPO_WITH_GROUP)
130 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
130 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
131 fixture.destroy_repo_group(repo_group_id)
131 fixture.destroy_repo_group(repo_group_id)
132
132
133
133
134 @pytest.fixture(scope="module")
134 @pytest.fixture(scope="module")
135 def rc_web_server_config(pylons_config):
135 def rc_web_server_config(testini_factory):
136 """
136 """
137 Configuration file used for the fixture `rc_web_server`.
137 Configuration file used for the fixture `rc_web_server`.
138 """
138 """
139 return pylons_config
139 CUSTOM_PARAMS = [
140 {'handler_console': {'level': 'DEBUG'}},
141 ]
142 return testini_factory(CUSTOM_PARAMS)
140
143
141
144
142 @pytest.fixture(scope="module")
145 @pytest.fixture(scope="module")
143 def rc_web_server(
146 def rc_web_server(
144 request, pylonsapp, rc_web_server_config, repos, rcextensions):
147 request, pylonsapp, rc_web_server_config, repos, rcextensions):
145 """
148 """
146 Run the web server as a subprocess.
149 Run the web server as a subprocess.
147
150
148 Since we have already a running vcsserver, this is not spawned again.
151 Since we have already a running vcsserver, this is not spawned again.
149 """
152 """
150 env = os.environ.copy()
153 env = os.environ.copy()
151 env['RC_NO_TMP_PATH'] = '1'
154 env['RC_NO_TMP_PATH'] = '1'
152
155
153 server_out = open(RC_LOG, 'w')
156 rc_log = RC_LOG
157 server_out = open(rc_log, 'w')
154
158
155 # TODO: Would be great to capture the output and err of the subprocess
159 # TODO: Would be great to capture the output and err of the subprocess
156 # and make it available in a section of the py.test report in case of an
160 # and make it available in a section of the py.test report in case of an
157 # error.
161 # error.
158
162
159 host_url = 'http://' + get_host_url(rc_web_server_config)
163 host_url = 'http://' + get_host_url(rc_web_server_config)
160 assert_no_running_instance(host_url)
164 assert_no_running_instance(host_url)
161 command = ['rcserver', rc_web_server_config]
165 command = ['pserve', rc_web_server_config]
162
166
163 print('Starting rcserver: {}'.format(host_url))
167 print('Starting rcserver: {}'.format(host_url))
164 print('Command: {}'.format(command))
168 print('Command: {}'.format(command))
165 print('Logfile: {}'.format(RC_LOG))
169 print('Logfile: {}'.format(rc_log))
166
170
167 proc = subprocess32.Popen(
171 proc = subprocess32.Popen(
168 command, bufsize=0, env=env, stdout=server_out, stderr=server_out)
172 command, bufsize=0, env=env, stdout=server_out, stderr=server_out)
169
173
170 wait_for_url(host_url, timeout=30)
174 wait_for_url(host_url, timeout=30)
171
175
172 @request.addfinalizer
176 @request.addfinalizer
173 def stop_web_server():
177 def stop_web_server():
174 # TODO: Find out how to integrate with the reporting of py.test to
178 # TODO: Find out how to integrate with the reporting of py.test to
175 # make this information available.
179 # make this information available.
176 print "\nServer log file written to %s" % (RC_LOG, )
180 print("\nServer log file written to %s" % (rc_log, ))
177 proc.kill()
181 proc.kill()
182 server_out.flush()
178 server_out.close()
183 server_out.close()
179
184
180 return RcWebServer(rc_web_server_config)
185 return RcWebServer(rc_web_server_config)
181
186
182
187
183 @pytest.fixture(scope='class', autouse=True)
188 @pytest.fixture(scope='class', autouse=True)
184 def disable_anonymous_user_access(pylonsapp):
189 def disable_anonymous_user_access(pylonsapp):
185 set_anonymous_access(False)
190 set_anonymous_access(False)
186
191
187
192
188 @pytest.fixture
193 @pytest.fixture
189 def disable_locking(pylonsapp):
194 def disable_locking(pylonsapp):
190 r = Repository.get_by_repo_name(GIT_REPO)
195 r = Repository.get_by_repo_name(GIT_REPO)
191 Repository.unlock(r)
196 Repository.unlock(r)
192 r.enable_locking = False
197 r.enable_locking = False
193 Session().add(r)
198 Session().add(r)
194 Session().commit()
199 Session().commit()
195
200
196 r = Repository.get_by_repo_name(HG_REPO)
201 r = Repository.get_by_repo_name(HG_REPO)
197 Repository.unlock(r)
202 Repository.unlock(r)
198 r.enable_locking = False
203 r.enable_locking = False
199 Session().add(r)
204 Session().add(r)
200 Session().commit()
205 Session().commit()
201
206
202
207
203 @pytest.fixture
208 @pytest.fixture
204 def enable_auth_plugins(request, pylonsapp, csrf_token):
209 def enable_auth_plugins(request, pylonsapp, csrf_token):
205 """
210 """
206 Return a factory object that when called, allows to control which
211 Return a factory object that when called, allows to control which
207 authentication plugins are enabled.
212 authentication plugins are enabled.
208 """
213 """
209 def _enable_plugins(plugins_list, override=None):
214 def _enable_plugins(plugins_list, override=None):
210 override = override or {}
215 override = override or {}
211 params = {
216 params = {
212 'auth_plugins': ','.join(plugins_list),
217 'auth_plugins': ','.join(plugins_list),
213 'csrf_token': csrf_token,
218 }
219
220 # helper translate some names to others
221 name_map = {
222 'token': 'authtoken'
214 }
223 }
215
224
216 for module in plugins_list:
225 for module in plugins_list:
217 plugin = rhodecode.authentication.base.loadplugin(module)
226 plugin_name = module.partition('#')[-1]
218 plugin_name = plugin.name
227 if plugin_name in name_map:
228 plugin_name = name_map[plugin_name]
219 enabled_plugin = 'auth_%s_enabled' % plugin_name
229 enabled_plugin = 'auth_%s_enabled' % plugin_name
220 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
230 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
221
231
222 # default params that are needed for each plugin,
232 # default params that are needed for each plugin,
223 # `enabled` and `cache_ttl`
233 # `enabled` and `cache_ttl`
224 params.update({
234 params.update({
225 enabled_plugin: True,
235 enabled_plugin: True,
226 cache_ttl: 0
236 cache_ttl: 0
227 })
237 })
228 if override.get:
238 if override.get:
229 params.update(override.get(module, {}))
239 params.update(override.get(module, {}))
230
240
231 validated_params = params
241 validated_params = params
232 for k, v in validated_params.items():
242 for k, v in validated_params.items():
233 setting = SettingsModel().create_or_update_setting(k, v)
243 setting = SettingsModel().create_or_update_setting(k, v)
234 Session().add(setting)
244 Session().add(setting)
235 Session().commit()
245 Session().commit()
236
246
237 def cleanup():
247 def cleanup():
238 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
248 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
239
249
240 request.addfinalizer(cleanup)
250 request.addfinalizer(cleanup)
241
251
242 return _enable_plugins
252 return _enable_plugins
243
253
244
254
245 @pytest.fixture
255 @pytest.fixture
246 def fs_repo_only(request, rhodecode_fixtures):
256 def fs_repo_only(request, rhodecode_fixtures):
247 def fs_repo_fabric(repo_name, repo_type):
257 def fs_repo_fabric(repo_name, repo_type):
248 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
258 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
249 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
259 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
250
260
251 def cleanup():
261 def cleanup():
252 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
262 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
253 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
263 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
254
264
255 request.addfinalizer(cleanup)
265 request.addfinalizer(cleanup)
256
266
257 return fs_repo_fabric
267 return fs_repo_fabric
@@ -1,364 +1,474 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 Test suite for making push/pull operations, on specially modified INI files
22 Test suite for making push/pull operations, on specially modified INI files
23
23
24 .. important::
24 .. important::
25
25
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 to redirect things to stderr instead of stdout.
27 to redirect things to stderr instead of stdout.
28 """
28 """
29
29
30
30
31 import os
31 import os
32 import time
32 import time
33
33
34 import pytest
34 import pytest
35
35
36 from rhodecode.lib.vcs.backends.git.repository import GitRepository
36 from rhodecode.lib.vcs.backends.git.repository import GitRepository
37 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.db import Repository, UserIpMap, CacheKey
39 from rhodecode.model.db import Repository, UserIpMap, CacheKey
39 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
40 from rhodecode.model.user import UserModel
41 from rhodecode.model.user import UserModel
41 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42
43
43 from rhodecode.tests.other.vcs_operations import (
44 from rhodecode.tests.other.vcs_operations import (
44 Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push,
45 Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push,
45 HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46 HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46
47
47
48
48 @pytest.mark.usefixtures("disable_locking")
49 @pytest.mark.usefixtures("disable_locking")
49 class TestVCSOperations:
50 class TestVCSOperations(object):
50
51
51 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 stdout, stderr = Command('/tmp').execute(
54 stdout, stderr = Command('/tmp').execute(
54 'hg clone', clone_url, tmpdir.strpath)
55 'hg clone', clone_url, tmpdir.strpath)
55 _check_proper_clone(stdout, stderr, 'hg')
56 _check_proper_clone(stdout, stderr, 'hg')
56
57
57 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 cmd = Command('/tmp')
60 cmd = Command('/tmp')
60 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 _check_proper_clone(stdout, stderr, 'git')
62 _check_proper_clone(stdout, stderr, 'git')
62 cmd.assert_returncode_success()
63 cmd.assert_returncode_success()
63
64
64 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
65 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
65 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
66 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
66 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
67 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
67 stdout, stderr = Command('/tmp').execute(
68 stdout, stderr = Command('/tmp').execute(
68 'hg clone', clone_url, tmpdir.strpath)
69 'hg clone', clone_url, tmpdir.strpath)
69 _check_proper_clone(stdout, stderr, 'hg')
70 _check_proper_clone(stdout, stderr, 'hg')
70
71
71 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
73 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
73 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 cmd = Command('/tmp')
75 cmd = Command('/tmp')
75 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
76 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
76 _check_proper_clone(stdout, stderr, 'git')
77 _check_proper_clone(stdout, stderr, 'git')
77 cmd.assert_returncode_success()
78 cmd.assert_returncode_success()
78
79
79 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
80 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
80 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
81 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
81 stdout, stderr = Command('/tmp').execute(
82 stdout, stderr = Command('/tmp').execute(
82 'hg clone', clone_url, tmpdir.strpath)
83 'hg clone', clone_url, tmpdir.strpath)
83 _check_proper_clone(stdout, stderr, 'hg')
84 _check_proper_clone(stdout, stderr, 'hg')
84
85
85 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
86 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
86 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
87 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
87 cmd = Command('/tmp')
88 cmd = Command('/tmp')
88 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
89 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
89 _check_proper_clone(stdout, stderr, 'git')
90 _check_proper_clone(stdout, stderr, 'git')
90 cmd.assert_returncode_success()
91 cmd.assert_returncode_success()
91
92
92 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
93 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
93 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
94 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
94 cmd = Command('/tmp')
95 cmd = Command('/tmp')
95 stdout, stderr = cmd.execute(
96 stdout, stderr = cmd.execute(
96 'git clone --depth=1', clone_url, tmpdir.strpath)
97 'git clone --depth=1', clone_url, tmpdir.strpath)
97
98
98 assert '' == stdout
99 assert '' == stdout
99 assert 'Cloning into' in stderr
100 assert 'Cloning into' in stderr
100 cmd.assert_returncode_success()
101 cmd.assert_returncode_success()
101
102
102 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
103 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
103 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
104 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
104 stdout, stderr = Command('/tmp').execute(
105 stdout, stderr = Command('/tmp').execute(
105 'hg clone', clone_url, tmpdir.strpath)
106 'hg clone', clone_url, tmpdir.strpath)
106 assert 'abort: authorization failed' in stderr
107 assert 'abort: authorization failed' in stderr
107
108
108 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
109 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
109 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
110 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
110 stdout, stderr = Command('/tmp').execute(
111 stdout, stderr = Command('/tmp').execute(
111 'git clone', clone_url, tmpdir.strpath)
112 'git clone', clone_url, tmpdir.strpath)
112 assert 'fatal: Authentication failed' in stderr
113 assert 'fatal: Authentication failed' in stderr
113
114
114 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
115 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
115 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
116 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
116 stdout, stderr = Command('/tmp').execute(
117 stdout, stderr = Command('/tmp').execute(
117 'hg clone', clone_url, tmpdir.strpath)
118 'hg clone', clone_url, tmpdir.strpath)
118 assert 'HTTP Error 404: Not Found' in stderr
119 assert 'HTTP Error 404: Not Found' in stderr
119
120
120 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
121 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
121 clone_url = rc_web_server.repo_clone_url(HG_REPO)
122 clone_url = rc_web_server.repo_clone_url(HG_REPO)
122 stdout, stderr = Command('/tmp').execute(
123 stdout, stderr = Command('/tmp').execute(
123 'git clone', clone_url, tmpdir.strpath)
124 'git clone', clone_url, tmpdir.strpath)
124 assert 'not found' in stderr
125 assert 'not found' in stderr
125
126
126 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
127 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
127 clone_url = rc_web_server.repo_clone_url('trololo')
128 clone_url = rc_web_server.repo_clone_url('trololo')
128 stdout, stderr = Command('/tmp').execute(
129 stdout, stderr = Command('/tmp').execute(
129 'hg clone', clone_url, tmpdir.strpath)
130 'hg clone', clone_url, tmpdir.strpath)
130 assert 'HTTP Error 404: Not Found' in stderr
131 assert 'HTTP Error 404: Not Found' in stderr
131
132
132 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
133 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
133 clone_url = rc_web_server.repo_clone_url('trololo')
134 clone_url = rc_web_server.repo_clone_url('trololo')
134 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
135 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
135 assert 'not found' in stderr
136 assert 'not found' in stderr
136
137
137 def test_clone_existing_path_hg_not_in_database(
138 def test_clone_existing_path_hg_not_in_database(
138 self, rc_web_server, tmpdir, fs_repo_only):
139 self, rc_web_server, tmpdir, fs_repo_only):
139
140
140 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
141 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
141 clone_url = rc_web_server.repo_clone_url(db_name)
142 clone_url = rc_web_server.repo_clone_url(db_name)
142 stdout, stderr = Command('/tmp').execute(
143 stdout, stderr = Command('/tmp').execute(
143 'hg clone', clone_url, tmpdir.strpath)
144 'hg clone', clone_url, tmpdir.strpath)
144 assert 'HTTP Error 404: Not Found' in stderr
145 assert 'HTTP Error 404: Not Found' in stderr
145
146
146 def test_clone_existing_path_git_not_in_database(
147 def test_clone_existing_path_git_not_in_database(
147 self, rc_web_server, tmpdir, fs_repo_only):
148 self, rc_web_server, tmpdir, fs_repo_only):
148 db_name = fs_repo_only('not-in-db-git', repo_type='git')
149 db_name = fs_repo_only('not-in-db-git', repo_type='git')
149 clone_url = rc_web_server.repo_clone_url(db_name)
150 clone_url = rc_web_server.repo_clone_url(db_name)
150 stdout, stderr = Command('/tmp').execute(
151 stdout, stderr = Command('/tmp').execute(
151 'git clone', clone_url, tmpdir.strpath)
152 'git clone', clone_url, tmpdir.strpath)
152 assert 'not found' in stderr
153 assert 'not found' in stderr
153
154
154 def test_clone_existing_path_hg_not_in_database_different_scm(
155 def test_clone_existing_path_hg_not_in_database_different_scm(
155 self, rc_web_server, tmpdir, fs_repo_only):
156 self, rc_web_server, tmpdir, fs_repo_only):
156 db_name = fs_repo_only('not-in-db-git', repo_type='git')
157 db_name = fs_repo_only('not-in-db-git', repo_type='git')
157 clone_url = rc_web_server.repo_clone_url(db_name)
158 clone_url = rc_web_server.repo_clone_url(db_name)
158 stdout, stderr = Command('/tmp').execute(
159 stdout, stderr = Command('/tmp').execute(
159 'hg clone', clone_url, tmpdir.strpath)
160 'hg clone', clone_url, tmpdir.strpath)
160 assert 'HTTP Error 404: Not Found' in stderr
161 assert 'HTTP Error 404: Not Found' in stderr
161
162
162 def test_clone_existing_path_git_not_in_database_different_scm(
163 def test_clone_existing_path_git_not_in_database_different_scm(
163 self, rc_web_server, tmpdir, fs_repo_only):
164 self, rc_web_server, tmpdir, fs_repo_only):
164 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
165 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
165 clone_url = rc_web_server.repo_clone_url(db_name)
166 clone_url = rc_web_server.repo_clone_url(db_name)
166 stdout, stderr = Command('/tmp').execute(
167 stdout, stderr = Command('/tmp').execute(
167 'git clone', clone_url, tmpdir.strpath)
168 'git clone', clone_url, tmpdir.strpath)
168 assert 'not found' in stderr
169 assert 'not found' in stderr
169
170
170 def test_push_new_file_hg(self, rc_web_server, tmpdir):
171 def test_push_new_file_hg(self, rc_web_server, tmpdir):
171 clone_url = rc_web_server.repo_clone_url(HG_REPO)
172 clone_url = rc_web_server.repo_clone_url(HG_REPO)
172 stdout, stderr = Command('/tmp').execute(
173 stdout, stderr = Command('/tmp').execute(
173 'hg clone', clone_url, tmpdir.strpath)
174 'hg clone', clone_url, tmpdir.strpath)
174
175
175 stdout, stderr = _add_files_and_push(
176 stdout, stderr = _add_files_and_push(
176 'hg', tmpdir.strpath, clone_url=clone_url)
177 'hg', tmpdir.strpath, clone_url=clone_url)
177
178
178 assert 'pushing to' in stdout
179 assert 'pushing to' in stdout
179 assert 'size summary' in stdout
180 assert 'size summary' in stdout
180
181
181 def test_push_new_file_git(self, rc_web_server, tmpdir):
182 def test_push_new_file_git(self, rc_web_server, tmpdir):
182 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
183 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
183 stdout, stderr = Command('/tmp').execute(
184 stdout, stderr = Command('/tmp').execute(
184 'git clone', clone_url, tmpdir.strpath)
185 'git clone', clone_url, tmpdir.strpath)
185
186
186 # commit some stuff into this repo
187 # commit some stuff into this repo
187 stdout, stderr = _add_files_and_push(
188 stdout, stderr = _add_files_and_push(
188 'git', tmpdir.strpath, clone_url=clone_url)
189 'git', tmpdir.strpath, clone_url=clone_url)
189
190
190 _check_proper_git_push(stdout, stderr)
191 _check_proper_git_push(stdout, stderr)
191
192
192 def test_push_invalidates_cache_hg(self, rc_web_server, tmpdir):
193 def test_push_invalidates_cache_hg(self, rc_web_server, tmpdir):
193 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).scalar()
194 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).scalar()
194 if not key:
195 if not key:
195 key = CacheKey(HG_REPO, HG_REPO)
196 key = CacheKey(HG_REPO, HG_REPO)
196
197
197 key.cache_active = True
198 key.cache_active = True
198 Session().add(key)
199 Session().add(key)
199 Session().commit()
200 Session().commit()
200
201
201 clone_url = rc_web_server.repo_clone_url(HG_REPO)
202 clone_url = rc_web_server.repo_clone_url(HG_REPO)
202 stdout, stderr = Command('/tmp').execute(
203 stdout, stderr = Command('/tmp').execute(
203 'hg clone', clone_url, tmpdir.strpath)
204 'hg clone', clone_url, tmpdir.strpath)
204
205
205 stdout, stderr = _add_files_and_push(
206 stdout, stderr = _add_files_and_push(
206 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
207 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
207
208
208 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).one()
209 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).one()
209 assert key.cache_active is False
210 assert key.cache_active is False
210
211
211 def test_push_invalidates_cache_git(self, rc_web_server, tmpdir):
212 def test_push_invalidates_cache_git(self, rc_web_server, tmpdir):
212 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).scalar()
213 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).scalar()
213 if not key:
214 if not key:
214 key = CacheKey(GIT_REPO, GIT_REPO)
215 key = CacheKey(GIT_REPO, GIT_REPO)
215
216
216 key.cache_active = True
217 key.cache_active = True
217 Session().add(key)
218 Session().add(key)
218 Session().commit()
219 Session().commit()
219
220
220 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
221 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
221 stdout, stderr = Command('/tmp').execute(
222 stdout, stderr = Command('/tmp').execute(
222 'git clone', clone_url, tmpdir.strpath)
223 'git clone', clone_url, tmpdir.strpath)
223
224
224 # commit some stuff into this repo
225 # commit some stuff into this repo
225 stdout, stderr = _add_files_and_push(
226 stdout, stderr = _add_files_and_push(
226 'git', tmpdir.strpath, clone_url=clone_url, files_no=1)
227 'git', tmpdir.strpath, clone_url=clone_url, files_no=1)
227 _check_proper_git_push(stdout, stderr)
228 _check_proper_git_push(stdout, stderr)
228
229
229 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).one()
230 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).one()
230
231
231 assert key.cache_active is False
232 assert key.cache_active is False
232
233
233 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
234 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
234 clone_url = rc_web_server.repo_clone_url(HG_REPO)
235 clone_url = rc_web_server.repo_clone_url(HG_REPO)
235 stdout, stderr = Command('/tmp').execute(
236 stdout, stderr = Command('/tmp').execute(
236 'hg clone', clone_url, tmpdir.strpath)
237 'hg clone', clone_url, tmpdir.strpath)
237
238
238 push_url = rc_web_server.repo_clone_url(
239 push_url = rc_web_server.repo_clone_url(
239 HG_REPO, user='bad', passwd='name')
240 HG_REPO, user='bad', passwd='name')
240 stdout, stderr = _add_files_and_push(
241 stdout, stderr = _add_files_and_push(
241 'hg', tmpdir.strpath, clone_url=push_url)
242 'hg', tmpdir.strpath, clone_url=push_url)
242
243
243 assert 'abort: authorization failed' in stderr
244 assert 'abort: authorization failed' in stderr
244
245
245 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
246 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
246 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
247 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
247 stdout, stderr = Command('/tmp').execute(
248 stdout, stderr = Command('/tmp').execute(
248 'git clone', clone_url, tmpdir.strpath)
249 'git clone', clone_url, tmpdir.strpath)
249
250
250 push_url = rc_web_server.repo_clone_url(
251 push_url = rc_web_server.repo_clone_url(
251 GIT_REPO, user='bad', passwd='name')
252 GIT_REPO, user='bad', passwd='name')
252 stdout, stderr = _add_files_and_push(
253 stdout, stderr = _add_files_and_push(
253 'git', tmpdir.strpath, clone_url=push_url)
254 'git', tmpdir.strpath, clone_url=push_url)
254
255
255 assert 'fatal: Authentication failed' in stderr
256 assert 'fatal: Authentication failed' in stderr
256
257
257 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
258 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
258 clone_url = rc_web_server.repo_clone_url(HG_REPO)
259 clone_url = rc_web_server.repo_clone_url(HG_REPO)
259 stdout, stderr = Command('/tmp').execute(
260 stdout, stderr = Command('/tmp').execute(
260 'hg clone', clone_url, tmpdir.strpath)
261 'hg clone', clone_url, tmpdir.strpath)
261
262
262 stdout, stderr = _add_files_and_push(
263 stdout, stderr = _add_files_and_push(
263 'hg', tmpdir.strpath,
264 'hg', tmpdir.strpath,
264 clone_url=rc_web_server.repo_clone_url('not-existing'))
265 clone_url=rc_web_server.repo_clone_url('not-existing'))
265
266
266 assert 'HTTP Error 404: Not Found' in stderr
267 assert 'HTTP Error 404: Not Found' in stderr
267
268
268 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
269 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
269 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
270 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
270 stdout, stderr = Command('/tmp').execute(
271 stdout, stderr = Command('/tmp').execute(
271 'git clone', clone_url, tmpdir.strpath)
272 'git clone', clone_url, tmpdir.strpath)
272
273
273 stdout, stderr = _add_files_and_push(
274 stdout, stderr = _add_files_and_push(
274 'git', tmpdir.strpath,
275 'git', tmpdir.strpath,
275 clone_url=rc_web_server.repo_clone_url('not-existing'))
276 clone_url=rc_web_server.repo_clone_url('not-existing'))
276
277
277 assert 'not found' in stderr
278 assert 'not found' in stderr
278
279
279 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
280 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
280 user_model = UserModel()
281 user_model = UserModel()
281 try:
282 try:
282 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
283 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
283 Session().commit()
284 Session().commit()
284 time.sleep(2)
285 time.sleep(2)
285 clone_url = rc_web_server.repo_clone_url(HG_REPO)
286 clone_url = rc_web_server.repo_clone_url(HG_REPO)
286 stdout, stderr = Command('/tmp').execute(
287 stdout, stderr = Command('/tmp').execute(
287 'hg clone', clone_url, tmpdir.strpath)
288 'hg clone', clone_url, tmpdir.strpath)
288 assert 'abort: HTTP Error 403: Forbidden' in stderr
289 assert 'abort: HTTP Error 403: Forbidden' in stderr
289 finally:
290 finally:
290 # release IP restrictions
291 # release IP restrictions
291 for ip in UserIpMap.getAll():
292 for ip in UserIpMap.getAll():
292 UserIpMap.delete(ip.ip_id)
293 UserIpMap.delete(ip.ip_id)
293 Session().commit()
294 Session().commit()
294
295
295 time.sleep(2)
296 time.sleep(2)
296
297
297 stdout, stderr = Command('/tmp').execute(
298 stdout, stderr = Command('/tmp').execute(
298 'hg clone', clone_url, tmpdir.strpath)
299 'hg clone', clone_url, tmpdir.strpath)
299 _check_proper_clone(stdout, stderr, 'hg')
300 _check_proper_clone(stdout, stderr, 'hg')
300
301
301 def test_ip_restriction_git(self, rc_web_server, tmpdir):
302 def test_ip_restriction_git(self, rc_web_server, tmpdir):
302 user_model = UserModel()
303 user_model = UserModel()
303 try:
304 try:
304 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
305 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
305 Session().commit()
306 Session().commit()
306 time.sleep(2)
307 time.sleep(2)
307 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
308 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
308 stdout, stderr = Command('/tmp').execute(
309 stdout, stderr = Command('/tmp').execute(
309 'git clone', clone_url, tmpdir.strpath)
310 'git clone', clone_url, tmpdir.strpath)
310 msg = "The requested URL returned error: 403"
311 msg = "The requested URL returned error: 403"
311 assert msg in stderr
312 assert msg in stderr
312 finally:
313 finally:
313 # release IP restrictions
314 # release IP restrictions
314 for ip in UserIpMap.getAll():
315 for ip in UserIpMap.getAll():
315 UserIpMap.delete(ip.ip_id)
316 UserIpMap.delete(ip.ip_id)
316 Session().commit()
317 Session().commit()
317
318
318 time.sleep(2)
319 time.sleep(2)
319
320
320 cmd = Command('/tmp')
321 cmd = Command('/tmp')
321 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
322 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
322 cmd.assert_returncode_success()
323 cmd.assert_returncode_success()
323 _check_proper_clone(stdout, stderr, 'git')
324 _check_proper_clone(stdout, stderr, 'git')
324
325
326 def test_clone_by_auth_token(
327 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
328 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
329 'egg:rhodecode-enterprise-ce#rhodecode'])
330
331 user = user_util.create_user()
332 token = user.auth_tokens[1]
333
334 clone_url = rc_web_server.repo_clone_url(
335 HG_REPO, user=user.username, passwd=token)
336
337 stdout, stderr = Command('/tmp').execute(
338 'hg clone', clone_url, tmpdir.strpath)
339 _check_proper_clone(stdout, stderr, 'hg')
340
341 def test_clone_by_auth_token_expired(
342 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
343 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
344 'egg:rhodecode-enterprise-ce#rhodecode'])
345
346 user = user_util.create_user()
347 auth_token = AuthTokenModel().create(
348 user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS)
349 token = auth_token.api_key
350
351 clone_url = rc_web_server.repo_clone_url(
352 HG_REPO, user=user.username, passwd=token)
353
354 stdout, stderr = Command('/tmp').execute(
355 'hg clone', clone_url, tmpdir.strpath)
356 assert 'abort: authorization failed' in stderr
357
358 def test_clone_by_auth_token_bad_role(
359 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
360 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
361 'egg:rhodecode-enterprise-ce#rhodecode'])
362
363 user = user_util.create_user()
364 auth_token = AuthTokenModel().create(
365 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
366 token = auth_token.api_key
367
368 clone_url = rc_web_server.repo_clone_url(
369 HG_REPO, user=user.username, passwd=token)
370
371 stdout, stderr = Command('/tmp').execute(
372 'hg clone', clone_url, tmpdir.strpath)
373 assert 'abort: authorization failed' in stderr
374
375 def test_clone_by_auth_token_user_disabled(
376 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
377 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
378 'egg:rhodecode-enterprise-ce#rhodecode'])
379 user = user_util.create_user()
380 user.active = False
381 Session().add(user)
382 Session().commit()
383 token = user.auth_tokens[1]
384
385 clone_url = rc_web_server.repo_clone_url(
386 HG_REPO, user=user.username, passwd=token)
387
388 stdout, stderr = Command('/tmp').execute(
389 'hg clone', clone_url, tmpdir.strpath)
390 assert 'abort: authorization failed' in stderr
391
392
393 def test_clone_by_auth_token_with_scope(
394 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
395 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
396 'egg:rhodecode-enterprise-ce#rhodecode'])
397 user = user_util.create_user()
398 auth_token = AuthTokenModel().create(
399 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
400 token = auth_token.api_key
401
402 # manually set scope
403 auth_token.repo = Repository.get_by_repo_name(HG_REPO)
404 Session().add(auth_token)
405 Session().commit()
406
407 clone_url = rc_web_server.repo_clone_url(
408 HG_REPO, user=user.username, passwd=token)
409
410 stdout, stderr = Command('/tmp').execute(
411 'hg clone', clone_url, tmpdir.strpath)
412 _check_proper_clone(stdout, stderr, 'hg')
413
414 def test_clone_by_auth_token_with_wrong_scope(
415 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
416 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
417 'egg:rhodecode-enterprise-ce#rhodecode'])
418 user = user_util.create_user()
419 auth_token = AuthTokenModel().create(
420 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
421 token = auth_token.api_key
422
423 # manually set scope
424 auth_token.repo = Repository.get_by_repo_name(GIT_REPO)
425 Session().add(auth_token)
426 Session().commit()
427
428 clone_url = rc_web_server.repo_clone_url(
429 HG_REPO, user=user.username, passwd=token)
430
431 stdout, stderr = Command('/tmp').execute(
432 'hg clone', clone_url, tmpdir.strpath)
433 assert 'abort: authorization failed' in stderr
434
325
435
326 def test_git_sets_default_branch_if_not_master(
436 def test_git_sets_default_branch_if_not_master(
327 backend_git, tmpdir, disable_locking, rc_web_server):
437 backend_git, tmpdir, disable_locking, rc_web_server):
328 empty_repo = backend_git.create_repo()
438 empty_repo = backend_git.create_repo()
329 clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name)
439 clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name)
330
440
331 cmd = Command(tmpdir.strpath)
441 cmd = Command(tmpdir.strpath)
332 cmd.execute('git clone', clone_url)
442 cmd.execute('git clone', clone_url)
333
443
334 repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name))
444 repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name))
335 repo.in_memory_commit.add(FileNode('file', content=''))
445 repo.in_memory_commit.add(FileNode('file', content=''))
336 repo.in_memory_commit.commit(
446 repo.in_memory_commit.commit(
337 message='Commit on branch test',
447 message='Commit on branch test',
338 author='Automatic test',
448 author='Automatic test',
339 branch='test')
449 branch='test')
340
450
341 repo_cmd = Command(repo.path)
451 repo_cmd = Command(repo.path)
342 stdout, stderr = repo_cmd.execute('git push --verbose origin test')
452 stdout, stderr = repo_cmd.execute('git push --verbose origin test')
343 _check_proper_git_push(
453 _check_proper_git_push(
344 stdout, stderr, branch='test', should_set_default_branch=True)
454 stdout, stderr, branch='test', should_set_default_branch=True)
345
455
346 stdout, stderr = cmd.execute(
456 stdout, stderr = cmd.execute(
347 'git clone', clone_url, empty_repo.repo_name + '-clone')
457 'git clone', clone_url, empty_repo.repo_name + '-clone')
348 _check_proper_clone(stdout, stderr, 'git')
458 _check_proper_clone(stdout, stderr, 'git')
349
459
350 # Doing an explicit commit in order to get latest user logs on MySQL
460 # Doing an explicit commit in order to get latest user logs on MySQL
351 Session().commit()
461 Session().commit()
352
462
353
463
354 def test_git_fetches_from_remote_repository_with_annotated_tags(
464 def test_git_fetches_from_remote_repository_with_annotated_tags(
355 backend_git, disable_locking, rc_web_server):
465 backend_git, disable_locking, rc_web_server):
356 # Note: This is a test specific to the git backend. It checks the
466 # Note: This is a test specific to the git backend. It checks the
357 # integration of fetching from a remote repository which contains
467 # integration of fetching from a remote repository which contains
358 # annotated tags.
468 # annotated tags.
359
469
360 # Dulwich shows this specific behavior only when
470 # Dulwich shows this specific behavior only when
361 # operating against a remote repository.
471 # operating against a remote repository.
362 source_repo = backend_git['annotated-tag']
472 source_repo = backend_git['annotated-tag']
363 target_vcs_repo = backend_git.create_repo().scm_instance()
473 target_vcs_repo = backend_git.create_repo().scm_instance()
364 target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name))
474 target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name))
General Comments 0
You need to be logged in to leave comments. Login now