##// END OF EJS Templates
vcs: reduce sql queries used during pull/push operations.
marcink -
r2140:61a36530 default
parent child Browse files
Show More
@@ -1,697 +1,704 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Authentication modules
22 Authentication modules
23 """
23 """
24
24
25 import colander
25 import colander
26 import copy
26 import copy
27 import logging
27 import logging
28 import time
28 import time
29 import traceback
29 import traceback
30 import warnings
30 import warnings
31 import functools
31 import functools
32
32
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34
35
35 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.lib import caches
38 from rhodecode.lib import caches
38 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.utils2 import md5_safe, safe_int
40 from rhodecode.lib.utils2 import md5_safe, safe_int
40 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.model.db import User
42 from rhodecode.model.db import User
42 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
43 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
46
47
47
48
48 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
49
50
50 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
51 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
52 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
53
54
54
55
55 class hybrid_property(object):
56 class hybrid_property(object):
56 """
57 """
57 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
58 """
59 """
59 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 self.fget = fget
61 self.fget = fget
61 self.fset = fset
62 self.fset = fset
62 self.fdel = fdel
63 self.fdel = fdel
63 self.expr = expr or fget
64 self.expr = expr or fget
64 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
65
66
66 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
67 if instance is None:
68 if instance is None:
68 return self.expr(owner)
69 return self.expr(owner)
69 else:
70 else:
70 return self.fget(instance)
71 return self.fget(instance)
71
72
72 def __set__(self, instance, value):
73 def __set__(self, instance, value):
73 self.fset(instance, value)
74 self.fset(instance, value)
74
75
75 def __delete__(self, instance):
76 def __delete__(self, instance):
76 self.fdel(instance)
77 self.fdel(instance)
77
78
78
79
79
80
80 class LazyFormencode(object):
81 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
83 self.formencode_obj = formencode_obj
83 self.args = args
84 self.args = args
84 self.kwargs = kwargs
85 self.kwargs = kwargs
85
86
86 def __call__(self, *args, **kwargs):
87 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
88 from inspect import isfunction
88 formencode_obj = self.formencode_obj
89 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
90 if isfunction(formencode_obj):
90 # case we wrap validators into functions
91 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
93
94
94
95
95 class RhodeCodeAuthPluginBase(object):
96 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
97 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
98 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
99 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
101 AUTH_CACHE = {}
101
102
102 auth_func_attrs = {
103 auth_func_attrs = {
103 "username": "unique username",
104 "username": "unique username",
104 "firstname": "first name",
105 "firstname": "first name",
105 "lastname": "last name",
106 "lastname": "last name",
106 "email": "email address",
107 "email": "email address",
107 "groups": '["list", "of", "groups"]',
108 "groups": '["list", "of", "groups"]',
108 "extern_name": "name in external source of record",
109 "extern_name": "name in external source of record",
109 "extern_type": "type of external source of record",
110 "extern_type": "type of external source of record",
110 "admin": 'True|False defines if user should be RhodeCode super admin',
111 "admin": 'True|False defines if user should be RhodeCode super admin',
111 "active":
112 "active":
112 'True|False defines active state of user internally for RhodeCode',
113 'True|False defines active state of user internally for RhodeCode',
113 "active_from_extern":
114 "active_from_extern":
114 "True|False\None, active state from the external auth, "
115 "True|False\None, active state from the external auth, "
115 "None means use definition from RhodeCode extern_type active value"
116 "None means use definition from RhodeCode extern_type active value"
116 }
117 }
117 # set on authenticate() method and via set_auth_type func.
118 # set on authenticate() method and via set_auth_type func.
118 auth_type = None
119 auth_type = None
119
120
120 # set on authenticate() method and via set_calling_scope_repo, this is a
121 # set on authenticate() method and via set_calling_scope_repo, this is a
121 # calling scope repository when doing authentication most likely on VCS
122 # calling scope repository when doing authentication most likely on VCS
122 # operations
123 # operations
123 acl_repo_name = None
124 acl_repo_name = None
124
125
125 # List of setting names to store encrypted. Plugins may override this list
126 # List of setting names to store encrypted. Plugins may override this list
126 # to store settings encrypted.
127 # to store settings encrypted.
127 _settings_encrypted = []
128 _settings_encrypted = []
128
129
129 # Mapping of python to DB settings model types. Plugins may override or
130 # Mapping of python to DB settings model types. Plugins may override or
130 # extend this mapping.
131 # extend this mapping.
131 _settings_type_map = {
132 _settings_type_map = {
132 colander.String: 'unicode',
133 colander.String: 'unicode',
133 colander.Integer: 'int',
134 colander.Integer: 'int',
134 colander.Boolean: 'bool',
135 colander.Boolean: 'bool',
135 colander.List: 'list',
136 colander.List: 'list',
136 }
137 }
137
138
138 # list of keys in settings that are unsafe to be logged, should be passwords
139 # list of keys in settings that are unsafe to be logged, should be passwords
139 # or other crucial credentials
140 # or other crucial credentials
140 _settings_unsafe_keys = []
141 _settings_unsafe_keys = []
141
142
142 def __init__(self, plugin_id):
143 def __init__(self, plugin_id):
143 self._plugin_id = plugin_id
144 self._plugin_id = plugin_id
144
145
145 def __str__(self):
146 def __str__(self):
146 return self.get_id()
147 return self.get_id()
147
148
148 def _get_setting_full_name(self, name):
149 def _get_setting_full_name(self, name):
149 """
150 """
150 Return the full setting name used for storing values in the database.
151 Return the full setting name used for storing values in the database.
151 """
152 """
152 # TODO: johbo: Using the name here is problematic. It would be good to
153 # TODO: johbo: Using the name here is problematic. It would be good to
153 # introduce either new models in the database to hold Plugin and
154 # introduce either new models in the database to hold Plugin and
154 # PluginSetting or to use the plugin id here.
155 # PluginSetting or to use the plugin id here.
155 return 'auth_{}_{}'.format(self.name, name)
156 return 'auth_{}_{}'.format(self.name, name)
156
157
157 def _get_setting_type(self, name):
158 def _get_setting_type(self, name):
158 """
159 """
159 Return the type of a setting. This type is defined by the SettingsModel
160 Return the type of a setting. This type is defined by the SettingsModel
160 and determines how the setting is stored in DB. Optionally the suffix
161 and determines how the setting is stored in DB. Optionally the suffix
161 `.encrypted` is appended to instruct SettingsModel to store it
162 `.encrypted` is appended to instruct SettingsModel to store it
162 encrypted.
163 encrypted.
163 """
164 """
164 schema_node = self.get_settings_schema().get(name)
165 schema_node = self.get_settings_schema().get(name)
165 db_type = self._settings_type_map.get(
166 db_type = self._settings_type_map.get(
166 type(schema_node.typ), 'unicode')
167 type(schema_node.typ), 'unicode')
167 if name in self._settings_encrypted:
168 if name in self._settings_encrypted:
168 db_type = '{}.encrypted'.format(db_type)
169 db_type = '{}.encrypted'.format(db_type)
169 return db_type
170 return db_type
170
171
172 @LazyProperty
173 def plugin_settings(self):
174 settings = SettingsModel().get_all_settings()
175 return settings
176
171 def is_enabled(self):
177 def is_enabled(self):
172 """
178 """
173 Returns true if this plugin is enabled. An enabled plugin can be
179 Returns true if this plugin is enabled. An enabled plugin can be
174 configured in the admin interface but it is not consulted during
180 configured in the admin interface but it is not consulted during
175 authentication.
181 authentication.
176 """
182 """
177 auth_plugins = SettingsModel().get_auth_plugins()
183 auth_plugins = SettingsModel().get_auth_plugins()
178 return self.get_id() in auth_plugins
184 return self.get_id() in auth_plugins
179
185
180 def is_active(self):
186 def is_active(self):
181 """
187 """
182 Returns true if the plugin is activated. An activated plugin is
188 Returns true if the plugin is activated. An activated plugin is
183 consulted during authentication, assumed it is also enabled.
189 consulted during authentication, assumed it is also enabled.
184 """
190 """
185 return self.get_setting_by_name('enabled')
191 return self.get_setting_by_name('enabled')
186
192
187 def get_id(self):
193 def get_id(self):
188 """
194 """
189 Returns the plugin id.
195 Returns the plugin id.
190 """
196 """
191 return self._plugin_id
197 return self._plugin_id
192
198
193 def get_display_name(self):
199 def get_display_name(self):
194 """
200 """
195 Returns a translation string for displaying purposes.
201 Returns a translation string for displaying purposes.
196 """
202 """
197 raise NotImplementedError('Not implemented in base class')
203 raise NotImplementedError('Not implemented in base class')
198
204
199 def get_settings_schema(self):
205 def get_settings_schema(self):
200 """
206 """
201 Returns a colander schema, representing the plugin settings.
207 Returns a colander schema, representing the plugin settings.
202 """
208 """
203 return AuthnPluginSettingsSchemaBase()
209 return AuthnPluginSettingsSchemaBase()
204
210
205 def get_setting_by_name(self, name, default=None):
211 def get_setting_by_name(self, name, default=None):
206 """
212 """
207 Returns a plugin setting by name.
213 Returns a plugin setting by name.
208 """
214 """
209 full_name = self._get_setting_full_name(name)
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
210 db_setting = SettingsModel().get_setting_by_name(full_name)
216 plugin_settings = self.plugin_settings
211 return db_setting.app_settings_value if db_setting else default
217
218 return plugin_settings.get(full_name) or default
212
219
213 def create_or_update_setting(self, name, value):
220 def create_or_update_setting(self, name, value):
214 """
221 """
215 Create or update a setting for this plugin in the persistent storage.
222 Create or update a setting for this plugin in the persistent storage.
216 """
223 """
217 full_name = self._get_setting_full_name(name)
224 full_name = self._get_setting_full_name(name)
218 type_ = self._get_setting_type(name)
225 type_ = self._get_setting_type(name)
219 db_setting = SettingsModel().create_or_update_setting(
226 db_setting = SettingsModel().create_or_update_setting(
220 full_name, value, type_)
227 full_name, value, type_)
221 return db_setting.app_settings_value
228 return db_setting.app_settings_value
222
229
223 def get_settings(self):
230 def get_settings(self):
224 """
231 """
225 Returns the plugin settings as dictionary.
232 Returns the plugin settings as dictionary.
226 """
233 """
227 settings = {}
234 settings = {}
228 for node in self.get_settings_schema():
235 for node in self.get_settings_schema():
229 settings[node.name] = self.get_setting_by_name(node.name)
236 settings[node.name] = self.get_setting_by_name(node.name)
230 return settings
237 return settings
231
238
232 def log_safe_settings(self, settings):
239 def log_safe_settings(self, settings):
233 """
240 """
234 returns a log safe representation of settings, without any secrets
241 returns a log safe representation of settings, without any secrets
235 """
242 """
236 settings_copy = copy.deepcopy(settings)
243 settings_copy = copy.deepcopy(settings)
237 for k in self._settings_unsafe_keys:
244 for k in self._settings_unsafe_keys:
238 if k in settings_copy:
245 if k in settings_copy:
239 del settings_copy[k]
246 del settings_copy[k]
240 return settings_copy
247 return settings_copy
241
248
242 @property
249 @property
243 def validators(self):
250 def validators(self):
244 """
251 """
245 Exposes RhodeCode validators modules
252 Exposes RhodeCode validators modules
246 """
253 """
247 # this is a hack to overcome issues with pylons threadlocals and
254 # this is a hack to overcome issues with pylons threadlocals and
248 # translator object _() not being registered properly.
255 # translator object _() not being registered properly.
249 class LazyCaller(object):
256 class LazyCaller(object):
250 def __init__(self, name):
257 def __init__(self, name):
251 self.validator_name = name
258 self.validator_name = name
252
259
253 def __call__(self, *args, **kwargs):
260 def __call__(self, *args, **kwargs):
254 from rhodecode.model import validators as v
261 from rhodecode.model import validators as v
255 obj = getattr(v, self.validator_name)
262 obj = getattr(v, self.validator_name)
256 # log.debug('Initializing lazy formencode object: %s', obj)
263 # log.debug('Initializing lazy formencode object: %s', obj)
257 return LazyFormencode(obj, *args, **kwargs)
264 return LazyFormencode(obj, *args, **kwargs)
258
265
259 class ProxyGet(object):
266 class ProxyGet(object):
260 def __getattribute__(self, name):
267 def __getattribute__(self, name):
261 return LazyCaller(name)
268 return LazyCaller(name)
262
269
263 return ProxyGet()
270 return ProxyGet()
264
271
265 @hybrid_property
272 @hybrid_property
266 def name(self):
273 def name(self):
267 """
274 """
268 Returns the name of this authentication plugin.
275 Returns the name of this authentication plugin.
269
276
270 :returns: string
277 :returns: string
271 """
278 """
272 raise NotImplementedError("Not implemented in base class")
279 raise NotImplementedError("Not implemented in base class")
273
280
274 def get_url_slug(self):
281 def get_url_slug(self):
275 """
282 """
276 Returns a slug which should be used when constructing URLs which refer
283 Returns a slug which should be used when constructing URLs which refer
277 to this plugin. By default it returns the plugin name. If the name is
284 to this plugin. By default it returns the plugin name. If the name is
278 not suitable for using it in an URL the plugin should override this
285 not suitable for using it in an URL the plugin should override this
279 method.
286 method.
280 """
287 """
281 return self.name
288 return self.name
282
289
283 @property
290 @property
284 def is_headers_auth(self):
291 def is_headers_auth(self):
285 """
292 """
286 Returns True if this authentication plugin uses HTTP headers as
293 Returns True if this authentication plugin uses HTTP headers as
287 authentication method.
294 authentication method.
288 """
295 """
289 return False
296 return False
290
297
291 @hybrid_property
298 @hybrid_property
292 def is_container_auth(self):
299 def is_container_auth(self):
293 """
300 """
294 Deprecated method that indicates if this authentication plugin uses
301 Deprecated method that indicates if this authentication plugin uses
295 HTTP headers as authentication method.
302 HTTP headers as authentication method.
296 """
303 """
297 warnings.warn(
304 warnings.warn(
298 'Use is_headers_auth instead.', category=DeprecationWarning)
305 'Use is_headers_auth instead.', category=DeprecationWarning)
299 return self.is_headers_auth
306 return self.is_headers_auth
300
307
301 @hybrid_property
308 @hybrid_property
302 def allows_creating_users(self):
309 def allows_creating_users(self):
303 """
310 """
304 Defines if Plugin allows users to be created on-the-fly when
311 Defines if Plugin allows users to be created on-the-fly when
305 authentication is called. Controls how external plugins should behave
312 authentication is called. Controls how external plugins should behave
306 in terms if they are allowed to create new users, or not. Base plugins
313 in terms if they are allowed to create new users, or not. Base plugins
307 should not be allowed to, but External ones should be !
314 should not be allowed to, but External ones should be !
308
315
309 :return: bool
316 :return: bool
310 """
317 """
311 return False
318 return False
312
319
313 def set_auth_type(self, auth_type):
320 def set_auth_type(self, auth_type):
314 self.auth_type = auth_type
321 self.auth_type = auth_type
315
322
316 def set_calling_scope_repo(self, acl_repo_name):
323 def set_calling_scope_repo(self, acl_repo_name):
317 self.acl_repo_name = acl_repo_name
324 self.acl_repo_name = acl_repo_name
318
325
319 def allows_authentication_from(
326 def allows_authentication_from(
320 self, user, allows_non_existing_user=True,
327 self, user, allows_non_existing_user=True,
321 allowed_auth_plugins=None, allowed_auth_sources=None):
328 allowed_auth_plugins=None, allowed_auth_sources=None):
322 """
329 """
323 Checks if this authentication module should accept a request for
330 Checks if this authentication module should accept a request for
324 the current user.
331 the current user.
325
332
326 :param user: user object fetched using plugin's get_user() method.
333 :param user: user object fetched using plugin's get_user() method.
327 :param allows_non_existing_user: if True, don't allow the
334 :param allows_non_existing_user: if True, don't allow the
328 user to be empty, meaning not existing in our database
335 user to be empty, meaning not existing in our database
329 :param allowed_auth_plugins: if provided, users extern_type will be
336 :param allowed_auth_plugins: if provided, users extern_type will be
330 checked against a list of provided extern types, which are plugin
337 checked against a list of provided extern types, which are plugin
331 auth_names in the end
338 auth_names in the end
332 :param allowed_auth_sources: authentication type allowed,
339 :param allowed_auth_sources: authentication type allowed,
333 `http` or `vcs` default is both.
340 `http` or `vcs` default is both.
334 defines if plugin will accept only http authentication vcs
341 defines if plugin will accept only http authentication vcs
335 authentication(git/hg) or both
342 authentication(git/hg) or both
336 :returns: boolean
343 :returns: boolean
337 """
344 """
338 if not user and not allows_non_existing_user:
345 if not user and not allows_non_existing_user:
339 log.debug('User is empty but plugin does not allow empty users,'
346 log.debug('User is empty but plugin does not allow empty users,'
340 'not allowed to authenticate')
347 'not allowed to authenticate')
341 return False
348 return False
342
349
343 expected_auth_plugins = allowed_auth_plugins or [self.name]
350 expected_auth_plugins = allowed_auth_plugins or [self.name]
344 if user and (user.extern_type and
351 if user and (user.extern_type and
345 user.extern_type not in expected_auth_plugins):
352 user.extern_type not in expected_auth_plugins):
346 log.debug(
353 log.debug(
347 'User `%s` is bound to `%s` auth type. Plugin allows only '
354 'User `%s` is bound to `%s` auth type. Plugin allows only '
348 '%s, skipping', user, user.extern_type, expected_auth_plugins)
355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
349
356
350 return False
357 return False
351
358
352 # by default accept both
359 # by default accept both
353 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
354 if self.auth_type not in expected_auth_from:
361 if self.auth_type not in expected_auth_from:
355 log.debug('Current auth source is %s but plugin only allows %s',
362 log.debug('Current auth source is %s but plugin only allows %s',
356 self.auth_type, expected_auth_from)
363 self.auth_type, expected_auth_from)
357 return False
364 return False
358
365
359 return True
366 return True
360
367
361 def get_user(self, username=None, **kwargs):
368 def get_user(self, username=None, **kwargs):
362 """
369 """
363 Helper method for user fetching in plugins, by default it's using
370 Helper method for user fetching in plugins, by default it's using
364 simple fetch by username, but this method can be custimized in plugins
371 simple fetch by username, but this method can be custimized in plugins
365 eg. headers auth plugin to fetch user by environ params
372 eg. headers auth plugin to fetch user by environ params
366
373
367 :param username: username if given to fetch from database
374 :param username: username if given to fetch from database
368 :param kwargs: extra arguments needed for user fetching.
375 :param kwargs: extra arguments needed for user fetching.
369 """
376 """
370 user = None
377 user = None
371 log.debug(
378 log.debug(
372 'Trying to fetch user `%s` from RhodeCode database', username)
379 'Trying to fetch user `%s` from RhodeCode database', username)
373 if username:
380 if username:
374 user = User.get_by_username(username)
381 user = User.get_by_username(username)
375 if not user:
382 if not user:
376 log.debug('User not found, fallback to fetch user in '
383 log.debug('User not found, fallback to fetch user in '
377 'case insensitive mode')
384 'case insensitive mode')
378 user = User.get_by_username(username, case_insensitive=True)
385 user = User.get_by_username(username, case_insensitive=True)
379 else:
386 else:
380 log.debug('provided username:`%s` is empty skipping...', username)
387 log.debug('provided username:`%s` is empty skipping...', username)
381 if not user:
388 if not user:
382 log.debug('User `%s` not found in database', username)
389 log.debug('User `%s` not found in database', username)
383 else:
390 else:
384 log.debug('Got DB user:%s', user)
391 log.debug('Got DB user:%s', user)
385 return user
392 return user
386
393
387 def user_activation_state(self):
394 def user_activation_state(self):
388 """
395 """
389 Defines user activation state when creating new users
396 Defines user activation state when creating new users
390
397
391 :returns: boolean
398 :returns: boolean
392 """
399 """
393 raise NotImplementedError("Not implemented in base class")
400 raise NotImplementedError("Not implemented in base class")
394
401
395 def auth(self, userobj, username, passwd, settings, **kwargs):
402 def auth(self, userobj, username, passwd, settings, **kwargs):
396 """
403 """
397 Given a user object (which may be null), username, a plaintext
404 Given a user object (which may be null), username, a plaintext
398 password, and a settings object (containing all the keys needed as
405 password, and a settings object (containing all the keys needed as
399 listed in settings()), authenticate this user's login attempt.
406 listed in settings()), authenticate this user's login attempt.
400
407
401 Return None on failure. On success, return a dictionary of the form:
408 Return None on failure. On success, return a dictionary of the form:
402
409
403 see: RhodeCodeAuthPluginBase.auth_func_attrs
410 see: RhodeCodeAuthPluginBase.auth_func_attrs
404 This is later validated for correctness
411 This is later validated for correctness
405 """
412 """
406 raise NotImplementedError("not implemented in base class")
413 raise NotImplementedError("not implemented in base class")
407
414
408 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
409 """
416 """
410 Wrapper to call self.auth() that validates call on it
417 Wrapper to call self.auth() that validates call on it
411
418
412 :param userobj: userobj
419 :param userobj: userobj
413 :param username: username
420 :param username: username
414 :param passwd: plaintext password
421 :param passwd: plaintext password
415 :param settings: plugin settings
422 :param settings: plugin settings
416 """
423 """
417 auth = self.auth(userobj, username, passwd, settings, **kwargs)
424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
418 if auth:
425 if auth:
419 # check if hash should be migrated ?
426 # check if hash should be migrated ?
420 new_hash = auth.get('_hash_migrate')
427 new_hash = auth.get('_hash_migrate')
421 if new_hash:
428 if new_hash:
422 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
429 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
423 return self._validate_auth_return(auth)
430 return self._validate_auth_return(auth)
424 return auth
431 return auth
425
432
426 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
433 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
427 new_hash_cypher = _RhodeCodeCryptoBCrypt()
434 new_hash_cypher = _RhodeCodeCryptoBCrypt()
428 # extra checks, so make sure new hash is correct.
435 # extra checks, so make sure new hash is correct.
429 password_encoded = safe_str(password)
436 password_encoded = safe_str(password)
430 if new_hash and new_hash_cypher.hash_check(
437 if new_hash and new_hash_cypher.hash_check(
431 password_encoded, new_hash):
438 password_encoded, new_hash):
432 cur_user = User.get_by_username(username)
439 cur_user = User.get_by_username(username)
433 cur_user.password = new_hash
440 cur_user.password = new_hash
434 Session().add(cur_user)
441 Session().add(cur_user)
435 Session().flush()
442 Session().flush()
436 log.info('Migrated user %s hash to bcrypt', cur_user)
443 log.info('Migrated user %s hash to bcrypt', cur_user)
437
444
438 def _validate_auth_return(self, ret):
445 def _validate_auth_return(self, ret):
439 if not isinstance(ret, dict):
446 if not isinstance(ret, dict):
440 raise Exception('returned value from auth must be a dict')
447 raise Exception('returned value from auth must be a dict')
441 for k in self.auth_func_attrs:
448 for k in self.auth_func_attrs:
442 if k not in ret:
449 if k not in ret:
443 raise Exception('Missing %s attribute from returned data' % k)
450 raise Exception('Missing %s attribute from returned data' % k)
444 return ret
451 return ret
445
452
446
453
447 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
448
455
449 @hybrid_property
456 @hybrid_property
450 def allows_creating_users(self):
457 def allows_creating_users(self):
451 return True
458 return True
452
459
453 def use_fake_password(self):
460 def use_fake_password(self):
454 """
461 """
455 Return a boolean that indicates whether or not we should set the user's
462 Return a boolean that indicates whether or not we should set the user's
456 password to a random value when it is authenticated by this plugin.
463 password to a random value when it is authenticated by this plugin.
457 If your plugin provides authentication, then you will generally
464 If your plugin provides authentication, then you will generally
458 want this.
465 want this.
459
466
460 :returns: boolean
467 :returns: boolean
461 """
468 """
462 raise NotImplementedError("Not implemented in base class")
469 raise NotImplementedError("Not implemented in base class")
463
470
464 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
465 # at this point _authenticate calls plugin's `auth()` function
472 # at this point _authenticate calls plugin's `auth()` function
466 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
467 userobj, username, passwd, settings, **kwargs)
474 userobj, username, passwd, settings, **kwargs)
468 if auth:
475 if auth:
469 # maybe plugin will clean the username ?
476 # maybe plugin will clean the username ?
470 # we should use the return value
477 # we should use the return value
471 username = auth['username']
478 username = auth['username']
472
479
473 # if external source tells us that user is not active, we should
480 # if external source tells us that user is not active, we should
474 # skip rest of the process. This can prevent from creating users in
481 # skip rest of the process. This can prevent from creating users in
475 # RhodeCode when using external authentication, but if it's
482 # RhodeCode when using external authentication, but if it's
476 # inactive user we shouldn't create that user anyway
483 # inactive user we shouldn't create that user anyway
477 if auth['active_from_extern'] is False:
484 if auth['active_from_extern'] is False:
478 log.warning(
485 log.warning(
479 "User %s authenticated against %s, but is inactive",
486 "User %s authenticated against %s, but is inactive",
480 username, self.__module__)
487 username, self.__module__)
481 return None
488 return None
482
489
483 cur_user = User.get_by_username(username, case_insensitive=True)
490 cur_user = User.get_by_username(username, case_insensitive=True)
484 is_user_existing = cur_user is not None
491 is_user_existing = cur_user is not None
485
492
486 if is_user_existing:
493 if is_user_existing:
487 log.debug('Syncing user `%s` from '
494 log.debug('Syncing user `%s` from '
488 '`%s` plugin', username, self.name)
495 '`%s` plugin', username, self.name)
489 else:
496 else:
490 log.debug('Creating non existing user `%s` from '
497 log.debug('Creating non existing user `%s` from '
491 '`%s` plugin', username, self.name)
498 '`%s` plugin', username, self.name)
492
499
493 if self.allows_creating_users:
500 if self.allows_creating_users:
494 log.debug('Plugin `%s` allows to '
501 log.debug('Plugin `%s` allows to '
495 'create new users', self.name)
502 'create new users', self.name)
496 else:
503 else:
497 log.debug('Plugin `%s` does not allow to '
504 log.debug('Plugin `%s` does not allow to '
498 'create new users', self.name)
505 'create new users', self.name)
499
506
500 user_parameters = {
507 user_parameters = {
501 'username': username,
508 'username': username,
502 'email': auth["email"],
509 'email': auth["email"],
503 'firstname': auth["firstname"],
510 'firstname': auth["firstname"],
504 'lastname': auth["lastname"],
511 'lastname': auth["lastname"],
505 'active': auth["active"],
512 'active': auth["active"],
506 'admin': auth["admin"],
513 'admin': auth["admin"],
507 'extern_name': auth["extern_name"],
514 'extern_name': auth["extern_name"],
508 'extern_type': self.name,
515 'extern_type': self.name,
509 'plugin': self,
516 'plugin': self,
510 'allow_to_create_user': self.allows_creating_users,
517 'allow_to_create_user': self.allows_creating_users,
511 }
518 }
512
519
513 if not is_user_existing:
520 if not is_user_existing:
514 if self.use_fake_password():
521 if self.use_fake_password():
515 # Randomize the PW because we don't need it, but don't want
522 # Randomize the PW because we don't need it, but don't want
516 # them blank either
523 # them blank either
517 passwd = PasswordGenerator().gen_password(length=16)
524 passwd = PasswordGenerator().gen_password(length=16)
518 user_parameters['password'] = passwd
525 user_parameters['password'] = passwd
519 else:
526 else:
520 # Since the password is required by create_or_update method of
527 # Since the password is required by create_or_update method of
521 # UserModel, we need to set it explicitly.
528 # UserModel, we need to set it explicitly.
522 # The create_or_update method is smart and recognises the
529 # The create_or_update method is smart and recognises the
523 # password hashes as well.
530 # password hashes as well.
524 user_parameters['password'] = cur_user.password
531 user_parameters['password'] = cur_user.password
525
532
526 # we either create or update users, we also pass the flag
533 # we either create or update users, we also pass the flag
527 # that controls if this method can actually do that.
534 # that controls if this method can actually do that.
528 # raises NotAllowedToCreateUserError if it cannot, and we try to.
535 # raises NotAllowedToCreateUserError if it cannot, and we try to.
529 user = UserModel().create_or_update(**user_parameters)
536 user = UserModel().create_or_update(**user_parameters)
530 Session().flush()
537 Session().flush()
531 # enforce user is just in given groups, all of them has to be ones
538 # enforce user is just in given groups, all of them has to be ones
532 # created from plugins. We store this info in _group_data JSON
539 # created from plugins. We store this info in _group_data JSON
533 # field
540 # field
534 try:
541 try:
535 groups = auth['groups'] or []
542 groups = auth['groups'] or []
536 UserGroupModel().enforce_groups(user, groups, self.name)
543 UserGroupModel().enforce_groups(user, groups, self.name)
537 except Exception:
544 except Exception:
538 # for any reason group syncing fails, we should
545 # for any reason group syncing fails, we should
539 # proceed with login
546 # proceed with login
540 log.error(traceback.format_exc())
547 log.error(traceback.format_exc())
541 Session().commit()
548 Session().commit()
542 return auth
549 return auth
543
550
544
551
545 def loadplugin(plugin_id):
552 def loadplugin(plugin_id):
546 """
553 """
547 Loads and returns an instantiated authentication plugin.
554 Loads and returns an instantiated authentication plugin.
548 Returns the RhodeCodeAuthPluginBase subclass on success,
555 Returns the RhodeCodeAuthPluginBase subclass on success,
549 or None on failure.
556 or None on failure.
550 """
557 """
551 # TODO: Disusing pyramids thread locals to retrieve the registry.
558 # TODO: Disusing pyramids thread locals to retrieve the registry.
552 authn_registry = get_authn_registry()
559 authn_registry = get_authn_registry()
553 plugin = authn_registry.get_plugin(plugin_id)
560 plugin = authn_registry.get_plugin(plugin_id)
554 if plugin is None:
561 if plugin is None:
555 log.error('Authentication plugin not found: "%s"', plugin_id)
562 log.error('Authentication plugin not found: "%s"', plugin_id)
556 return plugin
563 return plugin
557
564
558
565
559 def get_authn_registry(registry=None):
566 def get_authn_registry(registry=None):
560 registry = registry or get_current_registry()
567 registry = registry or get_current_registry()
561 authn_registry = registry.getUtility(IAuthnPluginRegistry)
568 authn_registry = registry.getUtility(IAuthnPluginRegistry)
562 return authn_registry
569 return authn_registry
563
570
564
571
565 def get_auth_cache_manager(custom_ttl=None):
572 def get_auth_cache_manager(custom_ttl=None):
566 return caches.get_cache_manager(
573 return caches.get_cache_manager(
567 'auth_plugins', 'rhodecode.authentication', custom_ttl)
574 'auth_plugins', 'rhodecode.authentication', custom_ttl)
568
575
569
576
570 def authenticate(username, password, environ=None, auth_type=None,
577 def authenticate(username, password, environ=None, auth_type=None,
571 skip_missing=False, registry=None, acl_repo_name=None):
578 skip_missing=False, registry=None, acl_repo_name=None):
572 """
579 """
573 Authentication function used for access control,
580 Authentication function used for access control,
574 It tries to authenticate based on enabled authentication modules.
581 It tries to authenticate based on enabled authentication modules.
575
582
576 :param username: username can be empty for headers auth
583 :param username: username can be empty for headers auth
577 :param password: password can be empty for headers auth
584 :param password: password can be empty for headers auth
578 :param environ: environ headers passed for headers auth
585 :param environ: environ headers passed for headers auth
579 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
586 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
580 :param skip_missing: ignores plugins that are in db but not in environment
587 :param skip_missing: ignores plugins that are in db but not in environment
581 :returns: None if auth failed, plugin_user dict if auth is correct
588 :returns: None if auth failed, plugin_user dict if auth is correct
582 """
589 """
583 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
590 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
584 raise ValueError('auth type must be on of http, vcs got "%s" instead'
591 raise ValueError('auth type must be on of http, vcs got "%s" instead'
585 % auth_type)
592 % auth_type)
586 headers_only = environ and not (username and password)
593 headers_only = environ and not (username and password)
587
594
588 authn_registry = get_authn_registry(registry)
595 authn_registry = get_authn_registry(registry)
589 for plugin in authn_registry.get_plugins_for_authentication():
596 for plugin in authn_registry.get_plugins_for_authentication():
590 plugin.set_auth_type(auth_type)
597 plugin.set_auth_type(auth_type)
591 plugin.set_calling_scope_repo(acl_repo_name)
598 plugin.set_calling_scope_repo(acl_repo_name)
592
599
593 if headers_only and not plugin.is_headers_auth:
600 if headers_only and not plugin.is_headers_auth:
594 log.debug('Auth type is for headers only and plugin `%s` is not '
601 log.debug('Auth type is for headers only and plugin `%s` is not '
595 'headers plugin, skipping...', plugin.get_id())
602 'headers plugin, skipping...', plugin.get_id())
596 continue
603 continue
597
604
598 # load plugin settings from RhodeCode database
605 # load plugin settings from RhodeCode database
599 plugin_settings = plugin.get_settings()
606 plugin_settings = plugin.get_settings()
600 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
607 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
601 log.debug('Plugin settings:%s', plugin_sanitized_settings)
608 log.debug('Plugin settings:%s', plugin_sanitized_settings)
602
609
603 log.debug('Trying authentication using ** %s **', plugin.get_id())
610 log.debug('Trying authentication using ** %s **', plugin.get_id())
604 # use plugin's method of user extraction.
611 # use plugin's method of user extraction.
605 user = plugin.get_user(username, environ=environ,
612 user = plugin.get_user(username, environ=environ,
606 settings=plugin_settings)
613 settings=plugin_settings)
607 display_user = user.username if user else username
614 display_user = user.username if user else username
608 log.debug(
615 log.debug(
609 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
616 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
610
617
611 if not plugin.allows_authentication_from(user):
618 if not plugin.allows_authentication_from(user):
612 log.debug('Plugin %s does not accept user `%s` for authentication',
619 log.debug('Plugin %s does not accept user `%s` for authentication',
613 plugin.get_id(), display_user)
620 plugin.get_id(), display_user)
614 continue
621 continue
615 else:
622 else:
616 log.debug('Plugin %s accepted user `%s` for authentication',
623 log.debug('Plugin %s accepted user `%s` for authentication',
617 plugin.get_id(), display_user)
624 plugin.get_id(), display_user)
618
625
619 log.info('Authenticating user `%s` using %s plugin',
626 log.info('Authenticating user `%s` using %s plugin',
620 display_user, plugin.get_id())
627 display_user, plugin.get_id())
621
628
622 _cache_ttl = 0
629 _cache_ttl = 0
623
630
624 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
631 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
625 # plugin cache set inside is more important than the settings value
632 # plugin cache set inside is more important than the settings value
626 _cache_ttl = plugin.AUTH_CACHE_TTL
633 _cache_ttl = plugin.AUTH_CACHE_TTL
627 elif plugin_settings.get('cache_ttl'):
634 elif plugin_settings.get('cache_ttl'):
628 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
635 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
629
636
630 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
637 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
631
638
632 # get instance of cache manager configured for a namespace
639 # get instance of cache manager configured for a namespace
633 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
640 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
634
641
635 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
642 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
636 plugin.get_id(), plugin_cache_active, _cache_ttl)
643 plugin.get_id(), plugin_cache_active, _cache_ttl)
637
644
638 # for environ based password can be empty, but then the validation is
645 # for environ based password can be empty, but then the validation is
639 # on the server that fills in the env data needed for authentication
646 # on the server that fills in the env data needed for authentication
640 _password_hash = md5_safe(plugin.name + username + (password or ''))
647 _password_hash = md5_safe(plugin.name + username + (password or ''))
641
648
642 # _authenticate is a wrapper for .auth() method of plugin.
649 # _authenticate is a wrapper for .auth() method of plugin.
643 # it checks if .auth() sends proper data.
650 # it checks if .auth() sends proper data.
644 # For RhodeCodeExternalAuthPlugin it also maps users to
651 # For RhodeCodeExternalAuthPlugin it also maps users to
645 # Database and maps the attributes returned from .auth()
652 # Database and maps the attributes returned from .auth()
646 # to RhodeCode database. If this function returns data
653 # to RhodeCode database. If this function returns data
647 # then auth is correct.
654 # then auth is correct.
648 start = time.time()
655 start = time.time()
649 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
656 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
650
657
651 def auth_func():
658 def auth_func():
652 """
659 """
653 This function is used internally in Cache of Beaker to calculate
660 This function is used internally in Cache of Beaker to calculate
654 Results
661 Results
655 """
662 """
656 return plugin._authenticate(
663 return plugin._authenticate(
657 user, username, password, plugin_settings,
664 user, username, password, plugin_settings,
658 environ=environ or {})
665 environ=environ or {})
659
666
660 if plugin_cache_active:
667 if plugin_cache_active:
661 plugin_user = cache_manager.get(
668 plugin_user = cache_manager.get(
662 _password_hash, createfunc=auth_func)
669 _password_hash, createfunc=auth_func)
663 else:
670 else:
664 plugin_user = auth_func()
671 plugin_user = auth_func()
665
672
666 auth_time = time.time() - start
673 auth_time = time.time() - start
667 log.debug('Authentication for plugin `%s` completed in %.3fs, '
674 log.debug('Authentication for plugin `%s` completed in %.3fs, '
668 'expiration time of fetched cache %.1fs.',
675 'expiration time of fetched cache %.1fs.',
669 plugin.get_id(), auth_time, _cache_ttl)
676 plugin.get_id(), auth_time, _cache_ttl)
670
677
671 log.debug('PLUGIN USER DATA: %s', plugin_user)
678 log.debug('PLUGIN USER DATA: %s', plugin_user)
672
679
673 if plugin_user:
680 if plugin_user:
674 log.debug('Plugin returned proper authentication data')
681 log.debug('Plugin returned proper authentication data')
675 return plugin_user
682 return plugin_user
676 # we failed to Auth because .auth() method didn't return proper user
683 # we failed to Auth because .auth() method didn't return proper user
677 log.debug("User `%s` failed to authenticate against %s",
684 log.debug("User `%s` failed to authenticate against %s",
678 display_user, plugin.get_id())
685 display_user, plugin.get_id())
679 return None
686 return None
680
687
681
688
682 def chop_at(s, sub, inclusive=False):
689 def chop_at(s, sub, inclusive=False):
683 """Truncate string ``s`` at the first occurrence of ``sub``.
690 """Truncate string ``s`` at the first occurrence of ``sub``.
684
691
685 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
692 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
686
693
687 >>> chop_at("plutocratic brats", "rat")
694 >>> chop_at("plutocratic brats", "rat")
688 'plutoc'
695 'plutoc'
689 >>> chop_at("plutocratic brats", "rat", True)
696 >>> chop_at("plutocratic brats", "rat", True)
690 'plutocrat'
697 'plutocrat'
691 """
698 """
692 pos = s.find(sub)
699 pos = s.find(sub)
693 if pos == -1:
700 if pos == -1:
694 return s
701 return s
695 if inclusive:
702 if inclusive:
696 return s[:pos+len(sub)]
703 return s[:pos+len(sub)]
697 return s[:pos]
704 return s[:pos]
@@ -1,87 +1,87 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.exceptions import ConfigurationError
23 from pyramid.exceptions import ConfigurationError
24 from zope.interface import implementer
24 from zope.interface import implementer
25
25
26 from rhodecode.authentication.interface import IAuthnPluginRegistry
26 from rhodecode.authentication.interface import IAuthnPluginRegistry
27 from rhodecode.lib.utils2 import safe_str
27 from rhodecode.lib.utils2 import safe_str
28 from rhodecode.model.settings import SettingsModel
28 from rhodecode.model.settings import SettingsModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 @implementer(IAuthnPluginRegistry)
33 @implementer(IAuthnPluginRegistry)
34 class AuthenticationPluginRegistry(object):
34 class AuthenticationPluginRegistry(object):
35
35
36 # INI settings key to set a fallback authentication plugin.
36 # INI settings key to set a fallback authentication plugin.
37 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
37 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
38
38
39 def __init__(self, settings):
39 def __init__(self, settings):
40 self._plugins = {}
40 self._plugins = {}
41 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
41 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
42
42
43 def add_authn_plugin(self, config, plugin):
43 def add_authn_plugin(self, config, plugin):
44 plugin_id = plugin.get_id()
44 plugin_id = plugin.get_id()
45 if plugin_id in self._plugins.keys():
45 if plugin_id in self._plugins.keys():
46 raise ConfigurationError(
46 raise ConfigurationError(
47 'Cannot register authentication plugin twice: "%s"', plugin_id)
47 'Cannot register authentication plugin twice: "%s"', plugin_id)
48 else:
48 else:
49 log.debug('Register authentication plugin: "%s"', plugin_id)
49 log.debug('Register authentication plugin: "%s"', plugin_id)
50 self._plugins[plugin_id] = plugin
50 self._plugins[plugin_id] = plugin
51
51
52 def get_plugins(self):
52 def get_plugins(self):
53 def sort_key(plugin):
53 def sort_key(plugin):
54 return str.lower(safe_str(plugin.get_display_name()))
54 return str.lower(safe_str(plugin.get_display_name()))
55
55
56 return sorted(self._plugins.values(), key=sort_key)
56 return sorted(self._plugins.values(), key=sort_key)
57
57
58 def get_plugin(self, plugin_id):
58 def get_plugin(self, plugin_id):
59 return self._plugins.get(plugin_id, None)
59 return self._plugins.get(plugin_id, None)
60
60
61 def get_plugins_for_authentication(self):
61 def get_plugins_for_authentication(self):
62 """
62 """
63 Returns a list of plugins which should be consulted when authenticating
63 Returns a list of plugins which should be consulted when authenticating
64 a user. It only returns plugins which are enabled and active.
64 a user. It only returns plugins which are enabled and active.
65 Additionally it includes the fallback plugin from the INI file, if
65 Additionally it includes the fallback plugin from the INI file, if
66 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
66 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
67 """
67 """
68 plugins = []
68 plugins = []
69
69
70 # Add all enabled and active plugins to the list. We iterate over the
70 # Add all enabled and active plugins to the list. We iterate over the
71 # auth_plugins setting from DB beacuse it also represents the ordering.
71 # auth_plugins setting from DB because it also represents the ordering.
72 enabled_plugins = SettingsModel().get_auth_plugins()
72 enabled_plugins = SettingsModel().get_auth_plugins()
73 for plugin_id in enabled_plugins:
73 for plugin_id in enabled_plugins:
74 plugin = self.get_plugin(plugin_id)
74 plugin = self.get_plugin(plugin_id)
75 if plugin is not None and plugin.is_active():
75 if plugin is not None and plugin.is_active():
76 plugins.append(plugin)
76 plugins.append(plugin)
77
77
78 # Add the fallback plugin from ini file.
78 # Add the fallback plugin from ini file.
79 if self._fallback_plugin:
79 if self._fallback_plugin:
80 log.warn(
80 log.warn(
81 'Using fallback authentication plugin from INI file: "%s"',
81 'Using fallback authentication plugin from INI file: "%s"',
82 self._fallback_plugin)
82 self._fallback_plugin)
83 plugin = self.get_plugin(self._fallback_plugin)
83 plugin = self.get_plugin(self._fallback_plugin)
84 if plugin is not None and plugin not in plugins:
84 if plugin is not None and plugin not in plugins:
85 plugins.append(plugin)
85 plugins.append(plugin)
86
86
87 return plugins
87 return plugins
@@ -1,627 +1,630 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
47 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
50 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
51 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
54
55 # NOTE(marcink): remove after base controller is no longer required
55 # NOTE(marcink): remove after base controller is no longer required
56 from pylons.controllers import WSGIController
56 from pylons.controllers import WSGIController
57 from pylons.i18n import translation
57 from pylons.i18n import translation
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 # hack to make the migration to pyramid easier
62 # hack to make the migration to pyramid easier
63 def render(template_name, extra_vars=None, cache_key=None,
63 def render(template_name, extra_vars=None, cache_key=None,
64 cache_type=None, cache_expire=None):
64 cache_type=None, cache_expire=None):
65 """Render a template with Mako
65 """Render a template with Mako
66
66
67 Accepts the cache options ``cache_key``, ``cache_type``, and
67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 ``cache_expire``.
68 ``cache_expire``.
69
69
70 """
70 """
71 from pylons.templating import literal
71 from pylons.templating import literal
72 from pylons.templating import cached_template, pylons_globals
72 from pylons.templating import cached_template, pylons_globals
73
73
74 # Create a render callable for the cache function
74 # Create a render callable for the cache function
75 def render_template():
75 def render_template():
76 # Pull in extra vars if needed
76 # Pull in extra vars if needed
77 globs = extra_vars or {}
77 globs = extra_vars or {}
78
78
79 # Second, get the globals
79 # Second, get the globals
80 globs.update(pylons_globals())
80 globs.update(pylons_globals())
81
81
82 globs['_ungettext'] = globs['ungettext']
82 globs['_ungettext'] = globs['ungettext']
83 # Grab a template reference
83 # Grab a template reference
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85
85
86 return literal(template.render_unicode(**globs))
86 return literal(template.render_unicode(**globs))
87
87
88 return cached_template(template_name, render_template, cache_key=cache_key,
88 return cached_template(template_name, render_template, cache_key=cache_key,
89 cache_type=cache_type, cache_expire=cache_expire)
89 cache_type=cache_type, cache_expire=cache_expire)
90
90
91 def _filter_proxy(ip):
91 def _filter_proxy(ip):
92 """
92 """
93 Passed in IP addresses in HEADERS can be in a special format of multiple
93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 ips. Those comma separated IPs are passed from various proxies in the
94 ips. Those comma separated IPs are passed from various proxies in the
95 chain of request processing. The left-most being the original client.
95 chain of request processing. The left-most being the original client.
96 We only care about the first IP which came from the org. client.
96 We only care about the first IP which came from the org. client.
97
97
98 :param ip: ip string from headers
98 :param ip: ip string from headers
99 """
99 """
100 if ',' in ip:
100 if ',' in ip:
101 _ips = ip.split(',')
101 _ips = ip.split(',')
102 _first_ip = _ips[0].strip()
102 _first_ip = _ips[0].strip()
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 return _first_ip
104 return _first_ip
105 return ip
105 return ip
106
106
107
107
108 def _filter_port(ip):
108 def _filter_port(ip):
109 """
109 """
110 Removes a port from ip, there are 4 main cases to handle here.
110 Removes a port from ip, there are 4 main cases to handle here.
111 - ipv4 eg. 127.0.0.1
111 - ipv4 eg. 127.0.0.1
112 - ipv6 eg. ::1
112 - ipv6 eg. ::1
113 - ipv4+port eg. 127.0.0.1:8080
113 - ipv4+port eg. 127.0.0.1:8080
114 - ipv6+port eg. [::1]:8080
114 - ipv6+port eg. [::1]:8080
115
115
116 :param ip:
116 :param ip:
117 """
117 """
118 def is_ipv6(ip_addr):
118 def is_ipv6(ip_addr):
119 if hasattr(socket, 'inet_pton'):
119 if hasattr(socket, 'inet_pton'):
120 try:
120 try:
121 socket.inet_pton(socket.AF_INET6, ip_addr)
121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 except socket.error:
122 except socket.error:
123 return False
123 return False
124 else:
124 else:
125 # fallback to ipaddress
125 # fallback to ipaddress
126 try:
126 try:
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 except Exception:
128 except Exception:
129 return False
129 return False
130 return True
130 return True
131
131
132 if ':' not in ip: # must be ipv4 pure ip
132 if ':' not in ip: # must be ipv4 pure ip
133 return ip
133 return ip
134
134
135 if '[' in ip and ']' in ip: # ipv6 with port
135 if '[' in ip and ']' in ip: # ipv6 with port
136 return ip.split(']')[0][1:].lower()
136 return ip.split(']')[0][1:].lower()
137
137
138 # must be ipv6 or ipv4 with port
138 # must be ipv6 or ipv4 with port
139 if is_ipv6(ip):
139 if is_ipv6(ip):
140 return ip
140 return ip
141 else:
141 else:
142 ip, _port = ip.split(':')[:2] # means ipv4+port
142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 return ip
143 return ip
144
144
145
145
146 def get_ip_addr(environ):
146 def get_ip_addr(environ):
147 proxy_key = 'HTTP_X_REAL_IP'
147 proxy_key = 'HTTP_X_REAL_IP'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 def_key = 'REMOTE_ADDR'
149 def_key = 'REMOTE_ADDR'
150 _filters = lambda x: _filter_port(_filter_proxy(x))
150 _filters = lambda x: _filter_port(_filter_proxy(x))
151
151
152 ip = environ.get(proxy_key)
152 ip = environ.get(proxy_key)
153 if ip:
153 if ip:
154 return _filters(ip)
154 return _filters(ip)
155
155
156 ip = environ.get(proxy_key2)
156 ip = environ.get(proxy_key2)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(def_key, '0.0.0.0')
160 ip = environ.get(def_key, '0.0.0.0')
161 return _filters(ip)
161 return _filters(ip)
162
162
163
163
164 def get_server_ip_addr(environ, log_errors=True):
164 def get_server_ip_addr(environ, log_errors=True):
165 hostname = environ.get('SERVER_NAME')
165 hostname = environ.get('SERVER_NAME')
166 try:
166 try:
167 return socket.gethostbyname(hostname)
167 return socket.gethostbyname(hostname)
168 except Exception as e:
168 except Exception as e:
169 if log_errors:
169 if log_errors:
170 # in some cases this lookup is not possible, and we don't want to
170 # in some cases this lookup is not possible, and we don't want to
171 # make it an exception in logs
171 # make it an exception in logs
172 log.exception('Could not retrieve server ip address: %s', e)
172 log.exception('Could not retrieve server ip address: %s', e)
173 return hostname
173 return hostname
174
174
175
175
176 def get_server_port(environ):
176 def get_server_port(environ):
177 return environ.get('SERVER_PORT')
177 return environ.get('SERVER_PORT')
178
178
179
179
180 def get_access_path(environ):
180 def get_access_path(environ):
181 path = environ.get('PATH_INFO')
181 path = environ.get('PATH_INFO')
182 org_req = environ.get('pylons.original_request')
182 org_req = environ.get('pylons.original_request')
183 if org_req:
183 if org_req:
184 path = org_req.environ.get('PATH_INFO')
184 path = org_req.environ.get('PATH_INFO')
185 return path
185 return path
186
186
187
187
188 def get_user_agent(environ):
188 def get_user_agent(environ):
189 return environ.get('HTTP_USER_AGENT')
189 return environ.get('HTTP_USER_AGENT')
190
190
191
191
192 def vcs_operation_context(
192 def vcs_operation_context(
193 environ, repo_name, username, action, scm, check_locking=True,
193 environ, repo_name, username, action, scm, check_locking=True,
194 is_shadow_repo=False):
194 is_shadow_repo=False):
195 """
195 """
196 Generate the context for a vcs operation, e.g. push or pull.
196 Generate the context for a vcs operation, e.g. push or pull.
197
197
198 This context is passed over the layers so that hooks triggered by the
198 This context is passed over the layers so that hooks triggered by the
199 vcs operation know details like the user, the user's IP address etc.
199 vcs operation know details like the user, the user's IP address etc.
200
200
201 :param check_locking: Allows to switch of the computation of the locking
201 :param check_locking: Allows to switch of the computation of the locking
202 data. This serves mainly the need of the simplevcs middleware to be
202 data. This serves mainly the need of the simplevcs middleware to be
203 able to disable this for certain operations.
203 able to disable this for certain operations.
204
204
205 """
205 """
206 # Tri-state value: False: unlock, None: nothing, True: lock
206 # Tri-state value: False: unlock, None: nothing, True: lock
207 make_lock = None
207 make_lock = None
208 locked_by = [None, None, None]
208 locked_by = [None, None, None]
209 is_anonymous = username == User.DEFAULT_USER
209 is_anonymous = username == User.DEFAULT_USER
210 if not is_anonymous and check_locking:
210 if not is_anonymous and check_locking:
211 log.debug('Checking locking on repository "%s"', repo_name)
211 log.debug('Checking locking on repository "%s"', repo_name)
212 user = User.get_by_username(username)
212 user = User.get_by_username(username)
213 repo = Repository.get_by_repo_name(repo_name)
213 repo = Repository.get_by_repo_name(repo_name)
214 make_lock, __, locked_by = repo.get_locking_state(
214 make_lock, __, locked_by = repo.get_locking_state(
215 action, user.user_id)
215 action, user.user_id)
216
216
217 settings_model = VcsSettingsModel(repo=repo_name)
217 settings_model = VcsSettingsModel(repo=repo_name)
218 ui_settings = settings_model.get_ui_settings()
218 ui_settings = settings_model.get_ui_settings()
219
219
220 extras = {
220 extras = {
221 'ip': get_ip_addr(environ),
221 'ip': get_ip_addr(environ),
222 'username': username,
222 'username': username,
223 'action': action,
223 'action': action,
224 'repository': repo_name,
224 'repository': repo_name,
225 'scm': scm,
225 'scm': scm,
226 'config': rhodecode.CONFIG['__file__'],
226 'config': rhodecode.CONFIG['__file__'],
227 'make_lock': make_lock,
227 'make_lock': make_lock,
228 'locked_by': locked_by,
228 'locked_by': locked_by,
229 'server_url': utils2.get_server_url(environ),
229 'server_url': utils2.get_server_url(environ),
230 'user_agent': get_user_agent(environ),
230 'user_agent': get_user_agent(environ),
231 'hooks': get_enabled_hook_classes(ui_settings),
231 'hooks': get_enabled_hook_classes(ui_settings),
232 'is_shadow_repo': is_shadow_repo,
232 'is_shadow_repo': is_shadow_repo,
233 }
233 }
234 return extras
234 return extras
235
235
236
236
237 class BasicAuth(AuthBasicAuthenticator):
237 class BasicAuth(AuthBasicAuthenticator):
238
238
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 initial_call_detection=False, acl_repo_name=None):
240 initial_call_detection=False, acl_repo_name=None):
241 self.realm = realm
241 self.realm = realm
242 self.initial_call = initial_call_detection
242 self.initial_call = initial_call_detection
243 self.authfunc = authfunc
243 self.authfunc = authfunc
244 self.registry = registry
244 self.registry = registry
245 self.acl_repo_name = acl_repo_name
245 self.acl_repo_name = acl_repo_name
246 self._rc_auth_http_code = auth_http_code
246 self._rc_auth_http_code = auth_http_code
247
247
248 def _get_response_from_code(self, http_code):
248 def _get_response_from_code(self, http_code):
249 try:
249 try:
250 return get_exception(safe_int(http_code))
250 return get_exception(safe_int(http_code))
251 except Exception:
251 except Exception:
252 log.exception('Failed to fetch response for code %s' % http_code)
252 log.exception('Failed to fetch response for code %s' % http_code)
253 return HTTPForbidden
253 return HTTPForbidden
254
254
255 def get_rc_realm(self):
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257
255 def build_authentication(self):
258 def build_authentication(self):
256 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
257 if self._rc_auth_http_code and not self.initial_call:
260 if self._rc_auth_http_code and not self.initial_call:
258 # return alternative HTTP code if alternative http return code
261 # return alternative HTTP code if alternative http return code
259 # is specified in RhodeCode config, but ONLY if it's not the
262 # is specified in RhodeCode config, but ONLY if it's not the
260 # FIRST call
263 # FIRST call
261 custom_response_klass = self._get_response_from_code(
264 custom_response_klass = self._get_response_from_code(
262 self._rc_auth_http_code)
265 self._rc_auth_http_code)
263 return custom_response_klass(headers=head)
266 return custom_response_klass(headers=head)
264 return HTTPUnauthorized(headers=head)
267 return HTTPUnauthorized(headers=head)
265
268
266 def authenticate(self, environ):
269 def authenticate(self, environ):
267 authorization = AUTHORIZATION(environ)
270 authorization = AUTHORIZATION(environ)
268 if not authorization:
271 if not authorization:
269 return self.build_authentication()
272 return self.build_authentication()
270 (authmeth, auth) = authorization.split(' ', 1)
273 (authmeth, auth) = authorization.split(' ', 1)
271 if 'basic' != authmeth.lower():
274 if 'basic' != authmeth.lower():
272 return self.build_authentication()
275 return self.build_authentication()
273 auth = auth.strip().decode('base64')
276 auth = auth.strip().decode('base64')
274 _parts = auth.split(':', 1)
277 _parts = auth.split(':', 1)
275 if len(_parts) == 2:
278 if len(_parts) == 2:
276 username, password = _parts
279 username, password = _parts
277 if self.authfunc(
280 if self.authfunc(
278 username, password, environ, VCS_TYPE,
281 username, password, environ, VCS_TYPE,
279 registry=self.registry, acl_repo_name=self.acl_repo_name):
282 registry=self.registry, acl_repo_name=self.acl_repo_name):
280 return username
283 return username
281 if username and password:
284 if username and password:
282 # we mark that we actually executed authentication once, at
285 # we mark that we actually executed authentication once, at
283 # that point we can use the alternative auth code
286 # that point we can use the alternative auth code
284 self.initial_call = False
287 self.initial_call = False
285
288
286 return self.build_authentication()
289 return self.build_authentication()
287
290
288 __call__ = authenticate
291 __call__ = authenticate
289
292
290
293
291 def calculate_version_hash(config):
294 def calculate_version_hash(config):
292 return md5(
295 return md5(
293 config.get('beaker.session.secret', '') +
296 config.get('beaker.session.secret', '') +
294 rhodecode.__version__)[:8]
297 rhodecode.__version__)[:8]
295
298
296
299
297 def get_current_lang(request):
300 def get_current_lang(request):
298 # NOTE(marcink): remove after pyramid move
301 # NOTE(marcink): remove after pyramid move
299 try:
302 try:
300 return translation.get_lang()[0]
303 return translation.get_lang()[0]
301 except:
304 except:
302 pass
305 pass
303
306
304 return getattr(request, '_LOCALE_', request.locale_name)
307 return getattr(request, '_LOCALE_', request.locale_name)
305
308
306
309
307 def attach_context_attributes(context, request, user_id):
310 def attach_context_attributes(context, request, user_id):
308 """
311 """
309 Attach variables into template context called `c`, please note that
312 Attach variables into template context called `c`, please note that
310 request could be pylons or pyramid request in here.
313 request could be pylons or pyramid request in here.
311 """
314 """
312 # NOTE(marcink): remove check after pyramid migration
315 # NOTE(marcink): remove check after pyramid migration
313 if hasattr(request, 'registry'):
316 if hasattr(request, 'registry'):
314 config = request.registry.settings
317 config = request.registry.settings
315 else:
318 else:
316 from pylons import config
319 from pylons import config
317
320
318 rc_config = SettingsModel().get_all_settings(cache=True)
321 rc_config = SettingsModel().get_all_settings(cache=True)
319
322
320 context.rhodecode_version = rhodecode.__version__
323 context.rhodecode_version = rhodecode.__version__
321 context.rhodecode_edition = config.get('rhodecode.edition')
324 context.rhodecode_edition = config.get('rhodecode.edition')
322 # unique secret + version does not leak the version but keep consistency
325 # unique secret + version does not leak the version but keep consistency
323 context.rhodecode_version_hash = calculate_version_hash(config)
326 context.rhodecode_version_hash = calculate_version_hash(config)
324
327
325 # Default language set for the incoming request
328 # Default language set for the incoming request
326 context.language = get_current_lang(request)
329 context.language = get_current_lang(request)
327
330
328 # Visual options
331 # Visual options
329 context.visual = AttributeDict({})
332 context.visual = AttributeDict({})
330
333
331 # DB stored Visual Items
334 # DB stored Visual Items
332 context.visual.show_public_icon = str2bool(
335 context.visual.show_public_icon = str2bool(
333 rc_config.get('rhodecode_show_public_icon'))
336 rc_config.get('rhodecode_show_public_icon'))
334 context.visual.show_private_icon = str2bool(
337 context.visual.show_private_icon = str2bool(
335 rc_config.get('rhodecode_show_private_icon'))
338 rc_config.get('rhodecode_show_private_icon'))
336 context.visual.stylify_metatags = str2bool(
339 context.visual.stylify_metatags = str2bool(
337 rc_config.get('rhodecode_stylify_metatags'))
340 rc_config.get('rhodecode_stylify_metatags'))
338 context.visual.dashboard_items = safe_int(
341 context.visual.dashboard_items = safe_int(
339 rc_config.get('rhodecode_dashboard_items', 100))
342 rc_config.get('rhodecode_dashboard_items', 100))
340 context.visual.admin_grid_items = safe_int(
343 context.visual.admin_grid_items = safe_int(
341 rc_config.get('rhodecode_admin_grid_items', 100))
344 rc_config.get('rhodecode_admin_grid_items', 100))
342 context.visual.repository_fields = str2bool(
345 context.visual.repository_fields = str2bool(
343 rc_config.get('rhodecode_repository_fields'))
346 rc_config.get('rhodecode_repository_fields'))
344 context.visual.show_version = str2bool(
347 context.visual.show_version = str2bool(
345 rc_config.get('rhodecode_show_version'))
348 rc_config.get('rhodecode_show_version'))
346 context.visual.use_gravatar = str2bool(
349 context.visual.use_gravatar = str2bool(
347 rc_config.get('rhodecode_use_gravatar'))
350 rc_config.get('rhodecode_use_gravatar'))
348 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
351 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
349 context.visual.default_renderer = rc_config.get(
352 context.visual.default_renderer = rc_config.get(
350 'rhodecode_markup_renderer', 'rst')
353 'rhodecode_markup_renderer', 'rst')
351 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
354 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
352 context.visual.rhodecode_support_url = \
355 context.visual.rhodecode_support_url = \
353 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
356 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
354
357
355 context.visual.affected_files_cut_off = 60
358 context.visual.affected_files_cut_off = 60
356
359
357 context.pre_code = rc_config.get('rhodecode_pre_code')
360 context.pre_code = rc_config.get('rhodecode_pre_code')
358 context.post_code = rc_config.get('rhodecode_post_code')
361 context.post_code = rc_config.get('rhodecode_post_code')
359 context.rhodecode_name = rc_config.get('rhodecode_title')
362 context.rhodecode_name = rc_config.get('rhodecode_title')
360 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
363 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
361 # if we have specified default_encoding in the request, it has more
364 # if we have specified default_encoding in the request, it has more
362 # priority
365 # priority
363 if request.GET.get('default_encoding'):
366 if request.GET.get('default_encoding'):
364 context.default_encodings.insert(0, request.GET.get('default_encoding'))
367 context.default_encodings.insert(0, request.GET.get('default_encoding'))
365 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
368 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
366
369
367 # INI stored
370 # INI stored
368 context.labs_active = str2bool(
371 context.labs_active = str2bool(
369 config.get('labs_settings_active', 'false'))
372 config.get('labs_settings_active', 'false'))
370 context.visual.allow_repo_location_change = str2bool(
373 context.visual.allow_repo_location_change = str2bool(
371 config.get('allow_repo_location_change', True))
374 config.get('allow_repo_location_change', True))
372 context.visual.allow_custom_hooks_settings = str2bool(
375 context.visual.allow_custom_hooks_settings = str2bool(
373 config.get('allow_custom_hooks_settings', True))
376 config.get('allow_custom_hooks_settings', True))
374 context.debug_style = str2bool(config.get('debug_style', False))
377 context.debug_style = str2bool(config.get('debug_style', False))
375
378
376 context.rhodecode_instanceid = config.get('instance_id')
379 context.rhodecode_instanceid = config.get('instance_id')
377
380
378 context.visual.cut_off_limit_diff = safe_int(
381 context.visual.cut_off_limit_diff = safe_int(
379 config.get('cut_off_limit_diff'))
382 config.get('cut_off_limit_diff'))
380 context.visual.cut_off_limit_file = safe_int(
383 context.visual.cut_off_limit_file = safe_int(
381 config.get('cut_off_limit_file'))
384 config.get('cut_off_limit_file'))
382
385
383 # AppEnlight
386 # AppEnlight
384 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
387 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
385 context.appenlight_api_public_key = config.get(
388 context.appenlight_api_public_key = config.get(
386 'appenlight.api_public_key', '')
389 'appenlight.api_public_key', '')
387 context.appenlight_server_url = config.get('appenlight.server_url', '')
390 context.appenlight_server_url = config.get('appenlight.server_url', '')
388
391
389 # JS template context
392 # JS template context
390 context.template_context = {
393 context.template_context = {
391 'repo_name': None,
394 'repo_name': None,
392 'repo_type': None,
395 'repo_type': None,
393 'repo_landing_commit': None,
396 'repo_landing_commit': None,
394 'rhodecode_user': {
397 'rhodecode_user': {
395 'username': None,
398 'username': None,
396 'email': None,
399 'email': None,
397 'notification_status': False
400 'notification_status': False
398 },
401 },
399 'visual': {
402 'visual': {
400 'default_renderer': None
403 'default_renderer': None
401 },
404 },
402 'commit_data': {
405 'commit_data': {
403 'commit_id': None
406 'commit_id': None
404 },
407 },
405 'pull_request_data': {'pull_request_id': None},
408 'pull_request_data': {'pull_request_id': None},
406 'timeago': {
409 'timeago': {
407 'refresh_time': 120 * 1000,
410 'refresh_time': 120 * 1000,
408 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
411 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
409 },
412 },
410 'pyramid_dispatch': {
413 'pyramid_dispatch': {
411
414
412 },
415 },
413 'extra': {'plugins': {}}
416 'extra': {'plugins': {}}
414 }
417 }
415 # END CONFIG VARS
418 # END CONFIG VARS
416
419
417 # TODO: This dosn't work when called from pylons compatibility tween.
420 # TODO: This dosn't work when called from pylons compatibility tween.
418 # Fix this and remove it from base controller.
421 # Fix this and remove it from base controller.
419 # context.repo_name = get_repo_slug(request) # can be empty
422 # context.repo_name = get_repo_slug(request) # can be empty
420
423
421 diffmode = 'sideside'
424 diffmode = 'sideside'
422 if request.GET.get('diffmode'):
425 if request.GET.get('diffmode'):
423 if request.GET['diffmode'] == 'unified':
426 if request.GET['diffmode'] == 'unified':
424 diffmode = 'unified'
427 diffmode = 'unified'
425 elif request.session.get('diffmode'):
428 elif request.session.get('diffmode'):
426 diffmode = request.session['diffmode']
429 diffmode = request.session['diffmode']
427
430
428 context.diffmode = diffmode
431 context.diffmode = diffmode
429
432
430 if request.session.get('diffmode') != diffmode:
433 if request.session.get('diffmode') != diffmode:
431 request.session['diffmode'] = diffmode
434 request.session['diffmode'] = diffmode
432
435
433 context.csrf_token = auth.get_csrf_token(session=request.session)
436 context.csrf_token = auth.get_csrf_token(session=request.session)
434 context.backends = rhodecode.BACKENDS.keys()
437 context.backends = rhodecode.BACKENDS.keys()
435 context.backends.sort()
438 context.backends.sort()
436 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
437
440
438 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
439 # given request will ALWAYS be pyramid one
442 # given request will ALWAYS be pyramid one
440 pyramid_request = pyramid.threadlocal.get_current_request()
443 pyramid_request = pyramid.threadlocal.get_current_request()
441 context.pyramid_request = pyramid_request
444 context.pyramid_request = pyramid_request
442
445
443 # web case
446 # web case
444 if hasattr(pyramid_request, 'user'):
447 if hasattr(pyramid_request, 'user'):
445 context.auth_user = pyramid_request.user
448 context.auth_user = pyramid_request.user
446 context.rhodecode_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
447
450
448 # api case
451 # api case
449 if hasattr(pyramid_request, 'rpc_user'):
452 if hasattr(pyramid_request, 'rpc_user'):
450 context.auth_user = pyramid_request.rpc_user
453 context.auth_user = pyramid_request.rpc_user
451 context.rhodecode_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
452
455
453 # attach the whole call context to the request
456 # attach the whole call context to the request
454 request.call_context = context
457 request.call_context = context
455
458
456
459
457 def get_auth_user(request):
460 def get_auth_user(request):
458 environ = request.environ
461 environ = request.environ
459 session = request.session
462 session = request.session
460
463
461 ip_addr = get_ip_addr(environ)
464 ip_addr = get_ip_addr(environ)
462 # make sure that we update permissions each time we call controller
465 # make sure that we update permissions each time we call controller
463 _auth_token = (request.GET.get('auth_token', '') or
466 _auth_token = (request.GET.get('auth_token', '') or
464 request.GET.get('api_key', ''))
467 request.GET.get('api_key', ''))
465
468
466 if _auth_token:
469 if _auth_token:
467 # when using API_KEY we assume user exists, and
470 # when using API_KEY we assume user exists, and
468 # doesn't need auth based on cookies.
471 # doesn't need auth based on cookies.
469 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
470 authenticated = False
473 authenticated = False
471 else:
474 else:
472 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
473 try:
476 try:
474 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
475 ip_addr=ip_addr)
478 ip_addr=ip_addr)
476 except UserCreationError as e:
479 except UserCreationError as e:
477 h.flash(e, 'error')
480 h.flash(e, 'error')
478 # container auth or other auth functions that create users
481 # container auth or other auth functions that create users
479 # on the fly can throw this exception signaling that there's
482 # on the fly can throw this exception signaling that there's
480 # issue with user creation, explanation should be provided
483 # issue with user creation, explanation should be provided
481 # in Exception itself. We then create a simple blank
484 # in Exception itself. We then create a simple blank
482 # AuthUser
485 # AuthUser
483 auth_user = AuthUser(ip_addr=ip_addr)
486 auth_user = AuthUser(ip_addr=ip_addr)
484
487
485 if password_changed(auth_user, session):
488 if password_changed(auth_user, session):
486 session.invalidate()
489 session.invalidate()
487 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
488 auth_user = AuthUser(ip_addr=ip_addr)
491 auth_user = AuthUser(ip_addr=ip_addr)
489
492
490 authenticated = cookie_store.get('is_authenticated')
493 authenticated = cookie_store.get('is_authenticated')
491
494
492 if not auth_user.is_authenticated and auth_user.is_user_object:
495 if not auth_user.is_authenticated and auth_user.is_user_object:
493 # user is not authenticated and not empty
496 # user is not authenticated and not empty
494 auth_user.set_authenticated(authenticated)
497 auth_user.set_authenticated(authenticated)
495
498
496 return auth_user
499 return auth_user
497
500
498
501
499 class BaseController(WSGIController):
502 class BaseController(WSGIController):
500
503
501 def __before__(self):
504 def __before__(self):
502 """
505 """
503 __before__ is called before controller methods and after __call__
506 __before__ is called before controller methods and after __call__
504 """
507 """
505 # on each call propagate settings calls into global settings.
508 # on each call propagate settings calls into global settings.
506 from pylons import config
509 from pylons import config
507 from pylons import tmpl_context as c, request, url
510 from pylons import tmpl_context as c, request, url
508 set_rhodecode_config(config)
511 set_rhodecode_config(config)
509 attach_context_attributes(c, request, self._rhodecode_user.user_id)
512 attach_context_attributes(c, request, self._rhodecode_user.user_id)
510
513
511 # TODO: Remove this when fixed in attach_context_attributes()
514 # TODO: Remove this when fixed in attach_context_attributes()
512 c.repo_name = get_repo_slug(request) # can be empty
515 c.repo_name = get_repo_slug(request) # can be empty
513
516
514 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
517 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
515 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
518 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
516 self.sa = meta.Session
519 self.sa = meta.Session
517 self.scm_model = ScmModel(self.sa)
520 self.scm_model = ScmModel(self.sa)
518
521
519 # set user language
522 # set user language
520 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
523 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
521 if user_lang:
524 if user_lang:
522 translation.set_lang(user_lang)
525 translation.set_lang(user_lang)
523 log.debug('set language to %s for user %s',
526 log.debug('set language to %s for user %s',
524 user_lang, self._rhodecode_user)
527 user_lang, self._rhodecode_user)
525
528
526 def _dispatch_redirect(self, with_url, environ, start_response):
529 def _dispatch_redirect(self, with_url, environ, start_response):
527 from webob.exc import HTTPFound
530 from webob.exc import HTTPFound
528 resp = HTTPFound(with_url)
531 resp = HTTPFound(with_url)
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
532 environ['SCRIPT_NAME'] = '' # handle prefix middleware
530 environ['PATH_INFO'] = with_url
533 environ['PATH_INFO'] = with_url
531 return resp(environ, start_response)
534 return resp(environ, start_response)
532
535
533 def __call__(self, environ, start_response):
536 def __call__(self, environ, start_response):
534 """Invoke the Controller"""
537 """Invoke the Controller"""
535 # WSGIController.__call__ dispatches to the Controller method
538 # WSGIController.__call__ dispatches to the Controller method
536 # the request is routed to. This routing information is
539 # the request is routed to. This routing information is
537 # available in environ['pylons.routes_dict']
540 # available in environ['pylons.routes_dict']
538 from rhodecode.lib import helpers as h
541 from rhodecode.lib import helpers as h
539 from pylons import tmpl_context as c, request, url
542 from pylons import tmpl_context as c, request, url
540
543
541 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
544 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
542 if environ.get('debugtoolbar.wants_pylons_context', False):
545 if environ.get('debugtoolbar.wants_pylons_context', False):
543 environ['debugtoolbar.pylons_context'] = c._current_obj()
546 environ['debugtoolbar.pylons_context'] = c._current_obj()
544
547
545 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
548 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
546 environ['pylons.routes_dict']['action']])
549 environ['pylons.routes_dict']['action']])
547
550
548 self.rc_config = SettingsModel().get_all_settings(cache=True)
551 self.rc_config = SettingsModel().get_all_settings(cache=True)
549 self.ip_addr = get_ip_addr(environ)
552 self.ip_addr = get_ip_addr(environ)
550
553
551 # The rhodecode auth user is looked up and passed through the
554 # The rhodecode auth user is looked up and passed through the
552 # environ by the pylons compatibility tween in pyramid.
555 # environ by the pylons compatibility tween in pyramid.
553 # So we can just grab it from there.
556 # So we can just grab it from there.
554 auth_user = environ['rc_auth_user']
557 auth_user = environ['rc_auth_user']
555
558
556 # set globals for auth user
559 # set globals for auth user
557 request.user = auth_user
560 request.user = auth_user
558 self._rhodecode_user = auth_user
561 self._rhodecode_user = auth_user
559
562
560 log.info('IP: %s User: %s accessed %s [%s]' % (
563 log.info('IP: %s User: %s accessed %s [%s]' % (
561 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
564 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
562 _route_name)
565 _route_name)
563 )
566 )
564
567
565 user_obj = auth_user.get_instance()
568 user_obj = auth_user.get_instance()
566 if user_obj and user_obj.user_data.get('force_password_change'):
569 if user_obj and user_obj.user_data.get('force_password_change'):
567 h.flash('You are required to change your password', 'warning',
570 h.flash('You are required to change your password', 'warning',
568 ignore_duplicate=True)
571 ignore_duplicate=True)
569 return self._dispatch_redirect(
572 return self._dispatch_redirect(
570 url('my_account_password'), environ, start_response)
573 url('my_account_password'), environ, start_response)
571
574
572 return WSGIController.__call__(self, environ, start_response)
575 return WSGIController.__call__(self, environ, start_response)
573
576
574
577
575 def h_filter(s):
578 def h_filter(s):
576 """
579 """
577 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
580 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
578 we wrap this with additional functionality that converts None to empty
581 we wrap this with additional functionality that converts None to empty
579 strings
582 strings
580 """
583 """
581 if s is None:
584 if s is None:
582 return markupsafe.Markup()
585 return markupsafe.Markup()
583 return markupsafe.escape(s)
586 return markupsafe.escape(s)
584
587
585
588
586 def add_events_routes(config):
589 def add_events_routes(config):
587 """
590 """
588 Adds routing that can be used in events. Because some events are triggered
591 Adds routing that can be used in events. Because some events are triggered
589 outside of pyramid context, we need to bootstrap request with some
592 outside of pyramid context, we need to bootstrap request with some
590 routing registered
593 routing registered
591 """
594 """
592 config.add_route(name='home', pattern='/')
595 config.add_route(name='home', pattern='/')
593
596
594 config.add_route(name='repo_summary', pattern='/{repo_name}')
597 config.add_route(name='repo_summary', pattern='/{repo_name}')
595 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
598 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
596 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
599 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
597
600
598 config.add_route(name='pullrequest_show',
601 config.add_route(name='pullrequest_show',
599 pattern='/{repo_name}/pull-request/{pull_request_id}')
602 pattern='/{repo_name}/pull-request/{pull_request_id}')
600 config.add_route(name='pull_requests_global',
603 config.add_route(name='pull_requests_global',
601 pattern='/pull-request/{pull_request_id}')
604 pattern='/pull-request/{pull_request_id}')
602
605
603 config.add_route(name='repo_commit',
606 config.add_route(name='repo_commit',
604 pattern='/{repo_name}/changeset/{commit_id}')
607 pattern='/{repo_name}/changeset/{commit_id}')
605 config.add_route(name='repo_files',
608 config.add_route(name='repo_files',
606 pattern='/{repo_name}/files/{commit_id}/{f_path}')
609 pattern='/{repo_name}/files/{commit_id}/{f_path}')
607
610
608
611
609 def bootstrap_request(**kwargs):
612 def bootstrap_request(**kwargs):
610 import pyramid.testing
613 import pyramid.testing
611
614
612 class TestRequest(pyramid.testing.DummyRequest):
615 class TestRequest(pyramid.testing.DummyRequest):
613 application_url = kwargs.pop('application_url', 'http://example.com')
616 application_url = kwargs.pop('application_url', 'http://example.com')
614 host = kwargs.pop('host', 'example.com:80')
617 host = kwargs.pop('host', 'example.com:80')
615 domain = kwargs.pop('domain', 'example.com')
618 domain = kwargs.pop('domain', 'example.com')
616
619
617 class TestDummySession(pyramid.testing.DummySession):
620 class TestDummySession(pyramid.testing.DummySession):
618 def save(*arg, **kw):
621 def save(*arg, **kw):
619 pass
622 pass
620
623
621 request = TestRequest(**kwargs)
624 request = TestRequest(**kwargs)
622 request.session = TestDummySession()
625 request.session = TestDummySession()
623
626
624 config = pyramid.testing.setUp(request=request)
627 config = pyramid.testing.setUp(request=request)
625 add_events_routes(config)
628 add_events_routes(config)
626 return request
629 return request
627
630
@@ -1,540 +1,540 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 (
39 from rhodecode.lib.base import (
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
41 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
43 NotAllowedToCreateUserError)
43 NotAllowedToCreateUserError)
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware import appenlight
46 from rhodecode.lib.middleware.utils import scm_app_http
46 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
47 from rhodecode.lib.utils import (
48 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
48 is_valid_repo, get_rhodecode_base_path, SLUG_RE)
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.backends import base
51 from rhodecode.lib.vcs.backends import base
52 from rhodecode.model import meta
52 from rhodecode.model import meta
53 from rhodecode.model.db import User, Repository, PullRequest
53 from rhodecode.model.db import User, Repository, PullRequest
54 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.pull_request import PullRequestModel
56
56 from rhodecode.model.settings import SettingsModel
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def initialize_generator(factory):
61 def initialize_generator(factory):
62 """
62 """
63 Initializes the returned generator by draining its first element.
63 Initializes the returned generator by draining its first element.
64
64
65 This can be used to give a generator an initializer, which is the code
65 This can be used to give a generator an initializer, which is the code
66 up to the first yield statement. This decorator enforces that the first
66 up to the first yield statement. This decorator enforces that the first
67 produced element has the value ``"__init__"`` to make its special
67 produced element has the value ``"__init__"`` to make its special
68 purpose very explicit in the using code.
68 purpose very explicit in the using code.
69 """
69 """
70
70
71 @wraps(factory)
71 @wraps(factory)
72 def wrapper(*args, **kwargs):
72 def wrapper(*args, **kwargs):
73 gen = factory(*args, **kwargs)
73 gen = factory(*args, **kwargs)
74 try:
74 try:
75 init = gen.next()
75 init = gen.next()
76 except StopIteration:
76 except StopIteration:
77 raise ValueError('Generator must yield at least one element.')
77 raise ValueError('Generator must yield at least one element.')
78 if init != "__init__":
78 if init != "__init__":
79 raise ValueError('First yielded element must be "__init__".')
79 raise ValueError('First yielded element must be "__init__".')
80 return gen
80 return gen
81 return wrapper
81 return wrapper
82
82
83
83
84 class SimpleVCS(object):
84 class SimpleVCS(object):
85 """Common functionality for SCM HTTP handlers."""
85 """Common functionality for SCM HTTP handlers."""
86
86
87 SCM = 'unknown'
87 SCM = 'unknown'
88
88
89 acl_repo_name = None
89 acl_repo_name = None
90 url_repo_name = None
90 url_repo_name = None
91 vcs_repo_name = None
91 vcs_repo_name = None
92
92
93 # We have to handle requests to shadow repositories different than requests
93 # We have to handle requests to shadow repositories different than requests
94 # to normal repositories. Therefore we have to distinguish them. To do this
94 # to normal repositories. Therefore we have to distinguish them. To do this
95 # we use this regex which will match only on URLs pointing to shadow
95 # we use this regex which will match only on URLs pointing to shadow
96 # repositories.
96 # repositories.
97 shadow_repo_re = re.compile(
97 shadow_repo_re = re.compile(
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
99 '(?P<target>{slug_pat})/' # target repo
99 '(?P<target>{slug_pat})/' # target repo
100 'pull-request/(?P<pr_id>\d+)/' # pull request
100 'pull-request/(?P<pr_id>\d+)/' # pull request
101 'repository$' # shadow repo
101 'repository$' # shadow repo
102 .format(slug_pat=SLUG_RE.pattern))
102 .format(slug_pat=SLUG_RE.pattern))
103
103
104 def __init__(self, application, config, registry):
104 def __init__(self, application, config, registry):
105 self.registry = registry
105 self.registry = registry
106 self.application = application
106 self.application = application
107 self.config = config
107 self.config = config
108 # re-populated by specialized middleware
108 # re-populated by specialized middleware
109 self.repo_vcs_config = base.Config()
109 self.repo_vcs_config = base.Config()
110
110 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
111 # base path of repo locations
111 self.basepath = rhodecode.CONFIG['base_path']
112 self.basepath = get_rhodecode_base_path()
112 registry.rhodecode_settings = self.rhodecode_settings
113 # authenticate this VCS request using authfunc
113 # authenticate this VCS request using authfunc
114 auth_ret_code_detection = \
114 auth_ret_code_detection = \
115 str2bool(self.config.get('auth_ret_code_detection', False))
115 str2bool(self.config.get('auth_ret_code_detection', False))
116 self.authenticate = BasicAuth(
116 self.authenticate = BasicAuth(
117 '', authenticate, registry, config.get('auth_ret_code'),
117 '', authenticate, registry, config.get('auth_ret_code'),
118 auth_ret_code_detection)
118 auth_ret_code_detection)
119 self.ip_addr = '0.0.0.0'
119 self.ip_addr = '0.0.0.0'
120
120
121 def set_repo_names(self, environ):
121 def set_repo_names(self, environ):
122 """
122 """
123 This will populate the attributes acl_repo_name, url_repo_name,
123 This will populate the attributes acl_repo_name, url_repo_name,
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
125 shadow) repositories all names are equal. In case of requests to a
125 shadow) repositories all names are equal. In case of requests to a
126 shadow repository the acl-name points to the target repo of the pull
126 shadow repository the acl-name points to the target repo of the pull
127 request and the vcs-name points to the shadow repo file system path.
127 request and the vcs-name points to the shadow repo file system path.
128 The url-name is always the URL used by the vcs client program.
128 The url-name is always the URL used by the vcs client program.
129
129
130 Example in case of a shadow repo:
130 Example in case of a shadow repo:
131 acl_repo_name = RepoGroup/MyRepo
131 acl_repo_name = RepoGroup/MyRepo
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
134 """
134 """
135 # First we set the repo name from URL for all attributes. This is the
135 # First we set the repo name from URL for all attributes. This is the
136 # default if handling normal (non shadow) repo requests.
136 # default if handling normal (non shadow) repo requests.
137 self.url_repo_name = self._get_repository_name(environ)
137 self.url_repo_name = self._get_repository_name(environ)
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
139 self.is_shadow_repo = False
139 self.is_shadow_repo = False
140
140
141 # Check if this is a request to a shadow repository.
141 # Check if this is a request to a shadow repository.
142 match = self.shadow_repo_re.match(self.url_repo_name)
142 match = self.shadow_repo_re.match(self.url_repo_name)
143 if match:
143 if match:
144 match_dict = match.groupdict()
144 match_dict = match.groupdict()
145
145
146 # Build acl repo name from regex match.
146 # Build acl repo name from regex match.
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
148 groups=match_dict['groups'] or '',
148 groups=match_dict['groups'] or '',
149 target=match_dict['target']))
149 target=match_dict['target']))
150
150
151 # Retrieve pull request instance by ID from regex match.
151 # Retrieve pull request instance by ID from regex match.
152 pull_request = PullRequest.get(match_dict['pr_id'])
152 pull_request = PullRequest.get(match_dict['pr_id'])
153
153
154 # Only proceed if we got a pull request and if acl repo name from
154 # Only proceed if we got a pull request and if acl repo name from
155 # URL equals the target repo name of the pull request.
155 # URL equals the target repo name of the pull request.
156 if pull_request and (acl_repo_name ==
156 if pull_request and (acl_repo_name ==
157 pull_request.target_repo.repo_name):
157 pull_request.target_repo.repo_name):
158 # Get file system path to shadow repository.
158 # Get file system path to shadow repository.
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
160 target_vcs = pull_request.target_repo.scm_instance()
160 target_vcs = pull_request.target_repo.scm_instance()
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
162 workspace_id)
162 workspace_id)
163
163
164 # Store names for later usage.
164 # Store names for later usage.
165 self.vcs_repo_name = vcs_repo_name
165 self.vcs_repo_name = vcs_repo_name
166 self.acl_repo_name = acl_repo_name
166 self.acl_repo_name = acl_repo_name
167 self.is_shadow_repo = True
167 self.is_shadow_repo = True
168
168
169 log.debug('Setting all VCS repository names: %s', {
169 log.debug('Setting all VCS repository names: %s', {
170 'acl_repo_name': self.acl_repo_name,
170 'acl_repo_name': self.acl_repo_name,
171 'url_repo_name': self.url_repo_name,
171 'url_repo_name': self.url_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
173 })
173 })
174
174
175 @property
175 @property
176 def scm_app(self):
176 def scm_app(self):
177 custom_implementation = self.config['vcs.scm_app_implementation']
177 custom_implementation = self.config['vcs.scm_app_implementation']
178 if custom_implementation == 'http':
178 if custom_implementation == 'http':
179 log.info('Using HTTP implementation of scm app.')
179 log.info('Using HTTP implementation of scm app.')
180 scm_app_impl = scm_app_http
180 scm_app_impl = scm_app_http
181 else:
181 else:
182 log.info('Using custom implementation of scm_app: "{}"'.format(
182 log.info('Using custom implementation of scm_app: "{}"'.format(
183 custom_implementation))
183 custom_implementation))
184 scm_app_impl = importlib.import_module(custom_implementation)
184 scm_app_impl = importlib.import_module(custom_implementation)
185 return scm_app_impl
185 return scm_app_impl
186
186
187 def _get_by_id(self, repo_name):
187 def _get_by_id(self, repo_name):
188 """
188 """
189 Gets a special pattern _<ID> from clone url and tries to replace it
189 Gets a special pattern _<ID> from clone url and tries to replace it
190 with a repository_name for support of _<ID> non changeable urls
190 with a repository_name for support of _<ID> non changeable urls
191 """
191 """
192
192
193 data = repo_name.split('/')
193 data = repo_name.split('/')
194 if len(data) >= 2:
194 if len(data) >= 2:
195 from rhodecode.model.repo import RepoModel
195 from rhodecode.model.repo import RepoModel
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
197 if by_id_match:
197 if by_id_match:
198 data[1] = by_id_match.repo_name
198 data[1] = by_id_match.repo_name
199
199
200 return safe_str('/'.join(data))
200 return safe_str('/'.join(data))
201
201
202 def _invalidate_cache(self, repo_name):
202 def _invalidate_cache(self, repo_name):
203 """
203 """
204 Set's cache for this repository for invalidation on next access
204 Set's cache for this repository for invalidation on next access
205
205
206 :param repo_name: full repo name, also a cache key
206 :param repo_name: full repo name, also a cache key
207 """
207 """
208 ScmModel().mark_for_invalidation(repo_name)
208 ScmModel().mark_for_invalidation(repo_name)
209
209
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
211 db_repo = Repository.get_by_repo_name(repo_name)
211 db_repo = Repository.get_by_repo_name(repo_name)
212 if not db_repo:
212 if not db_repo:
213 log.debug('Repository `%s` not found inside the database.',
213 log.debug('Repository `%s` not found inside the database.',
214 repo_name)
214 repo_name)
215 return False
215 return False
216
216
217 if db_repo.repo_type != scm_type:
217 if db_repo.repo_type != scm_type:
218 log.warning(
218 log.warning(
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
220 repo_name, db_repo.repo_type, scm_type)
220 repo_name, db_repo.repo_type, scm_type)
221 return False
221 return False
222
222
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
224
224
225 def valid_and_active_user(self, user):
225 def valid_and_active_user(self, user):
226 """
226 """
227 Checks if that user is not empty, and if it's actually object it checks
227 Checks if that user is not empty, and if it's actually object it checks
228 if he's active.
228 if he's active.
229
229
230 :param user: user object or None
230 :param user: user object or None
231 :return: boolean
231 :return: boolean
232 """
232 """
233 if user is None:
233 if user is None:
234 return False
234 return False
235
235
236 elif user.active:
236 elif user.active:
237 return True
237 return True
238
238
239 return False
239 return False
240
240
241 @property
241 @property
242 def is_shadow_repo_dir(self):
242 def is_shadow_repo_dir(self):
243 return os.path.isdir(self.vcs_repo_name)
243 return os.path.isdir(self.vcs_repo_name)
244
244
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
246 """
246 """
247 Checks permissions using action (push/pull) user and repository
247 Checks permissions using action (push/pull) user and repository
248 name
248 name
249
249
250 :param action: push or pull action
250 :param action: push or pull action
251 :param user: user instance
251 :param user: user instance
252 :param repo_name: repository name
252 :param repo_name: repository name
253 """
253 """
254 # check IP
254 # check IP
255 inherit = user.inherit_default_permissions
255 inherit = user.inherit_default_permissions
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
257 inherit_from_default=inherit)
257 inherit_from_default=inherit)
258 if ip_allowed:
258 if ip_allowed:
259 log.info('Access for IP:%s allowed', ip_addr)
259 log.info('Access for IP:%s allowed', ip_addr)
260 else:
260 else:
261 return False
261 return False
262
262
263 if action == 'push':
263 if action == 'push':
264 if not HasPermissionAnyMiddleware('repository.write',
264 if not HasPermissionAnyMiddleware('repository.write',
265 'repository.admin')(user,
265 'repository.admin')(user,
266 repo_name):
266 repo_name):
267 return False
267 return False
268
268
269 else:
269 else:
270 # any other action need at least read permission
270 # any other action need at least read permission
271 if not HasPermissionAnyMiddleware('repository.read',
271 if not HasPermissionAnyMiddleware('repository.read',
272 'repository.write',
272 'repository.write',
273 'repository.admin')(user,
273 'repository.admin')(user,
274 repo_name):
274 repo_name):
275 return False
275 return False
276
276
277 return True
277 return True
278
278
279 def _check_ssl(self, environ, start_response):
279 def _check_ssl(self, environ, start_response):
280 """
280 """
281 Checks the SSL check flag and returns False if SSL is not present
281 Checks the SSL check flag and returns False if SSL is not present
282 and required True otherwise
282 and required True otherwise
283 """
283 """
284 org_proto = environ['wsgi._org_proto']
284 org_proto = environ['wsgi._org_proto']
285 # check if we have SSL required ! if not it's a bad request !
285 # check if we have SSL required ! if not it's a bad request !
286 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
286 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
287 if require_ssl and org_proto == 'http':
287 if require_ssl and org_proto == 'http':
288 log.debug('proto is %s and SSL is required BAD REQUEST !',
288 log.debug('proto is %s and SSL is required BAD REQUEST !',
289 org_proto)
289 org_proto)
290 return False
290 return False
291 return True
291 return True
292
292
293 def __call__(self, environ, start_response):
293 def __call__(self, environ, start_response):
294 try:
294 try:
295 return self._handle_request(environ, start_response)
295 return self._handle_request(environ, start_response)
296 except Exception:
296 except Exception:
297 log.exception("Exception while handling request")
297 log.exception("Exception while handling request")
298 appenlight.track_exception(environ)
298 appenlight.track_exception(environ)
299 return HTTPInternalServerError()(environ, start_response)
299 return HTTPInternalServerError()(environ, start_response)
300 finally:
300 finally:
301 meta.Session.remove()
301 meta.Session.remove()
302
302
303 def _handle_request(self, environ, start_response):
303 def _handle_request(self, environ, start_response):
304
304
305 if not self._check_ssl(environ, start_response):
305 if not self._check_ssl(environ, start_response):
306 reason = ('SSL required, while RhodeCode was unable '
306 reason = ('SSL required, while RhodeCode was unable '
307 'to detect this as SSL request')
307 'to detect this as SSL request')
308 log.debug('User not allowed to proceed, %s', reason)
308 log.debug('User not allowed to proceed, %s', reason)
309 return HTTPNotAcceptable(reason)(environ, start_response)
309 return HTTPNotAcceptable(reason)(environ, start_response)
310
310
311 if not self.url_repo_name:
311 if not self.url_repo_name:
312 log.warning('Repository name is empty: %s', self.url_repo_name)
312 log.warning('Repository name is empty: %s', self.url_repo_name)
313 # failed to get repo name, we fail now
313 # failed to get repo name, we fail now
314 return HTTPNotFound()(environ, start_response)
314 return HTTPNotFound()(environ, start_response)
315 log.debug('Extracted repo name is %s', self.url_repo_name)
315 log.debug('Extracted repo name is %s', self.url_repo_name)
316
316
317 ip_addr = get_ip_addr(environ)
317 ip_addr = get_ip_addr(environ)
318 user_agent = get_user_agent(environ)
318 user_agent = get_user_agent(environ)
319 username = None
319 username = None
320
320
321 # skip passing error to error controller
321 # skip passing error to error controller
322 environ['pylons.status_code_redirect'] = True
322 environ['pylons.status_code_redirect'] = True
323
323
324 # ======================================================================
324 # ======================================================================
325 # GET ACTION PULL or PUSH
325 # GET ACTION PULL or PUSH
326 # ======================================================================
326 # ======================================================================
327 action = self._get_action(environ)
327 action = self._get_action(environ)
328
328
329 # ======================================================================
329 # ======================================================================
330 # Check if this is a request to a shadow repository of a pull request.
330 # Check if this is a request to a shadow repository of a pull request.
331 # In this case only pull action is allowed.
331 # In this case only pull action is allowed.
332 # ======================================================================
332 # ======================================================================
333 if self.is_shadow_repo and action != 'pull':
333 if self.is_shadow_repo and action != 'pull':
334 reason = 'Only pull action is allowed for shadow repositories.'
334 reason = 'Only pull action is allowed for shadow repositories.'
335 log.debug('User not allowed to proceed, %s', reason)
335 log.debug('User not allowed to proceed, %s', reason)
336 return HTTPNotAcceptable(reason)(environ, start_response)
336 return HTTPNotAcceptable(reason)(environ, start_response)
337
337
338 # Check if the shadow repo actually exists, in case someone refers
338 # Check if the shadow repo actually exists, in case someone refers
339 # to it, and it has been deleted because of successful merge.
339 # to it, and it has been deleted because of successful merge.
340 if self.is_shadow_repo and not self.is_shadow_repo_dir:
340 if self.is_shadow_repo and not self.is_shadow_repo_dir:
341 return HTTPNotFound()(environ, start_response)
341 return HTTPNotFound()(environ, start_response)
342
342
343 # ======================================================================
343 # ======================================================================
344 # CHECK ANONYMOUS PERMISSION
344 # CHECK ANONYMOUS PERMISSION
345 # ======================================================================
345 # ======================================================================
346 if action in ['pull', 'push']:
346 if action in ['pull', 'push']:
347 anonymous_user = User.get_default_user()
347 anonymous_user = User.get_default_user()
348 username = anonymous_user.username
348 username = anonymous_user.username
349 if anonymous_user.active:
349 if anonymous_user.active:
350 # ONLY check permissions if the user is activated
350 # ONLY check permissions if the user is activated
351 anonymous_perm = self._check_permission(
351 anonymous_perm = self._check_permission(
352 action, anonymous_user, self.acl_repo_name, ip_addr)
352 action, anonymous_user, self.acl_repo_name, ip_addr)
353 else:
353 else:
354 anonymous_perm = False
354 anonymous_perm = False
355
355
356 if not anonymous_user.active or not anonymous_perm:
356 if not anonymous_user.active or not anonymous_perm:
357 if not anonymous_user.active:
357 if not anonymous_user.active:
358 log.debug('Anonymous access is disabled, running '
358 log.debug('Anonymous access is disabled, running '
359 'authentication')
359 'authentication')
360
360
361 if not anonymous_perm:
361 if not anonymous_perm:
362 log.debug('Not enough credentials to access this '
362 log.debug('Not enough credentials to access this '
363 'repository as anonymous user')
363 'repository as anonymous user')
364
364
365 username = None
365 username = None
366 # ==============================================================
366 # ==============================================================
367 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
367 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
368 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
368 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
369 # ==============================================================
369 # ==============================================================
370
370
371 # try to auth based on environ, container auth methods
371 # try to auth based on environ, container auth methods
372 log.debug('Running PRE-AUTH for container based authentication')
372 log.debug('Running PRE-AUTH for container based authentication')
373 pre_auth = authenticate(
373 pre_auth = authenticate(
374 '', '', environ, VCS_TYPE, registry=self.registry,
374 '', '', environ, VCS_TYPE, registry=self.registry,
375 acl_repo_name=self.acl_repo_name)
375 acl_repo_name=self.acl_repo_name)
376 if pre_auth and pre_auth.get('username'):
376 if pre_auth and pre_auth.get('username'):
377 username = pre_auth['username']
377 username = pre_auth['username']
378 log.debug('PRE-AUTH got %s as username', username)
378 log.debug('PRE-AUTH got %s as username', username)
379
379
380 # If not authenticated by the container, running basic auth
380 # If not authenticated by the container, running basic auth
381 # before inject the calling repo_name for special scope checks
381 # before inject the calling repo_name for special scope checks
382 self.authenticate.acl_repo_name = self.acl_repo_name
382 self.authenticate.acl_repo_name = self.acl_repo_name
383 if not username:
383 if not username:
384 self.authenticate.realm = get_rhodecode_realm()
384 self.authenticate.realm = self.authenticate.get_rc_realm()
385
385
386 try:
386 try:
387 result = self.authenticate(environ)
387 result = self.authenticate(environ)
388 except (UserCreationError, NotAllowedToCreateUserError) as e:
388 except (UserCreationError, NotAllowedToCreateUserError) as e:
389 log.error(e)
389 log.error(e)
390 reason = safe_str(e)
390 reason = safe_str(e)
391 return HTTPNotAcceptable(reason)(environ, start_response)
391 return HTTPNotAcceptable(reason)(environ, start_response)
392
392
393 if isinstance(result, str):
393 if isinstance(result, str):
394 AUTH_TYPE.update(environ, 'basic')
394 AUTH_TYPE.update(environ, 'basic')
395 REMOTE_USER.update(environ, result)
395 REMOTE_USER.update(environ, result)
396 username = result
396 username = result
397 else:
397 else:
398 return result.wsgi_application(environ, start_response)
398 return result.wsgi_application(environ, start_response)
399
399
400 # ==============================================================
400 # ==============================================================
401 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
401 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
402 # ==============================================================
402 # ==============================================================
403 user = User.get_by_username(username)
403 user = User.get_by_username(username)
404 if not self.valid_and_active_user(user):
404 if not self.valid_and_active_user(user):
405 return HTTPForbidden()(environ, start_response)
405 return HTTPForbidden()(environ, start_response)
406 username = user.username
406 username = user.username
407 user.update_lastactivity()
407 user.update_lastactivity()
408 meta.Session().commit()
408 meta.Session().commit()
409
409
410 # check user attributes for password change flag
410 # check user attributes for password change flag
411 user_obj = user
411 user_obj = user
412 if user_obj and user_obj.username != User.DEFAULT_USER and \
412 if user_obj and user_obj.username != User.DEFAULT_USER and \
413 user_obj.user_data.get('force_password_change'):
413 user_obj.user_data.get('force_password_change'):
414 reason = 'password change required'
414 reason = 'password change required'
415 log.debug('User not allowed to authenticate, %s', reason)
415 log.debug('User not allowed to authenticate, %s', reason)
416 return HTTPNotAcceptable(reason)(environ, start_response)
416 return HTTPNotAcceptable(reason)(environ, start_response)
417
417
418 # check permissions for this repository
418 # check permissions for this repository
419 perm = self._check_permission(
419 perm = self._check_permission(
420 action, user, self.acl_repo_name, ip_addr)
420 action, user, self.acl_repo_name, ip_addr)
421 if not perm:
421 if not perm:
422 return HTTPForbidden()(environ, start_response)
422 return HTTPForbidden()(environ, start_response)
423
423
424 # extras are injected into UI object and later available
424 # extras are injected into UI object and later available
425 # in hooks executed by rhodecode
425 # in hooks executed by rhodecode
426 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
426 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
427 extras = vcs_operation_context(
427 extras = vcs_operation_context(
428 environ, repo_name=self.acl_repo_name, username=username,
428 environ, repo_name=self.acl_repo_name, username=username,
429 action=action, scm=self.SCM, check_locking=check_locking,
429 action=action, scm=self.SCM, check_locking=check_locking,
430 is_shadow_repo=self.is_shadow_repo
430 is_shadow_repo=self.is_shadow_repo
431 )
431 )
432
432
433 # ======================================================================
433 # ======================================================================
434 # REQUEST HANDLING
434 # REQUEST HANDLING
435 # ======================================================================
435 # ======================================================================
436 repo_path = os.path.join(
436 repo_path = os.path.join(
437 safe_str(self.basepath), safe_str(self.vcs_repo_name))
437 safe_str(self.basepath), safe_str(self.vcs_repo_name))
438 log.debug('Repository path is %s', repo_path)
438 log.debug('Repository path is %s', repo_path)
439
439
440 fix_PATH()
440 fix_PATH()
441
441
442 log.info(
442 log.info(
443 '%s action on %s repo "%s" by "%s" from %s %s',
443 '%s action on %s repo "%s" by "%s" from %s %s',
444 action, self.SCM, safe_str(self.url_repo_name),
444 action, self.SCM, safe_str(self.url_repo_name),
445 safe_str(username), ip_addr, user_agent)
445 safe_str(username), ip_addr, user_agent)
446
446
447 return self._generate_vcs_response(
447 return self._generate_vcs_response(
448 environ, start_response, repo_path, extras, action)
448 environ, start_response, repo_path, extras, action)
449
449
450 @initialize_generator
450 @initialize_generator
451 def _generate_vcs_response(
451 def _generate_vcs_response(
452 self, environ, start_response, repo_path, extras, action):
452 self, environ, start_response, repo_path, extras, action):
453 """
453 """
454 Returns a generator for the response content.
454 Returns a generator for the response content.
455
455
456 This method is implemented as a generator, so that it can trigger
456 This method is implemented as a generator, so that it can trigger
457 the cache validation after all content sent back to the client. It
457 the cache validation after all content sent back to the client. It
458 also handles the locking exceptions which will be triggered when
458 also handles the locking exceptions which will be triggered when
459 the first chunk is produced by the underlying WSGI application.
459 the first chunk is produced by the underlying WSGI application.
460 """
460 """
461 callback_daemon, extras = self._prepare_callback_daemon(extras)
461 callback_daemon, extras = self._prepare_callback_daemon(extras)
462 config = self._create_config(extras, self.acl_repo_name)
462 config = self._create_config(extras, self.acl_repo_name)
463 log.debug('HOOKS extras is %s', extras)
463 log.debug('HOOKS extras is %s', extras)
464 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
464 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
465
465
466 try:
466 try:
467 with callback_daemon:
467 with callback_daemon:
468 try:
468 try:
469 response = app(environ, start_response)
469 response = app(environ, start_response)
470 finally:
470 finally:
471 # This statement works together with the decorator
471 # This statement works together with the decorator
472 # "initialize_generator" above. The decorator ensures that
472 # "initialize_generator" above. The decorator ensures that
473 # we hit the first yield statement before the generator is
473 # we hit the first yield statement before the generator is
474 # returned back to the WSGI server. This is needed to
474 # returned back to the WSGI server. This is needed to
475 # ensure that the call to "app" above triggers the
475 # ensure that the call to "app" above triggers the
476 # needed callback to "start_response" before the
476 # needed callback to "start_response" before the
477 # generator is actually used.
477 # generator is actually used.
478 yield "__init__"
478 yield "__init__"
479
479
480 for chunk in response:
480 for chunk in response:
481 yield chunk
481 yield chunk
482 except Exception as exc:
482 except Exception as exc:
483 # TODO: martinb: Exceptions are only raised in case of the Pyro4
483 # TODO: martinb: Exceptions are only raised in case of the Pyro4
484 # backend. Refactor this except block after dropping Pyro4 support.
484 # backend. Refactor this except block after dropping Pyro4 support.
485 # TODO: johbo: Improve "translating" back the exception.
485 # TODO: johbo: Improve "translating" back the exception.
486 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
486 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
487 exc = HTTPLockedRC(*exc.args)
487 exc = HTTPLockedRC(*exc.args)
488 _code = rhodecode.CONFIG.get('lock_ret_code')
488 _code = rhodecode.CONFIG.get('lock_ret_code')
489 log.debug('Repository LOCKED ret code %s!', (_code,))
489 log.debug('Repository LOCKED ret code %s!', (_code,))
490 elif getattr(exc, '_vcs_kind', None) == 'requirement':
490 elif getattr(exc, '_vcs_kind', None) == 'requirement':
491 log.debug(
491 log.debug(
492 'Repository requires features unknown to this Mercurial')
492 'Repository requires features unknown to this Mercurial')
493 exc = HTTPRequirementError(*exc.args)
493 exc = HTTPRequirementError(*exc.args)
494 else:
494 else:
495 raise
495 raise
496
496
497 for chunk in exc(environ, start_response):
497 for chunk in exc(environ, start_response):
498 yield chunk
498 yield chunk
499 finally:
499 finally:
500 # invalidate cache on push
500 # invalidate cache on push
501 try:
501 try:
502 if action == 'push':
502 if action == 'push':
503 self._invalidate_cache(self.url_repo_name)
503 self._invalidate_cache(self.url_repo_name)
504 finally:
504 finally:
505 meta.Session.remove()
505 meta.Session.remove()
506
506
507 def _get_repository_name(self, environ):
507 def _get_repository_name(self, environ):
508 """Get repository name out of the environmnent
508 """Get repository name out of the environmnent
509
509
510 :param environ: WSGI environment
510 :param environ: WSGI environment
511 """
511 """
512 raise NotImplementedError()
512 raise NotImplementedError()
513
513
514 def _get_action(self, environ):
514 def _get_action(self, environ):
515 """Map request commands into a pull or push command.
515 """Map request commands into a pull or push command.
516
516
517 :param environ: WSGI environment
517 :param environ: WSGI environment
518 """
518 """
519 raise NotImplementedError()
519 raise NotImplementedError()
520
520
521 def _create_wsgi_app(self, repo_path, repo_name, config):
521 def _create_wsgi_app(self, repo_path, repo_name, config):
522 """Return the WSGI app that will finally handle the request."""
522 """Return the WSGI app that will finally handle the request."""
523 raise NotImplementedError()
523 raise NotImplementedError()
524
524
525 def _create_config(self, extras, repo_name):
525 def _create_config(self, extras, repo_name):
526 """Create a safe config representation."""
526 """Create a safe config representation."""
527 raise NotImplementedError()
527 raise NotImplementedError()
528
528
529 def _prepare_callback_daemon(self, extras):
529 def _prepare_callback_daemon(self, extras):
530 return prepare_callback_daemon(
530 return prepare_callback_daemon(
531 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
531 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
532 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
532 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
533
533
534
534
535 def _should_check_locking(query_string):
535 def _should_check_locking(query_string):
536 # this is kind of hacky, but due to how mercurial handles client-server
536 # this is kind of hacky, but due to how mercurial handles client-server
537 # server see all operation on commit; bookmarks, phases and
537 # server see all operation on commit; bookmarks, phases and
538 # obsolescence marker in different transaction, we don't want to check
538 # obsolescence marker in different transaction, we don't want to check
539 # locking on those
539 # locking on those
540 return query_string not in ['cmd=listkeys']
540 return query_string not in ['cmd=listkeys']
@@ -1,142 +1,142 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 import urlparse
22 import urlparse
23 import mock
23 import mock
24 import simplejson as json
24 import simplejson as json
25
25
26 from rhodecode.lib.vcs.backends.base import Config
26 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.tests.lib.middleware import mock_scm_app
27 from rhodecode.tests.lib.middleware import mock_scm_app
28 import rhodecode.lib.middleware.simplegit as simplegit
28 import rhodecode.lib.middleware.simplegit as simplegit
29
29
30
30
31 def get_environ(url, request_method):
31 def get_environ(url, request_method):
32 """Construct a minimum WSGI environ based on the URL."""
32 """Construct a minimum WSGI environ based on the URL."""
33 parsed_url = urlparse.urlparse(url)
33 parsed_url = urlparse.urlparse(url)
34 environ = {
34 environ = {
35 'PATH_INFO': parsed_url.path,
35 'PATH_INFO': parsed_url.path,
36 'QUERY_STRING': parsed_url.query,
36 'QUERY_STRING': parsed_url.query,
37 'REQUEST_METHOD': request_method,
37 'REQUEST_METHOD': request_method,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action, request_method',
44 'url, expected_action, request_method',
45 [
45 [
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
50 # Edge case: missing data for info/refs
50 # Edge case: missing data for info/refs
51 ('/foo/info/refs?service=', 'pull', 'GET'),
51 ('/foo/info/refs?service=', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
53 # Edge case: git command comes with service argument
53 # Edge case: git command comes with service argument
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
56 # Edge case: repo name conflicts with git commands
56 # Edge case: repo name conflicts with git commands
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
62 # Edge case: not a smart protocol url
62 # Edge case: not a smart protocol url
63 ('/foo/bar', 'pull', 'GET'),
63 ('/foo/bar', 'pull', 'GET'),
64 # GIT LFS cases, batch
64 # GIT LFS cases, batch
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
67 # GIT LFS oid, dl/upl
67 # GIT LFS oid, dl/upl
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
71 # Edge case: repo name conflicts with git commands
71 # Edge case: repo name conflicts with git commands
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
74
74
75 ])
75 ])
76 def test_get_action(url, expected_action, request_method, pylonsapp):
76 def test_get_action(url, expected_action, request_method, pylonsapp, request_stub):
77 app = simplegit.SimpleGit(application=None,
77 app = simplegit.SimpleGit(application=None,
78 config={'auth_ret_code': '', 'base_path': ''},
78 config={'auth_ret_code': '', 'base_path': ''},
79 registry=None)
79 registry=request_stub.registry)
80 assert expected_action == app._get_action(get_environ(url, request_method))
80 assert expected_action == app._get_action(get_environ(url, request_method))
81
81
82
82
83 @pytest.mark.parametrize(
83 @pytest.mark.parametrize(
84 'url, expected_repo_name, request_method',
84 'url, expected_repo_name, request_method',
85 [
85 [
86 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
86 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
87 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
87 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
88 ('/foo/git-upload-pack', 'foo', 'GET'),
88 ('/foo/git-upload-pack', 'foo', 'GET'),
89 ('/foo/git-receive-pack', 'foo', 'GET'),
89 ('/foo/git-receive-pack', 'foo', 'GET'),
90 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
91 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
91 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
92
92
93 # GIT LFS cases, batch
93 # GIT LFS cases, batch
94 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
94 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
95 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
95 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
96 # GIT LFS oid, dl/upl
96 # GIT LFS oid, dl/upl
97 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
97 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
98 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
98 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
99 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
99 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
100 # Edge case: repo name conflicts with git commands
100 # Edge case: repo name conflicts with git commands
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
102 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
102 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
103
103
104 ])
104 ])
105 def test_get_repository_name(url, expected_repo_name, request_method, pylonsapp):
105 def test_get_repository_name(url, expected_repo_name, request_method, pylonsapp, request_stub):
106 app = simplegit.SimpleGit(application=None,
106 app = simplegit.SimpleGit(application=None,
107 config={'auth_ret_code': '', 'base_path': ''},
107 config={'auth_ret_code': '', 'base_path': ''},
108 registry=None)
108 registry=request_stub.registry)
109 assert expected_repo_name == app._get_repository_name(
109 assert expected_repo_name == app._get_repository_name(
110 get_environ(url, request_method))
110 get_environ(url, request_method))
111
111
112
112
113 def test_get_config(pylonsapp, user_util):
113 def test_get_config(user_util, pylonsapp, request_stub):
114 repo = user_util.create_repo(repo_type='git')
114 repo = user_util.create_repo(repo_type='git')
115 app = simplegit.SimpleGit(application=None,
115 app = simplegit.SimpleGit(application=None,
116 config={'auth_ret_code': '', 'base_path': ''},
116 config={'auth_ret_code': '', 'base_path': ''},
117 registry=None)
117 registry=request_stub.registry)
118 extras = {'foo': 'FOO', 'bar': 'BAR'}
118 extras = {'foo': 'FOO', 'bar': 'BAR'}
119
119
120 # We copy the extras as the method below will change the contents.
120 # We copy the extras as the method below will change the contents.
121 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
121 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
122
122
123 expected_config = dict(extras)
123 expected_config = dict(extras)
124 expected_config.update({
124 expected_config.update({
125 'git_update_server_info': False,
125 'git_update_server_info': False,
126 'git_lfs_enabled': False,
126 'git_lfs_enabled': False,
127 'git_lfs_store_path': git_config['git_lfs_store_path']
127 'git_lfs_store_path': git_config['git_lfs_store_path']
128 })
128 })
129
129
130 assert git_config == expected_config
130 assert git_config == expected_config
131
131
132
132
133 def test_create_wsgi_app_uses_scm_app_from_simplevcs(pylonsapp):
133 def test_create_wsgi_app_uses_scm_app_from_simplevcs(pylonsapp, request_stub):
134 config = {
134 config = {
135 'auth_ret_code': '',
135 'auth_ret_code': '',
136 'base_path': '',
136 'base_path': '',
137 'vcs.scm_app_implementation':
137 'vcs.scm_app_implementation':
138 'rhodecode.tests.lib.middleware.mock_scm_app',
138 'rhodecode.tests.lib.middleware.mock_scm_app',
139 }
139 }
140 app = simplegit.SimpleGit(application=None, config=config, registry=None)
140 app = simplegit.SimpleGit(application=None, config=config, registry=request_stub.registry)
141 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
141 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
142 assert wsgi_app is mock_scm_app.mock_git_wsgi
142 assert wsgi_app is mock_scm_app.mock_git_wsgi
@@ -1,128 +1,129 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import simplejson as json
25 import simplejson as json
26
26
27 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.lib.vcs.backends.base import Config
28 from rhodecode.tests.lib.middleware import mock_scm_app
28 from rhodecode.tests.lib.middleware import mock_scm_app
29 import rhodecode.lib.middleware.simplehg as simplehg
29 import rhodecode.lib.middleware.simplehg as simplehg
30
30
31
31
32 def get_environ(url):
32 def get_environ(url):
33 """Construct a minimum WSGI environ based on the URL."""
33 """Construct a minimum WSGI environ based on the URL."""
34 parsed_url = urlparse.urlparse(url)
34 parsed_url = urlparse.urlparse(url)
35 environ = {
35 environ = {
36 'PATH_INFO': parsed_url.path,
36 'PATH_INFO': parsed_url.path,
37 'QUERY_STRING': parsed_url.query,
37 'QUERY_STRING': parsed_url.query,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action',
44 'url, expected_action',
45 [
45 [
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
50 # Edge case: unknown argument: assume pull
50 # Edge case: unknown argument: assume pull
51 ('/foo/bar?cmd=unknown&key=tip', 'pull'),
51 ('/foo/bar?cmd=unknown&key=tip', 'pull'),
52 ('/foo/bar?cmd=&key=tip', 'pull'),
52 ('/foo/bar?cmd=&key=tip', 'pull'),
53 # Edge case: not cmd argument
53 # Edge case: not cmd argument
54 ('/foo/bar?key=tip', 'pull'),
54 ('/foo/bar?key=tip', 'pull'),
55 ])
55 ])
56 def test_get_action(url, expected_action):
56 def test_get_action(url, expected_action, request_stub):
57 app = simplehg.SimpleHg(application=None,
57 app = simplehg.SimpleHg(application=None,
58 config={'auth_ret_code': '', 'base_path': ''},
58 config={'auth_ret_code': '', 'base_path': ''},
59 registry=None)
59 registry=request_stub.registry)
60 assert expected_action == app._get_action(get_environ(url))
60 assert expected_action == app._get_action(get_environ(url))
61
61
62
62
63 @pytest.mark.parametrize(
63 @pytest.mark.parametrize(
64 'url, expected_repo_name',
64 'url, expected_repo_name',
65 [
65 [
66 ('/foo?cmd=unbundle&key=tip', 'foo'),
66 ('/foo?cmd=unbundle&key=tip', 'foo'),
67 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
67 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
68 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
68 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
69 # Repos with trailing slashes.
69 # Repos with trailing slashes.
70 ('/foo/?cmd=unbundle&key=tip', 'foo'),
70 ('/foo/?cmd=unbundle&key=tip', 'foo'),
71 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
71 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
72 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
72 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
73 ])
73 ])
74 def test_get_repository_name(url, expected_repo_name):
74 def test_get_repository_name(url, expected_repo_name, request_stub):
75 app = simplehg.SimpleHg(application=None,
75 app = simplehg.SimpleHg(application=None,
76 config={'auth_ret_code': '', 'base_path': ''},
76 config={'auth_ret_code': '', 'base_path': ''},
77 registry=None)
77 registry=request_stub.registry)
78 assert expected_repo_name == app._get_repository_name(get_environ(url))
78 assert expected_repo_name == app._get_repository_name(get_environ(url))
79
79
80
80
81 def test_get_config(pylonsapp, user_util):
81 def test_get_config(user_util, pylonsapp, request_stub):
82 repo = user_util.create_repo(repo_type='git')
82 repo = user_util.create_repo(repo_type='git')
83 app = simplehg.SimpleHg(application=None,
83 app = simplehg.SimpleHg(application=None,
84 config={'auth_ret_code': '', 'base_path': ''},
84 config={'auth_ret_code': '', 'base_path': ''},
85 registry=None)
85 registry=request_stub.registry)
86 extras = [('foo', 'FOO', 'bar', 'BAR')]
86 extras = [('foo', 'FOO', 'bar', 'BAR')]
87
87
88 hg_config = app._create_config(extras, repo_name=repo.repo_name)
88 hg_config = app._create_config(extras, repo_name=repo.repo_name)
89
89
90 config = simplehg.utils.make_db_config(repo=repo.repo_name)
90 config = simplehg.utils.make_db_config(repo=repo.repo_name)
91 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
91 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
92 hg_config_org = config
92 hg_config_org = config
93
93
94 expected_config = [
94 expected_config = [
95 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
95 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
96 ('web', 'push_ssl', 'False'),
96 ('web', 'push_ssl', 'False'),
97 ('web', 'allow_push', '*'),
97 ('web', 'allow_push', '*'),
98 ('web', 'allow_archive', 'gz zip bz2'),
98 ('web', 'allow_archive', 'gz zip bz2'),
99 ('web', 'baseurl', '/'),
99 ('web', 'baseurl', '/'),
100 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
100 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
101 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
101 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
102 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
102 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
103 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
103 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
104 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
104 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
105 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
105 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
106 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
106 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
107 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
107 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
108 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
108 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
109 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
109 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
110 ('phases', 'publish', 'True'),
110 ('phases', 'publish', 'True'),
111 ('extensions', 'largefiles', ''),
111 ('extensions', 'largefiles', ''),
112 ('paths', '/', hg_config_org.get('paths', '/')),
112 ('paths', '/', hg_config_org.get('paths', '/')),
113 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
113 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
114 ]
114 ]
115 for entry in expected_config:
115 for entry in expected_config:
116 assert entry in hg_config
116 assert entry in hg_config
117
117
118
118
119 def test_create_wsgi_app_uses_scm_app_from_simplevcs():
119 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
120 config = {
120 config = {
121 'auth_ret_code': '',
121 'auth_ret_code': '',
122 'base_path': '',
122 'base_path': '',
123 'vcs.scm_app_implementation':
123 'vcs.scm_app_implementation':
124 'rhodecode.tests.lib.middleware.mock_scm_app',
124 'rhodecode.tests.lib.middleware.mock_scm_app',
125 }
125 }
126 app = simplehg.SimpleHg(application=None, config=config, registry=None)
126 app = simplehg.SimpleHg(
127 application=None, config=config, registry=request_stub.registry)
127 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
128 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
128 assert wsgi_app is mock_scm_app.mock_hg_wsgi
129 assert wsgi_app is mock_scm_app.mock_hg_wsgi
@@ -1,201 +1,201 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 from StringIO import StringIO
21 from StringIO import StringIO
22
22
23 import pytest
23 import pytest
24 from mock import patch, Mock
24 from mock import patch, Mock
25
25
26 import rhodecode
26 import rhodecode
27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
28
28
29
29
30 class TestSimpleSvn(object):
30 class TestSimpleSvn(object):
31 @pytest.fixture(autouse=True)
31 @pytest.fixture(autouse=True)
32 def simple_svn(self, pylonsapp):
32 def simple_svn(self, pylonsapp, request_stub):
33 self.app = SimpleSvn(
33 self.app = SimpleSvn(
34 application='None',
34 application='None',
35 config={'auth_ret_code': '',
35 config={'auth_ret_code': '',
36 'base_path': rhodecode.CONFIG['base_path']},
36 'base_path': rhodecode.CONFIG['base_path']},
37 registry=None)
37 registry=request_stub.registry)
38
38
39 def test_get_config(self):
39 def test_get_config(self):
40 extras = {'foo': 'FOO', 'bar': 'BAR'}
40 extras = {'foo': 'FOO', 'bar': 'BAR'}
41 config = self.app._create_config(extras, repo_name='test-repo')
41 config = self.app._create_config(extras, repo_name='test-repo')
42 assert config == extras
42 assert config == extras
43
43
44 @pytest.mark.parametrize(
44 @pytest.mark.parametrize(
45 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
45 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
46 def test_get_action_returns_pull(self, method):
46 def test_get_action_returns_pull(self, method):
47 environment = {'REQUEST_METHOD': method}
47 environment = {'REQUEST_METHOD': method}
48 action = self.app._get_action(environment)
48 action = self.app._get_action(environment)
49 assert action == 'pull'
49 assert action == 'pull'
50
50
51 @pytest.mark.parametrize(
51 @pytest.mark.parametrize(
52 'method', [
52 'method', [
53 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
53 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
54 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
54 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
55 ])
55 ])
56 def test_get_action_returns_push(self, method):
56 def test_get_action_returns_push(self, method):
57 environment = {'REQUEST_METHOD': method}
57 environment = {'REQUEST_METHOD': method}
58 action = self.app._get_action(environment)
58 action = self.app._get_action(environment)
59 assert action == 'push'
59 assert action == 'push'
60
60
61 @pytest.mark.parametrize(
61 @pytest.mark.parametrize(
62 'path, expected_name', [
62 'path, expected_name', [
63 ('/hello-svn', 'hello-svn'),
63 ('/hello-svn', 'hello-svn'),
64 ('/hello-svn/', 'hello-svn'),
64 ('/hello-svn/', 'hello-svn'),
65 ('/group/hello-svn/', 'group/hello-svn'),
65 ('/group/hello-svn/', 'group/hello-svn'),
66 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
66 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
67 ])
67 ])
68 def test_get_repository_name(self, path, expected_name):
68 def test_get_repository_name(self, path, expected_name):
69 environment = {'PATH_INFO': path}
69 environment = {'PATH_INFO': path}
70 name = self.app._get_repository_name(environment)
70 name = self.app._get_repository_name(environment)
71 assert name == expected_name
71 assert name == expected_name
72
72
73 def test_get_repository_name_subfolder(self, backend_svn):
73 def test_get_repository_name_subfolder(self, backend_svn):
74 repo = backend_svn.repo
74 repo = backend_svn.repo
75 environment = {
75 environment = {
76 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
76 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
77 name = self.app._get_repository_name(environment)
77 name = self.app._get_repository_name(environment)
78 assert name == repo.repo_name
78 assert name == repo.repo_name
79
79
80 def test_create_wsgi_app(self):
80 def test_create_wsgi_app(self):
81 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
81 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
82 mock_method.return_value = False
82 mock_method.return_value = False
83 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
83 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
84 wsgi_app_mock):
84 wsgi_app_mock):
85 config = Mock()
85 config = Mock()
86 wsgi_app = self.app._create_wsgi_app(
86 wsgi_app = self.app._create_wsgi_app(
87 repo_path='', repo_name='', config=config)
87 repo_path='', repo_name='', config=config)
88
88
89 wsgi_app_mock.assert_called_once_with(config)
89 wsgi_app_mock.assert_called_once_with(config)
90 assert wsgi_app == wsgi_app_mock()
90 assert wsgi_app == wsgi_app_mock()
91
91
92 def test_create_wsgi_app_when_enabled(self):
92 def test_create_wsgi_app_when_enabled(self):
93 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
93 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
94 mock_method.return_value = True
94 mock_method.return_value = True
95 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
95 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
96 wsgi_app_mock):
96 wsgi_app_mock):
97 config = Mock()
97 config = Mock()
98 wsgi_app = self.app._create_wsgi_app(
98 wsgi_app = self.app._create_wsgi_app(
99 repo_path='', repo_name='', config=config)
99 repo_path='', repo_name='', config=config)
100
100
101 wsgi_app_mock.assert_called_once_with(config)
101 wsgi_app_mock.assert_called_once_with(config)
102 assert wsgi_app == wsgi_app_mock()
102 assert wsgi_app == wsgi_app_mock()
103
103
104
104
105
105
106 class TestSimpleSvnApp(object):
106 class TestSimpleSvnApp(object):
107 data = '<xml></xml>'
107 data = '<xml></xml>'
108 path = '/group/my-repo'
108 path = '/group/my-repo'
109 wsgi_input = StringIO(data)
109 wsgi_input = StringIO(data)
110 environment = {
110 environment = {
111 'HTTP_DAV': (
111 'HTTP_DAV': (
112 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
112 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
113 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
113 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
114 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
114 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
115 'REQUEST_METHOD': 'OPTIONS',
115 'REQUEST_METHOD': 'OPTIONS',
116 'PATH_INFO': path,
116 'PATH_INFO': path,
117 'wsgi.input': wsgi_input,
117 'wsgi.input': wsgi_input,
118 'CONTENT_TYPE': 'text/xml',
118 'CONTENT_TYPE': 'text/xml',
119 'CONTENT_LENGTH': '130'
119 'CONTENT_LENGTH': '130'
120 }
120 }
121
121
122 def setup_method(self, method):
122 def setup_method(self, method):
123 self.host = 'http://localhost/'
123 self.host = 'http://localhost/'
124 self.app = SimpleSvnApp(
124 self.app = SimpleSvnApp(
125 config={'subversion_http_server_url': self.host})
125 config={'subversion_http_server_url': self.host})
126
126
127 def test_get_request_headers_with_content_type(self):
127 def test_get_request_headers_with_content_type(self):
128 expected_headers = {
128 expected_headers = {
129 'Dav': self.environment['HTTP_DAV'],
129 'Dav': self.environment['HTTP_DAV'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
132 'Content-Length': self.environment['CONTENT_LENGTH']
132 'Content-Length': self.environment['CONTENT_LENGTH']
133 }
133 }
134 headers = self.app._get_request_headers(self.environment)
134 headers = self.app._get_request_headers(self.environment)
135 assert headers == expected_headers
135 assert headers == expected_headers
136
136
137 def test_get_request_headers_without_content_type(self):
137 def test_get_request_headers_without_content_type(self):
138 environment = self.environment.copy()
138 environment = self.environment.copy()
139 environment.pop('CONTENT_TYPE')
139 environment.pop('CONTENT_TYPE')
140 expected_headers = {
140 expected_headers = {
141 'Dav': environment['HTTP_DAV'],
141 'Dav': environment['HTTP_DAV'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
144 }
144 }
145 request_headers = self.app._get_request_headers(environment)
145 request_headers = self.app._get_request_headers(environment)
146 assert request_headers == expected_headers
146 assert request_headers == expected_headers
147
147
148 def test_get_response_headers(self):
148 def test_get_response_headers(self):
149 headers = {
149 headers = {
150 'Connection': 'keep-alive',
150 'Connection': 'keep-alive',
151 'Keep-Alive': 'timeout=5, max=100',
151 'Keep-Alive': 'timeout=5, max=100',
152 'Transfer-Encoding': 'chunked',
152 'Transfer-Encoding': 'chunked',
153 'Content-Encoding': 'gzip',
153 'Content-Encoding': 'gzip',
154 'MS-Author-Via': 'DAV',
154 'MS-Author-Via': 'DAV',
155 'SVN-Supported-Posts': 'create-txn-with-props'
155 'SVN-Supported-Posts': 'create-txn-with-props'
156 }
156 }
157 expected_headers = [
157 expected_headers = [
158 ('MS-Author-Via', 'DAV'),
158 ('MS-Author-Via', 'DAV'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
160 ]
160 ]
161 response_headers = self.app._get_response_headers(headers)
161 response_headers = self.app._get_response_headers(headers)
162 assert sorted(response_headers) == sorted(expected_headers)
162 assert sorted(response_headers) == sorted(expected_headers)
163
163
164 def test_get_url(self):
164 def test_get_url(self):
165 url = self.app._get_url(self.path)
165 url = self.app._get_url(self.path)
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
167 assert url == expected_url
167 assert url == expected_url
168
168
169 def test_call(self):
169 def test_call(self):
170 start_response = Mock()
170 start_response = Mock()
171 response_mock = Mock()
171 response_mock = Mock()
172 response_mock.headers = {
172 response_mock.headers = {
173 'Content-Encoding': 'gzip',
173 'Content-Encoding': 'gzip',
174 'MS-Author-Via': 'DAV',
174 'MS-Author-Via': 'DAV',
175 'SVN-Supported-Posts': 'create-txn-with-props'
175 'SVN-Supported-Posts': 'create-txn-with-props'
176 }
176 }
177 response_mock.status_code = 200
177 response_mock.status_code = 200
178 response_mock.reason = 'OK'
178 response_mock.reason = 'OK'
179 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
179 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
180 request_mock):
180 request_mock):
181 request_mock.return_value = response_mock
181 request_mock.return_value = response_mock
182 self.app(self.environment, start_response)
182 self.app(self.environment, start_response)
183
183
184 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
184 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
185 expected_request_headers = {
185 expected_request_headers = {
186 'Dav': self.environment['HTTP_DAV'],
186 'Dav': self.environment['HTTP_DAV'],
187 'User-Agent': self.environment['HTTP_USER_AGENT'],
187 'User-Agent': self.environment['HTTP_USER_AGENT'],
188 'Content-Type': self.environment['CONTENT_TYPE'],
188 'Content-Type': self.environment['CONTENT_TYPE'],
189 'Content-Length': self.environment['CONTENT_LENGTH']
189 'Content-Length': self.environment['CONTENT_LENGTH']
190 }
190 }
191 expected_response_headers = [
191 expected_response_headers = [
192 ('SVN-Supported-Posts', 'create-txn-with-props'),
192 ('SVN-Supported-Posts', 'create-txn-with-props'),
193 ('MS-Author-Via', 'DAV'),
193 ('MS-Author-Via', 'DAV'),
194 ]
194 ]
195 request_mock.assert_called_once_with(
195 request_mock.assert_called_once_with(
196 self.environment['REQUEST_METHOD'], expected_url,
196 self.environment['REQUEST_METHOD'], expected_url,
197 data=self.data, headers=expected_request_headers)
197 data=self.data, headers=expected_request_headers)
198 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
198 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
199 args, _ = start_response.call_args
199 args, _ = start_response.call_args
200 assert args[0] == '200 OK'
200 assert args[0] == '200 OK'
201 assert sorted(args[1]) == sorted(expected_response_headers)
201 assert sorted(args[1]) == sorted(expected_response_headers)
@@ -1,485 +1,498 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.tests.utils import CustomTestApp
27 from rhodecode.tests.utils import CustomTestApp
27
28
28 from rhodecode.lib.caching_query import FromCache
29 from rhodecode.lib.caching_query import FromCache
29 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
30 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware.https_fixup import HttpsFixup
32 from rhodecode.lib.middleware.https_fixup import HttpsFixup
32 from rhodecode.lib.middleware.utils import scm_app_http
33 from rhodecode.lib.middleware.utils import scm_app_http
33 from rhodecode.model.db import User, _hash_key
34 from rhodecode.model.db import User, _hash_key
34 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
35 from rhodecode.tests import (
36 from rhodecode.tests import (
36 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
37 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
37 from rhodecode.tests.lib.middleware import mock_scm_app
38 from rhodecode.tests.lib.middleware import mock_scm_app
38
39
39
40
40 class StubVCSController(simplevcs.SimpleVCS):
41 class StubVCSController(simplevcs.SimpleVCS):
41
42
42 SCM = 'hg'
43 SCM = 'hg'
43 stub_response_body = tuple()
44 stub_response_body = tuple()
44
45
45 def __init__(self, *args, **kwargs):
46 def __init__(self, *args, **kwargs):
46 super(StubVCSController, self).__init__(*args, **kwargs)
47 super(StubVCSController, self).__init__(*args, **kwargs)
47 self._action = 'pull'
48 self._action = 'pull'
48 self._is_shadow_repo_dir = True
49 self._is_shadow_repo_dir = True
49 self._name = HG_REPO
50 self._name = HG_REPO
50 self.set_repo_names(None)
51 self.set_repo_names(None)
51
52
52 @property
53 @property
53 def is_shadow_repo_dir(self):
54 def is_shadow_repo_dir(self):
54 return self._is_shadow_repo_dir
55 return self._is_shadow_repo_dir
55
56
56 def _get_repository_name(self, environ):
57 def _get_repository_name(self, environ):
57 return self._name
58 return self._name
58
59
59 def _get_action(self, environ):
60 def _get_action(self, environ):
60 return self._action
61 return self._action
61
62
62 def _create_wsgi_app(self, repo_path, repo_name, config):
63 def _create_wsgi_app(self, repo_path, repo_name, config):
63 def fake_app(environ, start_response):
64 def fake_app(environ, start_response):
64 headers = [
65 headers = [
65 ('Http-Accept', 'application/mercurial')
66 ('Http-Accept', 'application/mercurial')
66 ]
67 ]
67 start_response('200 OK', headers)
68 start_response('200 OK', headers)
68 return self.stub_response_body
69 return self.stub_response_body
69 return fake_app
70 return fake_app
70
71
71 def _create_config(self, extras, repo_name):
72 def _create_config(self, extras, repo_name):
72 return None
73 return None
73
74
74
75
75 @pytest.fixture
76 @pytest.fixture
76 def vcscontroller(pylonsapp, config_stub):
77 def vcscontroller(pylonsapp, config_stub, request_stub):
77 config_stub.testing_securitypolicy()
78 config_stub.testing_securitypolicy()
78 config_stub.include('rhodecode.authentication')
79 config_stub.include('rhodecode.authentication')
79
80
80 #set_anonymous_access(True)
81 controller = StubVCSController(
81 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
82 pylonsapp, pylonsapp.config, request_stub.registry)
82 app = HttpsFixup(controller, pylonsapp.config)
83 app = HttpsFixup(controller, pylonsapp.config)
83 app = CustomTestApp(app)
84 app = CustomTestApp(app)
84
85
85 _remove_default_user_from_query_cache()
86 _remove_default_user_from_query_cache()
86
87
87 # Sanity checks that things are set up correctly
88 # Sanity checks that things are set up correctly
88 app.get('/' + HG_REPO, status=200)
89 app.get('/' + HG_REPO, status=200)
89
90
90 app.controller = controller
91 app.controller = controller
91 return app
92 return app
92
93
93
94
94 def _remove_default_user_from_query_cache():
95 def _remove_default_user_from_query_cache():
95 user = User.get_default_user(cache=True)
96 user = User.get_default_user(cache=True)
96 query = Session().query(User).filter(User.username == user.username)
97 query = Session().query(User).filter(User.username == user.username)
97 query = query.options(
98 query = query.options(
98 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
99 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
99 query.invalidate()
100 query.invalidate()
100 Session().expire(user)
101 Session().expire(user)
101
102
102
103
103 def test_handles_exceptions_during_permissions_checks(
104 def test_handles_exceptions_during_permissions_checks(
104 vcscontroller, disable_anonymous_user):
105 vcscontroller, disable_anonymous_user):
105 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
106 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
106 auth_password = base64.encodestring(user_and_pass).strip()
107 auth_password = base64.encodestring(user_and_pass).strip()
107 extra_environ = {
108 extra_environ = {
108 'AUTH_TYPE': 'Basic',
109 'AUTH_TYPE': 'Basic',
109 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
110 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
110 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
111 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
111 }
112 }
112
113
113 # Verify that things are hooked up correctly
114 # Verify that things are hooked up correctly
114 vcscontroller.get('/', status=200, extra_environ=extra_environ)
115 vcscontroller.get('/', status=200, extra_environ=extra_environ)
115
116
116 # Simulate trouble during permission checks
117 # Simulate trouble during permission checks
117 with mock.patch('rhodecode.model.db.User.get_by_username',
118 with mock.patch('rhodecode.model.db.User.get_by_username',
118 side_effect=Exception) as get_user:
119 side_effect=Exception) as get_user:
119 # Verify that a correct 500 is returned and check that the expected
120 # Verify that a correct 500 is returned and check that the expected
120 # code path was hit.
121 # code path was hit.
121 vcscontroller.get('/', status=500, extra_environ=extra_environ)
122 vcscontroller.get('/', status=500, extra_environ=extra_environ)
122 assert get_user.called
123 assert get_user.called
123
124
124
125
125 def test_returns_forbidden_if_no_anonymous_access(
126 def test_returns_forbidden_if_no_anonymous_access(
126 vcscontroller, disable_anonymous_user):
127 vcscontroller, disable_anonymous_user):
127 vcscontroller.get('/', status=401)
128 vcscontroller.get('/', status=401)
128
129
129
130
130 class StubFailVCSController(simplevcs.SimpleVCS):
131 class StubFailVCSController(simplevcs.SimpleVCS):
131 def _handle_request(self, environ, start_response):
132 def _handle_request(self, environ, start_response):
132 raise Exception("BOOM")
133 raise Exception("BOOM")
133
134
134
135
135 @pytest.fixture(scope='module')
136 @pytest.fixture(scope='module')
136 def fail_controller(pylonsapp):
137 def fail_controller(pylonsapp):
137 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
138 controller = StubFailVCSController(
139 pylonsapp, pylonsapp.config, pylonsapp.config)
138 controller = HttpsFixup(controller, pylonsapp.config)
140 controller = HttpsFixup(controller, pylonsapp.config)
139 controller = CustomTestApp(controller)
141 controller = CustomTestApp(controller)
140 return controller
142 return controller
141
143
142
144
143 def test_handles_exceptions_as_internal_server_error(fail_controller):
145 def test_handles_exceptions_as_internal_server_error(fail_controller):
144 fail_controller.get('/', status=500)
146 fail_controller.get('/', status=500)
145
147
146
148
147 def test_provides_traceback_for_appenlight(fail_controller):
149 def test_provides_traceback_for_appenlight(fail_controller):
148 response = fail_controller.get(
150 response = fail_controller.get(
149 '/', status=500, extra_environ={'appenlight.client': 'fake'})
151 '/', status=500, extra_environ={'appenlight.client': 'fake'})
150 assert 'appenlight.__traceback' in response.request.environ
152 assert 'appenlight.__traceback' in response.request.environ
151
153
152
154
153 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
155 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp, request_stub):
154 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
156 controller = StubVCSController(
157 pylonsapp, pylonsapp.config, request_stub.registry)
155 assert controller.scm_app is scm_app_http
158 assert controller.scm_app is scm_app_http
156
159
157
160
158 def test_allows_to_override_scm_app_via_config(pylonsapp):
161 def test_allows_to_override_scm_app_via_config(pylonsapp, request_stub):
159 config = pylonsapp.config.copy()
162 config = pylonsapp.config.copy()
160 config['vcs.scm_app_implementation'] = (
163 config['vcs.scm_app_implementation'] = (
161 'rhodecode.tests.lib.middleware.mock_scm_app')
164 'rhodecode.tests.lib.middleware.mock_scm_app')
162 controller = StubVCSController(pylonsapp, config, None)
165 controller = StubVCSController(
166 pylonsapp, config, request_stub.registry)
163 assert controller.scm_app is mock_scm_app
167 assert controller.scm_app is mock_scm_app
164
168
165
169
166 @pytest.mark.parametrize('query_string, expected', [
170 @pytest.mark.parametrize('query_string, expected', [
167 ('cmd=stub_command', True),
171 ('cmd=stub_command', True),
168 ('cmd=listkeys', False),
172 ('cmd=listkeys', False),
169 ])
173 ])
170 def test_should_check_locking(query_string, expected):
174 def test_should_check_locking(query_string, expected):
171 result = simplevcs._should_check_locking(query_string)
175 result = simplevcs._should_check_locking(query_string)
172 assert result == expected
176 assert result == expected
173
177
174
178
175 class TestShadowRepoRegularExpression(object):
179 class TestShadowRepoRegularExpression(object):
176 pr_segment = 'pull-request'
180 pr_segment = 'pull-request'
177 shadow_segment = 'repository'
181 shadow_segment = 'repository'
178
182
179 @pytest.mark.parametrize('url, expected', [
183 @pytest.mark.parametrize('url, expected', [
180 # repo with/without groups
184 # repo with/without groups
181 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
185 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
182 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
186 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
183 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
187 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
184 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
188 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
185
189
186 # pull request ID
190 # pull request ID
187 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
191 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
188 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
192 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
189 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
193 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
190 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
194 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
191
195
192 # unicode
196 # unicode
193 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
197 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
194 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
198 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
195
199
196 # trailing/leading slash
200 # trailing/leading slash
197 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
201 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
198 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
202 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
199 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
203 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
200
204
201 # misc
205 # misc
202 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
206 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
203 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
207 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
204 ])
208 ])
205 def test_shadow_repo_regular_expression(self, url, expected):
209 def test_shadow_repo_regular_expression(self, url, expected):
206 from rhodecode.lib.middleware.simplevcs import SimpleVCS
210 from rhodecode.lib.middleware.simplevcs import SimpleVCS
207 url = url.format(
211 url = url.format(
208 pr_segment=self.pr_segment,
212 pr_segment=self.pr_segment,
209 shadow_segment=self.shadow_segment)
213 shadow_segment=self.shadow_segment)
210 match_obj = SimpleVCS.shadow_repo_re.match(url)
214 match_obj = SimpleVCS.shadow_repo_re.match(url)
211 assert (match_obj is not None) == expected
215 assert (match_obj is not None) == expected
212
216
213
217
214 @pytest.mark.backends('git', 'hg')
218 @pytest.mark.backends('git', 'hg')
215 class TestShadowRepoExposure(object):
219 class TestShadowRepoExposure(object):
216
220
217 def test_pull_on_shadow_repo_propagates_to_wsgi_app(self, pylonsapp):
221 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
222 self, pylonsapp, request_stub):
218 """
223 """
219 Check that a pull action to a shadow repo is propagated to the
224 Check that a pull action to a shadow repo is propagated to the
220 underlying wsgi app.
225 underlying wsgi app.
221 """
226 """
222 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
227 controller = StubVCSController(
228 pylonsapp, pylonsapp.config, request_stub.registry)
223 controller._check_ssl = mock.Mock()
229 controller._check_ssl = mock.Mock()
224 controller.is_shadow_repo = True
230 controller.is_shadow_repo = True
225 controller._action = 'pull'
231 controller._action = 'pull'
226 controller._is_shadow_repo_dir = True
232 controller._is_shadow_repo_dir = True
227 controller.stub_response_body = 'dummy body value'
233 controller.stub_response_body = 'dummy body value'
228 environ_stub = {
234 environ_stub = {
229 'HTTP_HOST': 'test.example.com',
235 'HTTP_HOST': 'test.example.com',
230 'HTTP_ACCEPT': 'application/mercurial',
236 'HTTP_ACCEPT': 'application/mercurial',
231 'REQUEST_METHOD': 'GET',
237 'REQUEST_METHOD': 'GET',
232 'wsgi.url_scheme': 'http',
238 'wsgi.url_scheme': 'http',
233 }
239 }
234
240
235 response = controller(environ_stub, mock.Mock())
241 response = controller(environ_stub, mock.Mock())
236 response_body = ''.join(response)
242 response_body = ''.join(response)
237
243
238 # Assert that we got the response from the wsgi app.
244 # Assert that we got the response from the wsgi app.
239 assert response_body == controller.stub_response_body
245 assert response_body == controller.stub_response_body
240
246
241 def test_pull_on_shadow_repo_that_is_missing(self, pylonsapp):
247 def test_pull_on_shadow_repo_that_is_missing(self, pylonsapp, request_stub):
242 """
248 """
243 Check that a pull action to a shadow repo is propagated to the
249 Check that a pull action to a shadow repo is propagated to the
244 underlying wsgi app.
250 underlying wsgi app.
245 """
251 """
246 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
252 controller = StubVCSController(
253 pylonsapp, pylonsapp.config, request_stub.registry)
247 controller._check_ssl = mock.Mock()
254 controller._check_ssl = mock.Mock()
248 controller.is_shadow_repo = True
255 controller.is_shadow_repo = True
249 controller._action = 'pull'
256 controller._action = 'pull'
250 controller._is_shadow_repo_dir = False
257 controller._is_shadow_repo_dir = False
251 controller.stub_response_body = 'dummy body value'
258 controller.stub_response_body = 'dummy body value'
252 environ_stub = {
259 environ_stub = {
253 'HTTP_HOST': 'test.example.com',
260 'HTTP_HOST': 'test.example.com',
254 'HTTP_ACCEPT': 'application/mercurial',
261 'HTTP_ACCEPT': 'application/mercurial',
255 'REQUEST_METHOD': 'GET',
262 'REQUEST_METHOD': 'GET',
256 'wsgi.url_scheme': 'http',
263 'wsgi.url_scheme': 'http',
257 }
264 }
258
265
259 response = controller(environ_stub, mock.Mock())
266 response = controller(environ_stub, mock.Mock())
260 response_body = ''.join(response)
267 response_body = ''.join(response)
261
268
262 # Assert that we got the response from the wsgi app.
269 # Assert that we got the response from the wsgi app.
263 assert '404 Not Found' in response_body
270 assert '404 Not Found' in response_body
264
271
265 def test_push_on_shadow_repo_raises(self, pylonsapp):
272 def test_push_on_shadow_repo_raises(self, pylonsapp, request_stub):
266 """
273 """
267 Check that a push action to a shadow repo is aborted.
274 Check that a push action to a shadow repo is aborted.
268 """
275 """
269 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
276 controller = StubVCSController(
277 pylonsapp, pylonsapp.config, request_stub.registry)
270 controller._check_ssl = mock.Mock()
278 controller._check_ssl = mock.Mock()
271 controller.is_shadow_repo = True
279 controller.is_shadow_repo = True
272 controller._action = 'push'
280 controller._action = 'push'
273 controller.stub_response_body = 'dummy body value'
281 controller.stub_response_body = 'dummy body value'
274 environ_stub = {
282 environ_stub = {
275 'HTTP_HOST': 'test.example.com',
283 'HTTP_HOST': 'test.example.com',
276 'HTTP_ACCEPT': 'application/mercurial',
284 'HTTP_ACCEPT': 'application/mercurial',
277 'REQUEST_METHOD': 'GET',
285 'REQUEST_METHOD': 'GET',
278 'wsgi.url_scheme': 'http',
286 'wsgi.url_scheme': 'http',
279 }
287 }
280
288
281 response = controller(environ_stub, mock.Mock())
289 response = controller(environ_stub, mock.Mock())
282 response_body = ''.join(response)
290 response_body = ''.join(response)
283
291
284 assert response_body != controller.stub_response_body
292 assert response_body != controller.stub_response_body
285 # Assert that a 406 error is returned.
293 # Assert that a 406 error is returned.
286 assert '406 Not Acceptable' in response_body
294 assert '406 Not Acceptable' in response_body
287
295
288 def test_set_repo_names_no_shadow(self, pylonsapp):
296 def test_set_repo_names_no_shadow(self, pylonsapp, request_stub):
289 """
297 """
290 Check that the set_repo_names method sets all names to the one returned
298 Check that the set_repo_names method sets all names to the one returned
291 by the _get_repository_name method on a request to a non shadow repo.
299 by the _get_repository_name method on a request to a non shadow repo.
292 """
300 """
293 environ_stub = {}
301 environ_stub = {}
294 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
302 controller = StubVCSController(
303 pylonsapp, pylonsapp.config, request_stub.registry)
295 controller._name = 'RepoGroup/MyRepo'
304 controller._name = 'RepoGroup/MyRepo'
296 controller.set_repo_names(environ_stub)
305 controller.set_repo_names(environ_stub)
297 assert not controller.is_shadow_repo
306 assert not controller.is_shadow_repo
298 assert (controller.url_repo_name ==
307 assert (controller.url_repo_name ==
299 controller.acl_repo_name ==
308 controller.acl_repo_name ==
300 controller.vcs_repo_name ==
309 controller.vcs_repo_name ==
301 controller._get_repository_name(environ_stub))
310 controller._get_repository_name(environ_stub))
302
311
303 def test_set_repo_names_with_shadow(self, pylonsapp, pr_util, config_stub):
312 def test_set_repo_names_with_shadow(
313 self, pylonsapp, pr_util, config_stub, request_stub):
304 """
314 """
305 Check that the set_repo_names method sets correct names on a request
315 Check that the set_repo_names method sets correct names on a request
306 to a shadow repo.
316 to a shadow repo.
307 """
317 """
308 from rhodecode.model.pull_request import PullRequestModel
318 from rhodecode.model.pull_request import PullRequestModel
309
319
310 pull_request = pr_util.create_pull_request()
320 pull_request = pr_util.create_pull_request()
311 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
321 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
312 target=pull_request.target_repo.repo_name,
322 target=pull_request.target_repo.repo_name,
313 pr_id=pull_request.pull_request_id,
323 pr_id=pull_request.pull_request_id,
314 pr_segment=TestShadowRepoRegularExpression.pr_segment,
324 pr_segment=TestShadowRepoRegularExpression.pr_segment,
315 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
325 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
316 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
326 controller = StubVCSController(
327 pylonsapp, pylonsapp.config, request_stub.registry)
317 controller._name = shadow_url
328 controller._name = shadow_url
318 controller.set_repo_names({})
329 controller.set_repo_names({})
319
330
320 # Get file system path to shadow repo for assertions.
331 # Get file system path to shadow repo for assertions.
321 workspace_id = PullRequestModel()._workspace_id(pull_request)
332 workspace_id = PullRequestModel()._workspace_id(pull_request)
322 target_vcs = pull_request.target_repo.scm_instance()
333 target_vcs = pull_request.target_repo.scm_instance()
323 vcs_repo_name = target_vcs._get_shadow_repository_path(
334 vcs_repo_name = target_vcs._get_shadow_repository_path(
324 workspace_id)
335 workspace_id)
325
336
326 assert controller.vcs_repo_name == vcs_repo_name
337 assert controller.vcs_repo_name == vcs_repo_name
327 assert controller.url_repo_name == shadow_url
338 assert controller.url_repo_name == shadow_url
328 assert controller.acl_repo_name == pull_request.target_repo.repo_name
339 assert controller.acl_repo_name == pull_request.target_repo.repo_name
329 assert controller.is_shadow_repo
340 assert controller.is_shadow_repo
330
341
331 def test_set_repo_names_with_shadow_but_missing_pr(
342 def test_set_repo_names_with_shadow_but_missing_pr(
332 self, pylonsapp, pr_util, config_stub):
343 self, pylonsapp, pr_util, config_stub, request_stub):
333 """
344 """
334 Checks that the set_repo_names method enforces matching target repos
345 Checks that the set_repo_names method enforces matching target repos
335 and pull request IDs.
346 and pull request IDs.
336 """
347 """
337 pull_request = pr_util.create_pull_request()
348 pull_request = pr_util.create_pull_request()
338 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
349 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
339 target=pull_request.target_repo.repo_name,
350 target=pull_request.target_repo.repo_name,
340 pr_id=999999999,
351 pr_id=999999999,
341 pr_segment=TestShadowRepoRegularExpression.pr_segment,
352 pr_segment=TestShadowRepoRegularExpression.pr_segment,
342 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
353 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
343 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
354 controller = StubVCSController(
355 pylonsapp, pylonsapp.config, request_stub.registry)
344 controller._name = shadow_url
356 controller._name = shadow_url
345 controller.set_repo_names({})
357 controller.set_repo_names({})
346
358
347 assert not controller.is_shadow_repo
359 assert not controller.is_shadow_repo
348 assert (controller.url_repo_name ==
360 assert (controller.url_repo_name ==
349 controller.acl_repo_name ==
361 controller.acl_repo_name ==
350 controller.vcs_repo_name)
362 controller.vcs_repo_name)
351
363
352
364
353 @pytest.mark.usefixtures('db')
365 @pytest.mark.usefixtures('db')
354 class TestGenerateVcsResponse(object):
366 class TestGenerateVcsResponse(object):
355
367
356 def test_ensures_that_start_response_is_called_early_enough(self):
368 def test_ensures_that_start_response_is_called_early_enough(self):
357 self.call_controller_with_response_body(iter(['a', 'b']))
369 self.call_controller_with_response_body(iter(['a', 'b']))
358 assert self.start_response.called
370 assert self.start_response.called
359
371
360 def test_invalidates_cache_after_body_is_consumed(self):
372 def test_invalidates_cache_after_body_is_consumed(self):
361 result = self.call_controller_with_response_body(iter(['a', 'b']))
373 result = self.call_controller_with_response_body(iter(['a', 'b']))
362 assert not self.was_cache_invalidated()
374 assert not self.was_cache_invalidated()
363 # Consume the result
375 # Consume the result
364 list(result)
376 list(result)
365 assert self.was_cache_invalidated()
377 assert self.was_cache_invalidated()
366
378
367 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
379 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
368 def test_handles_locking_exception(self, http_locked_rc):
380 def test_handles_locking_exception(self, http_locked_rc):
369 result = self.call_controller_with_response_body(
381 result = self.call_controller_with_response_body(
370 self.raise_result_iter(vcs_kind='repo_locked'))
382 self.raise_result_iter(vcs_kind='repo_locked'))
371 assert not http_locked_rc.called
383 assert not http_locked_rc.called
372 # Consume the result
384 # Consume the result
373 list(result)
385 list(result)
374 assert http_locked_rc.called
386 assert http_locked_rc.called
375
387
376 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
388 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
377 def test_handles_requirement_exception(self, http_requirement):
389 def test_handles_requirement_exception(self, http_requirement):
378 result = self.call_controller_with_response_body(
390 result = self.call_controller_with_response_body(
379 self.raise_result_iter(vcs_kind='requirement'))
391 self.raise_result_iter(vcs_kind='requirement'))
380 assert not http_requirement.called
392 assert not http_requirement.called
381 # Consume the result
393 # Consume the result
382 list(result)
394 list(result)
383 assert http_requirement.called
395 assert http_requirement.called
384
396
385 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
397 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
386 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
398 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
387 app_factory_patcher = mock.patch.object(
399 app_factory_patcher = mock.patch.object(
388 StubVCSController, '_create_wsgi_app')
400 StubVCSController, '_create_wsgi_app')
389 with app_factory_patcher as app_factory:
401 with app_factory_patcher as app_factory:
390 app_factory().side_effect = self.vcs_exception()
402 app_factory().side_effect = self.vcs_exception()
391 result = self.call_controller_with_response_body(['a'])
403 result = self.call_controller_with_response_body(['a'])
392 list(result)
404 list(result)
393 assert http_locked_rc.called
405 assert http_locked_rc.called
394
406
395 def test_raises_unknown_exceptions(self):
407 def test_raises_unknown_exceptions(self):
396 result = self.call_controller_with_response_body(
408 result = self.call_controller_with_response_body(
397 self.raise_result_iter(vcs_kind='unknown'))
409 self.raise_result_iter(vcs_kind='unknown'))
398 with pytest.raises(Exception):
410 with pytest.raises(Exception):
399 list(result)
411 list(result)
400
412
401 def test_prepare_callback_daemon_is_called(self):
413 def test_prepare_callback_daemon_is_called(self):
402 def side_effect(extras):
414 def side_effect(extras):
403 return DummyHooksCallbackDaemon(), extras
415 return DummyHooksCallbackDaemon(), extras
404
416
405 prepare_patcher = mock.patch.object(
417 prepare_patcher = mock.patch.object(
406 StubVCSController, '_prepare_callback_daemon')
418 StubVCSController, '_prepare_callback_daemon')
407 with prepare_patcher as prepare_mock:
419 with prepare_patcher as prepare_mock:
408 prepare_mock.side_effect = side_effect
420 prepare_mock.side_effect = side_effect
409 self.call_controller_with_response_body(iter(['a', 'b']))
421 self.call_controller_with_response_body(iter(['a', 'b']))
410 assert prepare_mock.called
422 assert prepare_mock.called
411 assert prepare_mock.call_count == 1
423 assert prepare_mock.call_count == 1
412
424
413 def call_controller_with_response_body(self, response_body):
425 def call_controller_with_response_body(self, response_body):
414 settings = {
426 settings = {
415 'base_path': 'fake_base_path',
427 'base_path': 'fake_base_path',
416 'vcs.hooks.protocol': 'http',
428 'vcs.hooks.protocol': 'http',
417 'vcs.hooks.direct_calls': False,
429 'vcs.hooks.direct_calls': False,
418 }
430 }
419 controller = StubVCSController(None, settings, None)
431 registry = AttributeDict()
432 controller = StubVCSController(None, settings, registry)
420 controller._invalidate_cache = mock.Mock()
433 controller._invalidate_cache = mock.Mock()
421 controller.stub_response_body = response_body
434 controller.stub_response_body = response_body
422 self.start_response = mock.Mock()
435 self.start_response = mock.Mock()
423 result = controller._generate_vcs_response(
436 result = controller._generate_vcs_response(
424 environ={}, start_response=self.start_response,
437 environ={}, start_response=self.start_response,
425 repo_path='fake_repo_path',
438 repo_path='fake_repo_path',
426 extras={}, action='push')
439 extras={}, action='push')
427 self.controller = controller
440 self.controller = controller
428 return result
441 return result
429
442
430 def raise_result_iter(self, vcs_kind='repo_locked'):
443 def raise_result_iter(self, vcs_kind='repo_locked'):
431 """
444 """
432 Simulates an exception due to a vcs raised exception if kind vcs_kind
445 Simulates an exception due to a vcs raised exception if kind vcs_kind
433 """
446 """
434 raise self.vcs_exception(vcs_kind=vcs_kind)
447 raise self.vcs_exception(vcs_kind=vcs_kind)
435 yield "never_reached"
448 yield "never_reached"
436
449
437 def vcs_exception(self, vcs_kind='repo_locked'):
450 def vcs_exception(self, vcs_kind='repo_locked'):
438 locked_exception = Exception('TEST_MESSAGE')
451 locked_exception = Exception('TEST_MESSAGE')
439 locked_exception._vcs_kind = vcs_kind
452 locked_exception._vcs_kind = vcs_kind
440 return locked_exception
453 return locked_exception
441
454
442 def was_cache_invalidated(self):
455 def was_cache_invalidated(self):
443 return self.controller._invalidate_cache.called
456 return self.controller._invalidate_cache.called
444
457
445
458
446 class TestInitializeGenerator(object):
459 class TestInitializeGenerator(object):
447
460
448 def test_drains_first_element(self):
461 def test_drains_first_element(self):
449 gen = self.factory(['__init__', 1, 2])
462 gen = self.factory(['__init__', 1, 2])
450 result = list(gen)
463 result = list(gen)
451 assert result == [1, 2]
464 assert result == [1, 2]
452
465
453 @pytest.mark.parametrize('values', [
466 @pytest.mark.parametrize('values', [
454 [],
467 [],
455 [1, 2],
468 [1, 2],
456 ])
469 ])
457 def test_raises_value_error(self, values):
470 def test_raises_value_error(self, values):
458 with pytest.raises(ValueError):
471 with pytest.raises(ValueError):
459 self.factory(values)
472 self.factory(values)
460
473
461 @simplevcs.initialize_generator
474 @simplevcs.initialize_generator
462 def factory(self, iterable):
475 def factory(self, iterable):
463 for elem in iterable:
476 for elem in iterable:
464 yield elem
477 yield elem
465
478
466
479
467 class TestPrepareHooksDaemon(object):
480 class TestPrepareHooksDaemon(object):
468 def test_calls_imported_prepare_callback_daemon(self, app_settings):
481 def test_calls_imported_prepare_callback_daemon(self, app_settings, request_stub):
469 expected_extras = {'extra1': 'value1'}
482 expected_extras = {'extra1': 'value1'}
470 daemon = DummyHooksCallbackDaemon()
483 daemon = DummyHooksCallbackDaemon()
471
484
472 controller = StubVCSController(None, app_settings, None)
485 controller = StubVCSController(None, app_settings, request_stub.registry)
473 prepare_patcher = mock.patch.object(
486 prepare_patcher = mock.patch.object(
474 simplevcs, 'prepare_callback_daemon',
487 simplevcs, 'prepare_callback_daemon',
475 return_value=(daemon, expected_extras))
488 return_value=(daemon, expected_extras))
476 with prepare_patcher as prepare_mock:
489 with prepare_patcher as prepare_mock:
477 callback_daemon, extras = controller._prepare_callback_daemon(
490 callback_daemon, extras = controller._prepare_callback_daemon(
478 expected_extras.copy())
491 expected_extras.copy())
479 prepare_mock.assert_called_once_with(
492 prepare_mock.assert_called_once_with(
480 expected_extras,
493 expected_extras,
481 protocol=app_settings['vcs.hooks.protocol'],
494 protocol=app_settings['vcs.hooks.protocol'],
482 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
495 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
483
496
484 assert callback_daemon == daemon
497 assert callback_daemon == daemon
485 assert extras == extras
498 assert extras == extras
General Comments 0
You need to be logged in to leave comments. Login now