##// END OF EJS Templates
vcs: Pass registry to vcs for user authentication....
Martin Bornhold -
r591:bc63cba1 default
parent child Browse files
Show More
@@ -1,620 +1,620 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Authentication modules
22 Authentication modules
23 """
23 """
24
24
25 import colander
25 import colander
26 import logging
26 import logging
27 import time
27 import time
28 import traceback
28 import traceback
29 import warnings
29 import warnings
30
30
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from sqlalchemy.ext.hybrid import hybrid_property
33
33
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.lib import caches
36 from rhodecode.lib import caches
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
38 from rhodecode.lib.utils2 import md5_safe, safe_int
38 from rhodecode.lib.utils2 import md5_safe, safe_int
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.user import UserModel
43 from rhodecode.model.user import UserModel
44 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 # auth types that authenticate() function can receive
49 # auth types that authenticate() function can receive
50 VCS_TYPE = 'vcs'
50 VCS_TYPE = 'vcs'
51 HTTP_TYPE = 'http'
51 HTTP_TYPE = 'http'
52
52
53
53
54 class LazyFormencode(object):
54 class LazyFormencode(object):
55 def __init__(self, formencode_obj, *args, **kwargs):
55 def __init__(self, formencode_obj, *args, **kwargs):
56 self.formencode_obj = formencode_obj
56 self.formencode_obj = formencode_obj
57 self.args = args
57 self.args = args
58 self.kwargs = kwargs
58 self.kwargs = kwargs
59
59
60 def __call__(self, *args, **kwargs):
60 def __call__(self, *args, **kwargs):
61 from inspect import isfunction
61 from inspect import isfunction
62 formencode_obj = self.formencode_obj
62 formencode_obj = self.formencode_obj
63 if isfunction(formencode_obj):
63 if isfunction(formencode_obj):
64 # case we wrap validators into functions
64 # case we wrap validators into functions
65 formencode_obj = self.formencode_obj(*args, **kwargs)
65 formencode_obj = self.formencode_obj(*args, **kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
67
67
68
68
69 class RhodeCodeAuthPluginBase(object):
69 class RhodeCodeAuthPluginBase(object):
70 # cache the authentication request for N amount of seconds. Some kind
70 # cache the authentication request for N amount of seconds. Some kind
71 # of authentication methods are very heavy and it's very efficient to cache
71 # of authentication methods are very heavy and it's very efficient to cache
72 # the result of a call. If it's set to None (default) cache is off
72 # the result of a call. If it's set to None (default) cache is off
73 AUTH_CACHE_TTL = None
73 AUTH_CACHE_TTL = None
74 AUTH_CACHE = {}
74 AUTH_CACHE = {}
75
75
76 auth_func_attrs = {
76 auth_func_attrs = {
77 "username": "unique username",
77 "username": "unique username",
78 "firstname": "first name",
78 "firstname": "first name",
79 "lastname": "last name",
79 "lastname": "last name",
80 "email": "email address",
80 "email": "email address",
81 "groups": '["list", "of", "groups"]',
81 "groups": '["list", "of", "groups"]',
82 "extern_name": "name in external source of record",
82 "extern_name": "name in external source of record",
83 "extern_type": "type of external source of record",
83 "extern_type": "type of external source of record",
84 "admin": 'True|False defines if user should be RhodeCode super admin',
84 "admin": 'True|False defines if user should be RhodeCode super admin',
85 "active":
85 "active":
86 'True|False defines active state of user internally for RhodeCode',
86 'True|False defines active state of user internally for RhodeCode',
87 "active_from_extern":
87 "active_from_extern":
88 "True|False\None, active state from the external auth, "
88 "True|False\None, active state from the external auth, "
89 "None means use definition from RhodeCode extern_type active value"
89 "None means use definition from RhodeCode extern_type active value"
90 }
90 }
91 # set on authenticate() method and via set_auth_type func.
91 # set on authenticate() method and via set_auth_type func.
92 auth_type = None
92 auth_type = None
93
93
94 # List of setting names to store encrypted. Plugins may override this list
94 # List of setting names to store encrypted. Plugins may override this list
95 # to store settings encrypted.
95 # to store settings encrypted.
96 _settings_encrypted = []
96 _settings_encrypted = []
97
97
98 # Mapping of python to DB settings model types. Plugins may override or
98 # Mapping of python to DB settings model types. Plugins may override or
99 # extend this mapping.
99 # extend this mapping.
100 _settings_type_map = {
100 _settings_type_map = {
101 colander.String: 'unicode',
101 colander.String: 'unicode',
102 colander.Integer: 'int',
102 colander.Integer: 'int',
103 colander.Boolean: 'bool',
103 colander.Boolean: 'bool',
104 colander.List: 'list',
104 colander.List: 'list',
105 }
105 }
106
106
107 def __init__(self, plugin_id):
107 def __init__(self, plugin_id):
108 self._plugin_id = plugin_id
108 self._plugin_id = plugin_id
109
109
110 def __str__(self):
110 def __str__(self):
111 return self.get_id()
111 return self.get_id()
112
112
113 def _get_setting_full_name(self, name):
113 def _get_setting_full_name(self, name):
114 """
114 """
115 Return the full setting name used for storing values in the database.
115 Return the full setting name used for storing values in the database.
116 """
116 """
117 # TODO: johbo: Using the name here is problematic. It would be good to
117 # TODO: johbo: Using the name here is problematic. It would be good to
118 # introduce either new models in the database to hold Plugin and
118 # introduce either new models in the database to hold Plugin and
119 # PluginSetting or to use the plugin id here.
119 # PluginSetting or to use the plugin id here.
120 return 'auth_{}_{}'.format(self.name, name)
120 return 'auth_{}_{}'.format(self.name, name)
121
121
122 def _get_setting_type(self, name):
122 def _get_setting_type(self, name):
123 """
123 """
124 Return the type of a setting. This type is defined by the SettingsModel
124 Return the type of a setting. This type is defined by the SettingsModel
125 and determines how the setting is stored in DB. Optionally the suffix
125 and determines how the setting is stored in DB. Optionally the suffix
126 `.encrypted` is appended to instruct SettingsModel to store it
126 `.encrypted` is appended to instruct SettingsModel to store it
127 encrypted.
127 encrypted.
128 """
128 """
129 schema_node = self.get_settings_schema().get(name)
129 schema_node = self.get_settings_schema().get(name)
130 db_type = self._settings_type_map.get(
130 db_type = self._settings_type_map.get(
131 type(schema_node.typ), 'unicode')
131 type(schema_node.typ), 'unicode')
132 if name in self._settings_encrypted:
132 if name in self._settings_encrypted:
133 db_type = '{}.encrypted'.format(db_type)
133 db_type = '{}.encrypted'.format(db_type)
134 return db_type
134 return db_type
135
135
136 def is_enabled(self):
136 def is_enabled(self):
137 """
137 """
138 Returns true if this plugin is enabled. An enabled plugin can be
138 Returns true if this plugin is enabled. An enabled plugin can be
139 configured in the admin interface but it is not consulted during
139 configured in the admin interface but it is not consulted during
140 authentication.
140 authentication.
141 """
141 """
142 auth_plugins = SettingsModel().get_auth_plugins()
142 auth_plugins = SettingsModel().get_auth_plugins()
143 return self.get_id() in auth_plugins
143 return self.get_id() in auth_plugins
144
144
145 def is_active(self):
145 def is_active(self):
146 """
146 """
147 Returns true if the plugin is activated. An activated plugin is
147 Returns true if the plugin is activated. An activated plugin is
148 consulted during authentication, assumed it is also enabled.
148 consulted during authentication, assumed it is also enabled.
149 """
149 """
150 return self.get_setting_by_name('enabled')
150 return self.get_setting_by_name('enabled')
151
151
152 def get_id(self):
152 def get_id(self):
153 """
153 """
154 Returns the plugin id.
154 Returns the plugin id.
155 """
155 """
156 return self._plugin_id
156 return self._plugin_id
157
157
158 def get_display_name(self):
158 def get_display_name(self):
159 """
159 """
160 Returns a translation string for displaying purposes.
160 Returns a translation string for displaying purposes.
161 """
161 """
162 raise NotImplementedError('Not implemented in base class')
162 raise NotImplementedError('Not implemented in base class')
163
163
164 def get_settings_schema(self):
164 def get_settings_schema(self):
165 """
165 """
166 Returns a colander schema, representing the plugin settings.
166 Returns a colander schema, representing the plugin settings.
167 """
167 """
168 return AuthnPluginSettingsSchemaBase()
168 return AuthnPluginSettingsSchemaBase()
169
169
170 def get_setting_by_name(self, name, default=None):
170 def get_setting_by_name(self, name, default=None):
171 """
171 """
172 Returns a plugin setting by name.
172 Returns a plugin setting by name.
173 """
173 """
174 full_name = self._get_setting_full_name(name)
174 full_name = self._get_setting_full_name(name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
176 return db_setting.app_settings_value if db_setting else default
176 return db_setting.app_settings_value if db_setting else default
177
177
178 def create_or_update_setting(self, name, value):
178 def create_or_update_setting(self, name, value):
179 """
179 """
180 Create or update a setting for this plugin in the persistent storage.
180 Create or update a setting for this plugin in the persistent storage.
181 """
181 """
182 full_name = self._get_setting_full_name(name)
182 full_name = self._get_setting_full_name(name)
183 type_ = self._get_setting_type(name)
183 type_ = self._get_setting_type(name)
184 db_setting = SettingsModel().create_or_update_setting(
184 db_setting = SettingsModel().create_or_update_setting(
185 full_name, value, type_)
185 full_name, value, type_)
186 return db_setting.app_settings_value
186 return db_setting.app_settings_value
187
187
188 def get_settings(self):
188 def get_settings(self):
189 """
189 """
190 Returns the plugin settings as dictionary.
190 Returns the plugin settings as dictionary.
191 """
191 """
192 settings = {}
192 settings = {}
193 for node in self.get_settings_schema():
193 for node in self.get_settings_schema():
194 settings[node.name] = self.get_setting_by_name(node.name)
194 settings[node.name] = self.get_setting_by_name(node.name)
195 return settings
195 return settings
196
196
197 @property
197 @property
198 def validators(self):
198 def validators(self):
199 """
199 """
200 Exposes RhodeCode validators modules
200 Exposes RhodeCode validators modules
201 """
201 """
202 # this is a hack to overcome issues with pylons threadlocals and
202 # this is a hack to overcome issues with pylons threadlocals and
203 # translator object _() not beein registered properly.
203 # translator object _() not beein registered properly.
204 class LazyCaller(object):
204 class LazyCaller(object):
205 def __init__(self, name):
205 def __init__(self, name):
206 self.validator_name = name
206 self.validator_name = name
207
207
208 def __call__(self, *args, **kwargs):
208 def __call__(self, *args, **kwargs):
209 from rhodecode.model import validators as v
209 from rhodecode.model import validators as v
210 obj = getattr(v, self.validator_name)
210 obj = getattr(v, self.validator_name)
211 # log.debug('Initializing lazy formencode object: %s', obj)
211 # log.debug('Initializing lazy formencode object: %s', obj)
212 return LazyFormencode(obj, *args, **kwargs)
212 return LazyFormencode(obj, *args, **kwargs)
213
213
214 class ProxyGet(object):
214 class ProxyGet(object):
215 def __getattribute__(self, name):
215 def __getattribute__(self, name):
216 return LazyCaller(name)
216 return LazyCaller(name)
217
217
218 return ProxyGet()
218 return ProxyGet()
219
219
220 @hybrid_property
220 @hybrid_property
221 def name(self):
221 def name(self):
222 """
222 """
223 Returns the name of this authentication plugin.
223 Returns the name of this authentication plugin.
224
224
225 :returns: string
225 :returns: string
226 """
226 """
227 raise NotImplementedError("Not implemented in base class")
227 raise NotImplementedError("Not implemented in base class")
228
228
229 @property
229 @property
230 def is_headers_auth(self):
230 def is_headers_auth(self):
231 """
231 """
232 Returns True if this authentication plugin uses HTTP headers as
232 Returns True if this authentication plugin uses HTTP headers as
233 authentication method.
233 authentication method.
234 """
234 """
235 return False
235 return False
236
236
237 @hybrid_property
237 @hybrid_property
238 def is_container_auth(self):
238 def is_container_auth(self):
239 """
239 """
240 Deprecated method that indicates if this authentication plugin uses
240 Deprecated method that indicates if this authentication plugin uses
241 HTTP headers as authentication method.
241 HTTP headers as authentication method.
242 """
242 """
243 warnings.warn(
243 warnings.warn(
244 'Use is_headers_auth instead.', category=DeprecationWarning)
244 'Use is_headers_auth instead.', category=DeprecationWarning)
245 return self.is_headers_auth
245 return self.is_headers_auth
246
246
247 @hybrid_property
247 @hybrid_property
248 def allows_creating_users(self):
248 def allows_creating_users(self):
249 """
249 """
250 Defines if Plugin allows users to be created on-the-fly when
250 Defines if Plugin allows users to be created on-the-fly when
251 authentication is called. Controls how external plugins should behave
251 authentication is called. Controls how external plugins should behave
252 in terms if they are allowed to create new users, or not. Base plugins
252 in terms if they are allowed to create new users, or not. Base plugins
253 should not be allowed to, but External ones should be !
253 should not be allowed to, but External ones should be !
254
254
255 :return: bool
255 :return: bool
256 """
256 """
257 return False
257 return False
258
258
259 def set_auth_type(self, auth_type):
259 def set_auth_type(self, auth_type):
260 self.auth_type = auth_type
260 self.auth_type = auth_type
261
261
262 def allows_authentication_from(
262 def allows_authentication_from(
263 self, user, allows_non_existing_user=True,
263 self, user, allows_non_existing_user=True,
264 allowed_auth_plugins=None, allowed_auth_sources=None):
264 allowed_auth_plugins=None, allowed_auth_sources=None):
265 """
265 """
266 Checks if this authentication module should accept a request for
266 Checks if this authentication module should accept a request for
267 the current user.
267 the current user.
268
268
269 :param user: user object fetched using plugin's get_user() method.
269 :param user: user object fetched using plugin's get_user() method.
270 :param allows_non_existing_user: if True, don't allow the
270 :param allows_non_existing_user: if True, don't allow the
271 user to be empty, meaning not existing in our database
271 user to be empty, meaning not existing in our database
272 :param allowed_auth_plugins: if provided, users extern_type will be
272 :param allowed_auth_plugins: if provided, users extern_type will be
273 checked against a list of provided extern types, which are plugin
273 checked against a list of provided extern types, which are plugin
274 auth_names in the end
274 auth_names in the end
275 :param allowed_auth_sources: authentication type allowed,
275 :param allowed_auth_sources: authentication type allowed,
276 `http` or `vcs` default is both.
276 `http` or `vcs` default is both.
277 defines if plugin will accept only http authentication vcs
277 defines if plugin will accept only http authentication vcs
278 authentication(git/hg) or both
278 authentication(git/hg) or both
279 :returns: boolean
279 :returns: boolean
280 """
280 """
281 if not user and not allows_non_existing_user:
281 if not user and not allows_non_existing_user:
282 log.debug('User is empty but plugin does not allow empty users,'
282 log.debug('User is empty but plugin does not allow empty users,'
283 'not allowed to authenticate')
283 'not allowed to authenticate')
284 return False
284 return False
285
285
286 expected_auth_plugins = allowed_auth_plugins or [self.name]
286 expected_auth_plugins = allowed_auth_plugins or [self.name]
287 if user and (user.extern_type and
287 if user and (user.extern_type and
288 user.extern_type not in expected_auth_plugins):
288 user.extern_type not in expected_auth_plugins):
289 log.debug(
289 log.debug(
290 'User `%s` is bound to `%s` auth type. Plugin allows only '
290 'User `%s` is bound to `%s` auth type. Plugin allows only '
291 '%s, skipping', user, user.extern_type, expected_auth_plugins)
291 '%s, skipping', user, user.extern_type, expected_auth_plugins)
292
292
293 return False
293 return False
294
294
295 # by default accept both
295 # by default accept both
296 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
296 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
297 if self.auth_type not in expected_auth_from:
297 if self.auth_type not in expected_auth_from:
298 log.debug('Current auth source is %s but plugin only allows %s',
298 log.debug('Current auth source is %s but plugin only allows %s',
299 self.auth_type, expected_auth_from)
299 self.auth_type, expected_auth_from)
300 return False
300 return False
301
301
302 return True
302 return True
303
303
304 def get_user(self, username=None, **kwargs):
304 def get_user(self, username=None, **kwargs):
305 """
305 """
306 Helper method for user fetching in plugins, by default it's using
306 Helper method for user fetching in plugins, by default it's using
307 simple fetch by username, but this method can be custimized in plugins
307 simple fetch by username, but this method can be custimized in plugins
308 eg. headers auth plugin to fetch user by environ params
308 eg. headers auth plugin to fetch user by environ params
309
309
310 :param username: username if given to fetch from database
310 :param username: username if given to fetch from database
311 :param kwargs: extra arguments needed for user fetching.
311 :param kwargs: extra arguments needed for user fetching.
312 """
312 """
313 user = None
313 user = None
314 log.debug(
314 log.debug(
315 'Trying to fetch user `%s` from RhodeCode database', username)
315 'Trying to fetch user `%s` from RhodeCode database', username)
316 if username:
316 if username:
317 user = User.get_by_username(username)
317 user = User.get_by_username(username)
318 if not user:
318 if not user:
319 log.debug('User not found, fallback to fetch user in '
319 log.debug('User not found, fallback to fetch user in '
320 'case insensitive mode')
320 'case insensitive mode')
321 user = User.get_by_username(username, case_insensitive=True)
321 user = User.get_by_username(username, case_insensitive=True)
322 else:
322 else:
323 log.debug('provided username:`%s` is empty skipping...', username)
323 log.debug('provided username:`%s` is empty skipping...', username)
324 if not user:
324 if not user:
325 log.debug('User `%s` not found in database', username)
325 log.debug('User `%s` not found in database', username)
326 return user
326 return user
327
327
328 def user_activation_state(self):
328 def user_activation_state(self):
329 """
329 """
330 Defines user activation state when creating new users
330 Defines user activation state when creating new users
331
331
332 :returns: boolean
332 :returns: boolean
333 """
333 """
334 raise NotImplementedError("Not implemented in base class")
334 raise NotImplementedError("Not implemented in base class")
335
335
336 def auth(self, userobj, username, passwd, settings, **kwargs):
336 def auth(self, userobj, username, passwd, settings, **kwargs):
337 """
337 """
338 Given a user object (which may be null), username, a plaintext
338 Given a user object (which may be null), username, a plaintext
339 password, and a settings object (containing all the keys needed as
339 password, and a settings object (containing all the keys needed as
340 listed in settings()), authenticate this user's login attempt.
340 listed in settings()), authenticate this user's login attempt.
341
341
342 Return None on failure. On success, return a dictionary of the form:
342 Return None on failure. On success, return a dictionary of the form:
343
343
344 see: RhodeCodeAuthPluginBase.auth_func_attrs
344 see: RhodeCodeAuthPluginBase.auth_func_attrs
345 This is later validated for correctness
345 This is later validated for correctness
346 """
346 """
347 raise NotImplementedError("not implemented in base class")
347 raise NotImplementedError("not implemented in base class")
348
348
349 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
349 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
350 """
350 """
351 Wrapper to call self.auth() that validates call on it
351 Wrapper to call self.auth() that validates call on it
352
352
353 :param userobj: userobj
353 :param userobj: userobj
354 :param username: username
354 :param username: username
355 :param passwd: plaintext password
355 :param passwd: plaintext password
356 :param settings: plugin settings
356 :param settings: plugin settings
357 """
357 """
358 auth = self.auth(userobj, username, passwd, settings, **kwargs)
358 auth = self.auth(userobj, username, passwd, settings, **kwargs)
359 if auth:
359 if auth:
360 # check if hash should be migrated ?
360 # check if hash should be migrated ?
361 new_hash = auth.get('_hash_migrate')
361 new_hash = auth.get('_hash_migrate')
362 if new_hash:
362 if new_hash:
363 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
363 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
364 return self._validate_auth_return(auth)
364 return self._validate_auth_return(auth)
365 return auth
365 return auth
366
366
367 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
367 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
368 new_hash_cypher = _RhodeCodeCryptoBCrypt()
368 new_hash_cypher = _RhodeCodeCryptoBCrypt()
369 # extra checks, so make sure new hash is correct.
369 # extra checks, so make sure new hash is correct.
370 password_encoded = safe_str(password)
370 password_encoded = safe_str(password)
371 if new_hash and new_hash_cypher.hash_check(
371 if new_hash and new_hash_cypher.hash_check(
372 password_encoded, new_hash):
372 password_encoded, new_hash):
373 cur_user = User.get_by_username(username)
373 cur_user = User.get_by_username(username)
374 cur_user.password = new_hash
374 cur_user.password = new_hash
375 Session().add(cur_user)
375 Session().add(cur_user)
376 Session().flush()
376 Session().flush()
377 log.info('Migrated user %s hash to bcrypt', cur_user)
377 log.info('Migrated user %s hash to bcrypt', cur_user)
378
378
379 def _validate_auth_return(self, ret):
379 def _validate_auth_return(self, ret):
380 if not isinstance(ret, dict):
380 if not isinstance(ret, dict):
381 raise Exception('returned value from auth must be a dict')
381 raise Exception('returned value from auth must be a dict')
382 for k in self.auth_func_attrs:
382 for k in self.auth_func_attrs:
383 if k not in ret:
383 if k not in ret:
384 raise Exception('Missing %s attribute from returned data' % k)
384 raise Exception('Missing %s attribute from returned data' % k)
385 return ret
385 return ret
386
386
387
387
388 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
388 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
389
389
390 @hybrid_property
390 @hybrid_property
391 def allows_creating_users(self):
391 def allows_creating_users(self):
392 return True
392 return True
393
393
394 def use_fake_password(self):
394 def use_fake_password(self):
395 """
395 """
396 Return a boolean that indicates whether or not we should set the user's
396 Return a boolean that indicates whether or not we should set the user's
397 password to a random value when it is authenticated by this plugin.
397 password to a random value when it is authenticated by this plugin.
398 If your plugin provides authentication, then you will generally
398 If your plugin provides authentication, then you will generally
399 want this.
399 want this.
400
400
401 :returns: boolean
401 :returns: boolean
402 """
402 """
403 raise NotImplementedError("Not implemented in base class")
403 raise NotImplementedError("Not implemented in base class")
404
404
405 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
405 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
406 # at this point _authenticate calls plugin's `auth()` function
406 # at this point _authenticate calls plugin's `auth()` function
407 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
407 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
408 userobj, username, passwd, settings, **kwargs)
408 userobj, username, passwd, settings, **kwargs)
409 if auth:
409 if auth:
410 # maybe plugin will clean the username ?
410 # maybe plugin will clean the username ?
411 # we should use the return value
411 # we should use the return value
412 username = auth['username']
412 username = auth['username']
413
413
414 # if external source tells us that user is not active, we should
414 # if external source tells us that user is not active, we should
415 # skip rest of the process. This can prevent from creating users in
415 # skip rest of the process. This can prevent from creating users in
416 # RhodeCode when using external authentication, but if it's
416 # RhodeCode when using external authentication, but if it's
417 # inactive user we shouldn't create that user anyway
417 # inactive user we shouldn't create that user anyway
418 if auth['active_from_extern'] is False:
418 if auth['active_from_extern'] is False:
419 log.warning(
419 log.warning(
420 "User %s authenticated against %s, but is inactive",
420 "User %s authenticated against %s, but is inactive",
421 username, self.__module__)
421 username, self.__module__)
422 return None
422 return None
423
423
424 cur_user = User.get_by_username(username, case_insensitive=True)
424 cur_user = User.get_by_username(username, case_insensitive=True)
425 is_user_existing = cur_user is not None
425 is_user_existing = cur_user is not None
426
426
427 if is_user_existing:
427 if is_user_existing:
428 log.debug('Syncing user `%s` from '
428 log.debug('Syncing user `%s` from '
429 '`%s` plugin', username, self.name)
429 '`%s` plugin', username, self.name)
430 else:
430 else:
431 log.debug('Creating non existing user `%s` from '
431 log.debug('Creating non existing user `%s` from '
432 '`%s` plugin', username, self.name)
432 '`%s` plugin', username, self.name)
433
433
434 if self.allows_creating_users:
434 if self.allows_creating_users:
435 log.debug('Plugin `%s` allows to '
435 log.debug('Plugin `%s` allows to '
436 'create new users', self.name)
436 'create new users', self.name)
437 else:
437 else:
438 log.debug('Plugin `%s` does not allow to '
438 log.debug('Plugin `%s` does not allow to '
439 'create new users', self.name)
439 'create new users', self.name)
440
440
441 user_parameters = {
441 user_parameters = {
442 'username': username,
442 'username': username,
443 'email': auth["email"],
443 'email': auth["email"],
444 'firstname': auth["firstname"],
444 'firstname': auth["firstname"],
445 'lastname': auth["lastname"],
445 'lastname': auth["lastname"],
446 'active': auth["active"],
446 'active': auth["active"],
447 'admin': auth["admin"],
447 'admin': auth["admin"],
448 'extern_name': auth["extern_name"],
448 'extern_name': auth["extern_name"],
449 'extern_type': self.name,
449 'extern_type': self.name,
450 'plugin': self,
450 'plugin': self,
451 'allow_to_create_user': self.allows_creating_users,
451 'allow_to_create_user': self.allows_creating_users,
452 }
452 }
453
453
454 if not is_user_existing:
454 if not is_user_existing:
455 if self.use_fake_password():
455 if self.use_fake_password():
456 # Randomize the PW because we don't need it, but don't want
456 # Randomize the PW because we don't need it, but don't want
457 # them blank either
457 # them blank either
458 passwd = PasswordGenerator().gen_password(length=16)
458 passwd = PasswordGenerator().gen_password(length=16)
459 user_parameters['password'] = passwd
459 user_parameters['password'] = passwd
460 else:
460 else:
461 # Since the password is required by create_or_update method of
461 # Since the password is required by create_or_update method of
462 # UserModel, we need to set it explicitly.
462 # UserModel, we need to set it explicitly.
463 # The create_or_update method is smart and recognises the
463 # The create_or_update method is smart and recognises the
464 # password hashes as well.
464 # password hashes as well.
465 user_parameters['password'] = cur_user.password
465 user_parameters['password'] = cur_user.password
466
466
467 # we either create or update users, we also pass the flag
467 # we either create or update users, we also pass the flag
468 # that controls if this method can actually do that.
468 # that controls if this method can actually do that.
469 # raises NotAllowedToCreateUserError if it cannot, and we try to.
469 # raises NotAllowedToCreateUserError if it cannot, and we try to.
470 user = UserModel().create_or_update(**user_parameters)
470 user = UserModel().create_or_update(**user_parameters)
471 Session().flush()
471 Session().flush()
472 # enforce user is just in given groups, all of them has to be ones
472 # enforce user is just in given groups, all of them has to be ones
473 # created from plugins. We store this info in _group_data JSON
473 # created from plugins. We store this info in _group_data JSON
474 # field
474 # field
475 try:
475 try:
476 groups = auth['groups'] or []
476 groups = auth['groups'] or []
477 UserGroupModel().enforce_groups(user, groups, self.name)
477 UserGroupModel().enforce_groups(user, groups, self.name)
478 except Exception:
478 except Exception:
479 # for any reason group syncing fails, we should
479 # for any reason group syncing fails, we should
480 # proceed with login
480 # proceed with login
481 log.error(traceback.format_exc())
481 log.error(traceback.format_exc())
482 Session().commit()
482 Session().commit()
483 return auth
483 return auth
484
484
485
485
486 def loadplugin(plugin_id):
486 def loadplugin(plugin_id):
487 """
487 """
488 Loads and returns an instantiated authentication plugin.
488 Loads and returns an instantiated authentication plugin.
489 Returns the RhodeCodeAuthPluginBase subclass on success,
489 Returns the RhodeCodeAuthPluginBase subclass on success,
490 or None on failure.
490 or None on failure.
491 """
491 """
492 # TODO: Disusing pyramids thread locals to retrieve the registry.
492 # TODO: Disusing pyramids thread locals to retrieve the registry.
493 authn_registry = get_authn_registry()
493 authn_registry = get_authn_registry()
494 plugin = authn_registry.get_plugin(plugin_id)
494 plugin = authn_registry.get_plugin(plugin_id)
495 if plugin is None:
495 if plugin is None:
496 log.error('Authentication plugin not found: "%s"', plugin_id)
496 log.error('Authentication plugin not found: "%s"', plugin_id)
497 return plugin
497 return plugin
498
498
499
499
500 def get_authn_registry(registry=None):
500 def get_authn_registry(registry=None):
501 registry = registry or get_current_registry()
501 registry = registry or get_current_registry()
502 authn_registry = registry.getUtility(IAuthnPluginRegistry)
502 authn_registry = registry.getUtility(IAuthnPluginRegistry)
503 return authn_registry
503 return authn_registry
504
504
505
505
506 def get_auth_cache_manager(custom_ttl=None):
506 def get_auth_cache_manager(custom_ttl=None):
507 return caches.get_cache_manager(
507 return caches.get_cache_manager(
508 'auth_plugins', 'rhodecode.authentication', custom_ttl)
508 'auth_plugins', 'rhodecode.authentication', custom_ttl)
509
509
510
510
511 def authenticate(username, password, environ=None, auth_type=None,
511 def authenticate(username, password, environ=None, auth_type=None,
512 skip_missing=False):
512 skip_missing=False, registry=None):
513 """
513 """
514 Authentication function used for access control,
514 Authentication function used for access control,
515 It tries to authenticate based on enabled authentication modules.
515 It tries to authenticate based on enabled authentication modules.
516
516
517 :param username: username can be empty for headers auth
517 :param username: username can be empty for headers auth
518 :param password: password can be empty for headers auth
518 :param password: password can be empty for headers auth
519 :param environ: environ headers passed for headers auth
519 :param environ: environ headers passed for headers auth
520 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
520 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
521 :param skip_missing: ignores plugins that are in db but not in environment
521 :param skip_missing: ignores plugins that are in db but not in environment
522 :returns: None if auth failed, plugin_user dict if auth is correct
522 :returns: None if auth failed, plugin_user dict if auth is correct
523 """
523 """
524 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
524 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
525 raise ValueError('auth type must be on of http, vcs got "%s" instead'
525 raise ValueError('auth type must be on of http, vcs got "%s" instead'
526 % auth_type)
526 % auth_type)
527 headers_only = environ and not (username and password)
527 headers_only = environ and not (username and password)
528
528
529 authn_registry = get_authn_registry()
529 authn_registry = get_authn_registry(registry)
530 for plugin in authn_registry.get_plugins_for_authentication():
530 for plugin in authn_registry.get_plugins_for_authentication():
531 plugin.set_auth_type(auth_type)
531 plugin.set_auth_type(auth_type)
532 user = plugin.get_user(username)
532 user = plugin.get_user(username)
533 display_user = user.username if user else username
533 display_user = user.username if user else username
534
534
535 if headers_only and not plugin.is_headers_auth:
535 if headers_only and not plugin.is_headers_auth:
536 log.debug('Auth type is for headers only and plugin `%s` is not '
536 log.debug('Auth type is for headers only and plugin `%s` is not '
537 'headers plugin, skipping...', plugin.get_id())
537 'headers plugin, skipping...', plugin.get_id())
538 continue
538 continue
539
539
540 # load plugin settings from RhodeCode database
540 # load plugin settings from RhodeCode database
541 plugin_settings = plugin.get_settings()
541 plugin_settings = plugin.get_settings()
542 log.debug('Plugin settings:%s', plugin_settings)
542 log.debug('Plugin settings:%s', plugin_settings)
543
543
544 log.debug('Trying authentication using ** %s **', plugin.get_id())
544 log.debug('Trying authentication using ** %s **', plugin.get_id())
545 # use plugin's method of user extraction.
545 # use plugin's method of user extraction.
546 user = plugin.get_user(username, environ=environ,
546 user = plugin.get_user(username, environ=environ,
547 settings=plugin_settings)
547 settings=plugin_settings)
548 display_user = user.username if user else username
548 display_user = user.username if user else username
549 log.debug(
549 log.debug(
550 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
550 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
551
551
552 if not plugin.allows_authentication_from(user):
552 if not plugin.allows_authentication_from(user):
553 log.debug('Plugin %s does not accept user `%s` for authentication',
553 log.debug('Plugin %s does not accept user `%s` for authentication',
554 plugin.get_id(), display_user)
554 plugin.get_id(), display_user)
555 continue
555 continue
556 else:
556 else:
557 log.debug('Plugin %s accepted user `%s` for authentication',
557 log.debug('Plugin %s accepted user `%s` for authentication',
558 plugin.get_id(), display_user)
558 plugin.get_id(), display_user)
559
559
560 log.info('Authenticating user `%s` using %s plugin',
560 log.info('Authenticating user `%s` using %s plugin',
561 display_user, plugin.get_id())
561 display_user, plugin.get_id())
562
562
563 _cache_ttl = 0
563 _cache_ttl = 0
564
564
565 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
565 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
566 # plugin cache set inside is more important than the settings value
566 # plugin cache set inside is more important than the settings value
567 _cache_ttl = plugin.AUTH_CACHE_TTL
567 _cache_ttl = plugin.AUTH_CACHE_TTL
568 elif plugin_settings.get('cache_ttl'):
568 elif plugin_settings.get('cache_ttl'):
569 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
569 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
570
570
571 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
571 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
572
572
573 # get instance of cache manager configured for a namespace
573 # get instance of cache manager configured for a namespace
574 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
574 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
575
575
576 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
576 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
577 plugin.get_id(), plugin_cache_active, _cache_ttl)
577 plugin.get_id(), plugin_cache_active, _cache_ttl)
578
578
579 # for environ based password can be empty, but then the validation is
579 # for environ based password can be empty, but then the validation is
580 # on the server that fills in the env data needed for authentication
580 # on the server that fills in the env data needed for authentication
581 _password_hash = md5_safe(plugin.name + username + (password or ''))
581 _password_hash = md5_safe(plugin.name + username + (password or ''))
582
582
583 # _authenticate is a wrapper for .auth() method of plugin.
583 # _authenticate is a wrapper for .auth() method of plugin.
584 # it checks if .auth() sends proper data.
584 # it checks if .auth() sends proper data.
585 # For RhodeCodeExternalAuthPlugin it also maps users to
585 # For RhodeCodeExternalAuthPlugin it also maps users to
586 # Database and maps the attributes returned from .auth()
586 # Database and maps the attributes returned from .auth()
587 # to RhodeCode database. If this function returns data
587 # to RhodeCode database. If this function returns data
588 # then auth is correct.
588 # then auth is correct.
589 start = time.time()
589 start = time.time()
590 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
590 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
591
591
592 def auth_func():
592 def auth_func():
593 """
593 """
594 This function is used internally in Cache of Beaker to calculate
594 This function is used internally in Cache of Beaker to calculate
595 Results
595 Results
596 """
596 """
597 return plugin._authenticate(
597 return plugin._authenticate(
598 user, username, password, plugin_settings,
598 user, username, password, plugin_settings,
599 environ=environ or {})
599 environ=environ or {})
600
600
601 if plugin_cache_active:
601 if plugin_cache_active:
602 plugin_user = cache_manager.get(
602 plugin_user = cache_manager.get(
603 _password_hash, createfunc=auth_func)
603 _password_hash, createfunc=auth_func)
604 else:
604 else:
605 plugin_user = auth_func()
605 plugin_user = auth_func()
606
606
607 auth_time = time.time() - start
607 auth_time = time.time() - start
608 log.debug('Authentication for plugin `%s` completed in %.3fs, '
608 log.debug('Authentication for plugin `%s` completed in %.3fs, '
609 'expiration time of fetched cache %.1fs.',
609 'expiration time of fetched cache %.1fs.',
610 plugin.get_id(), auth_time, _cache_ttl)
610 plugin.get_id(), auth_time, _cache_ttl)
611
611
612 log.debug('PLUGIN USER DATA: %s', plugin_user)
612 log.debug('PLUGIN USER DATA: %s', plugin_user)
613
613
614 if plugin_user:
614 if plugin_user:
615 log.debug('Plugin returned proper authentication data')
615 log.debug('Plugin returned proper authentication data')
616 return plugin_user
616 return plugin_user
617 # we failed to Auth because .auth() method didn't return proper user
617 # we failed to Auth because .auth() method didn't return proper user
618 log.debug("User `%s` failed to authenticate against %s",
618 log.debug("User `%s` failed to authenticate against %s",
619 display_user, plugin.get_id())
619 display_user, plugin.get_id())
620 return None
620 return None
@@ -1,585 +1,587 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31
31
32 from paste.auth.basic import AuthBasicAuthenticator
32 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from pylons import config, tmpl_context as c, request, session, url
35 from pylons import config, tmpl_context as c, request, session, url
36 from pylons.controllers import WSGIController
36 from pylons.controllers import WSGIController
37 from pylons.controllers.util import redirect
37 from pylons.controllers.util import redirect
38 from pylons.i18n import translation
38 from pylons.i18n import translation
39 # marcink: don't remove this import
39 # marcink: don't remove this import
40 from pylons.templating import render_mako as render # noqa
40 from pylons.templating import render_mako as render # noqa
41 from pylons.i18n.translation import _
41 from pylons.i18n.translation import _
42 from webob.exc import HTTPFound
42 from webob.exc import HTTPFound
43
43
44
44
45 import rhodecode
45 import rhodecode
46 from rhodecode.authentication.base import VCS_TYPE
46 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.lib import auth, utils2
47 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import helpers as h
48 from rhodecode.lib import helpers as h
49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.exceptions import UserCreationError
50 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.utils import (
51 from rhodecode.lib.utils import (
52 get_repo_slug, set_rhodecode_config, password_changed,
52 get_repo_slug, set_rhodecode_config, password_changed,
53 get_enabled_hook_classes)
53 get_enabled_hook_classes)
54 from rhodecode.lib.utils2 import (
54 from rhodecode.lib.utils2 import (
55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.model import meta
57 from rhodecode.model import meta
58 from rhodecode.model.db import Repository, User
58 from rhodecode.model.db import Repository, User
59 from rhodecode.model.notification import NotificationModel
59 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62
62
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 def _filter_proxy(ip):
67 def _filter_proxy(ip):
68 """
68 """
69 Passed in IP addresses in HEADERS can be in a special format of multiple
69 Passed in IP addresses in HEADERS can be in a special format of multiple
70 ips. Those comma separated IPs are passed from various proxies in the
70 ips. Those comma separated IPs are passed from various proxies in the
71 chain of request processing. The left-most being the original client.
71 chain of request processing. The left-most being the original client.
72 We only care about the first IP which came from the org. client.
72 We only care about the first IP which came from the org. client.
73
73
74 :param ip: ip string from headers
74 :param ip: ip string from headers
75 """
75 """
76 if ',' in ip:
76 if ',' in ip:
77 _ips = ip.split(',')
77 _ips = ip.split(',')
78 _first_ip = _ips[0].strip()
78 _first_ip = _ips[0].strip()
79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 return _first_ip
80 return _first_ip
81 return ip
81 return ip
82
82
83
83
84 def _filter_port(ip):
84 def _filter_port(ip):
85 """
85 """
86 Removes a port from ip, there are 4 main cases to handle here.
86 Removes a port from ip, there are 4 main cases to handle here.
87 - ipv4 eg. 127.0.0.1
87 - ipv4 eg. 127.0.0.1
88 - ipv6 eg. ::1
88 - ipv6 eg. ::1
89 - ipv4+port eg. 127.0.0.1:8080
89 - ipv4+port eg. 127.0.0.1:8080
90 - ipv6+port eg. [::1]:8080
90 - ipv6+port eg. [::1]:8080
91
91
92 :param ip:
92 :param ip:
93 """
93 """
94 def is_ipv6(ip_addr):
94 def is_ipv6(ip_addr):
95 if hasattr(socket, 'inet_pton'):
95 if hasattr(socket, 'inet_pton'):
96 try:
96 try:
97 socket.inet_pton(socket.AF_INET6, ip_addr)
97 socket.inet_pton(socket.AF_INET6, ip_addr)
98 except socket.error:
98 except socket.error:
99 return False
99 return False
100 else:
100 else:
101 # fallback to ipaddress
101 # fallback to ipaddress
102 try:
102 try:
103 ipaddress.IPv6Address(ip_addr)
103 ipaddress.IPv6Address(ip_addr)
104 except Exception:
104 except Exception:
105 return False
105 return False
106 return True
106 return True
107
107
108 if ':' not in ip: # must be ipv4 pure ip
108 if ':' not in ip: # must be ipv4 pure ip
109 return ip
109 return ip
110
110
111 if '[' in ip and ']' in ip: # ipv6 with port
111 if '[' in ip and ']' in ip: # ipv6 with port
112 return ip.split(']')[0][1:].lower()
112 return ip.split(']')[0][1:].lower()
113
113
114 # must be ipv6 or ipv4 with port
114 # must be ipv6 or ipv4 with port
115 if is_ipv6(ip):
115 if is_ipv6(ip):
116 return ip
116 return ip
117 else:
117 else:
118 ip, _port = ip.split(':')[:2] # means ipv4+port
118 ip, _port = ip.split(':')[:2] # means ipv4+port
119 return ip
119 return ip
120
120
121
121
122 def get_ip_addr(environ):
122 def get_ip_addr(environ):
123 proxy_key = 'HTTP_X_REAL_IP'
123 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 def_key = 'REMOTE_ADDR'
125 def_key = 'REMOTE_ADDR'
126 _filters = lambda x: _filter_port(_filter_proxy(x))
126 _filters = lambda x: _filter_port(_filter_proxy(x))
127
127
128 ip = environ.get(proxy_key)
128 ip = environ.get(proxy_key)
129 if ip:
129 if ip:
130 return _filters(ip)
130 return _filters(ip)
131
131
132 ip = environ.get(proxy_key2)
132 ip = environ.get(proxy_key2)
133 if ip:
133 if ip:
134 return _filters(ip)
134 return _filters(ip)
135
135
136 ip = environ.get(def_key, '0.0.0.0')
136 ip = environ.get(def_key, '0.0.0.0')
137 return _filters(ip)
137 return _filters(ip)
138
138
139
139
140 def get_server_ip_addr(environ, log_errors=True):
140 def get_server_ip_addr(environ, log_errors=True):
141 hostname = environ.get('SERVER_NAME')
141 hostname = environ.get('SERVER_NAME')
142 try:
142 try:
143 return socket.gethostbyname(hostname)
143 return socket.gethostbyname(hostname)
144 except Exception as e:
144 except Exception as e:
145 if log_errors:
145 if log_errors:
146 # in some cases this lookup is not possible, and we don't want to
146 # in some cases this lookup is not possible, and we don't want to
147 # make it an exception in logs
147 # make it an exception in logs
148 log.exception('Could not retrieve server ip address: %s', e)
148 log.exception('Could not retrieve server ip address: %s', e)
149 return hostname
149 return hostname
150
150
151
151
152 def get_server_port(environ):
152 def get_server_port(environ):
153 return environ.get('SERVER_PORT')
153 return environ.get('SERVER_PORT')
154
154
155
155
156 def get_access_path(environ):
156 def get_access_path(environ):
157 path = environ.get('PATH_INFO')
157 path = environ.get('PATH_INFO')
158 org_req = environ.get('pylons.original_request')
158 org_req = environ.get('pylons.original_request')
159 if org_req:
159 if org_req:
160 path = org_req.environ.get('PATH_INFO')
160 path = org_req.environ.get('PATH_INFO')
161 return path
161 return path
162
162
163
163
164 def vcs_operation_context(
164 def vcs_operation_context(
165 environ, repo_name, username, action, scm, check_locking=True):
165 environ, repo_name, username, action, scm, check_locking=True):
166 """
166 """
167 Generate the context for a vcs operation, e.g. push or pull.
167 Generate the context for a vcs operation, e.g. push or pull.
168
168
169 This context is passed over the layers so that hooks triggered by the
169 This context is passed over the layers so that hooks triggered by the
170 vcs operation know details like the user, the user's IP address etc.
170 vcs operation know details like the user, the user's IP address etc.
171
171
172 :param check_locking: Allows to switch of the computation of the locking
172 :param check_locking: Allows to switch of the computation of the locking
173 data. This serves mainly the need of the simplevcs middleware to be
173 data. This serves mainly the need of the simplevcs middleware to be
174 able to disable this for certain operations.
174 able to disable this for certain operations.
175
175
176 """
176 """
177 # Tri-state value: False: unlock, None: nothing, True: lock
177 # Tri-state value: False: unlock, None: nothing, True: lock
178 make_lock = None
178 make_lock = None
179 locked_by = [None, None, None]
179 locked_by = [None, None, None]
180 is_anonymous = username == User.DEFAULT_USER
180 is_anonymous = username == User.DEFAULT_USER
181 if not is_anonymous and check_locking:
181 if not is_anonymous and check_locking:
182 log.debug('Checking locking on repository "%s"', repo_name)
182 log.debug('Checking locking on repository "%s"', repo_name)
183 user = User.get_by_username(username)
183 user = User.get_by_username(username)
184 repo = Repository.get_by_repo_name(repo_name)
184 repo = Repository.get_by_repo_name(repo_name)
185 make_lock, __, locked_by = repo.get_locking_state(
185 make_lock, __, locked_by = repo.get_locking_state(
186 action, user.user_id)
186 action, user.user_id)
187
187
188 settings_model = VcsSettingsModel(repo=repo_name)
188 settings_model = VcsSettingsModel(repo=repo_name)
189 ui_settings = settings_model.get_ui_settings()
189 ui_settings = settings_model.get_ui_settings()
190
190
191 extras = {
191 extras = {
192 'ip': get_ip_addr(environ),
192 'ip': get_ip_addr(environ),
193 'username': username,
193 'username': username,
194 'action': action,
194 'action': action,
195 'repository': repo_name,
195 'repository': repo_name,
196 'scm': scm,
196 'scm': scm,
197 'config': rhodecode.CONFIG['__file__'],
197 'config': rhodecode.CONFIG['__file__'],
198 'make_lock': make_lock,
198 'make_lock': make_lock,
199 'locked_by': locked_by,
199 'locked_by': locked_by,
200 'server_url': utils2.get_server_url(environ),
200 'server_url': utils2.get_server_url(environ),
201 'hooks': get_enabled_hook_classes(ui_settings),
201 'hooks': get_enabled_hook_classes(ui_settings),
202 }
202 }
203 return extras
203 return extras
204
204
205
205
206 class BasicAuth(AuthBasicAuthenticator):
206 class BasicAuth(AuthBasicAuthenticator):
207
207
208 def __init__(self, realm, authfunc, auth_http_code=None,
208 def __init__(self, realm, authfunc, registry, auth_http_code=None,
209 initial_call_detection=False):
209 initial_call_detection=False):
210 self.realm = realm
210 self.realm = realm
211 self.initial_call = initial_call_detection
211 self.initial_call = initial_call_detection
212 self.authfunc = authfunc
212 self.authfunc = authfunc
213 self.registry = registry
213 self._rc_auth_http_code = auth_http_code
214 self._rc_auth_http_code = auth_http_code
214
215
215 def _get_response_from_code(self, http_code):
216 def _get_response_from_code(self, http_code):
216 try:
217 try:
217 return get_exception(safe_int(http_code))
218 return get_exception(safe_int(http_code))
218 except Exception:
219 except Exception:
219 log.exception('Failed to fetch response for code %s' % http_code)
220 log.exception('Failed to fetch response for code %s' % http_code)
220 return HTTPForbidden
221 return HTTPForbidden
221
222
222 def build_authentication(self):
223 def build_authentication(self):
223 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
224 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
224 if self._rc_auth_http_code and not self.initial_call:
225 if self._rc_auth_http_code and not self.initial_call:
225 # return alternative HTTP code if alternative http return code
226 # return alternative HTTP code if alternative http return code
226 # is specified in RhodeCode config, but ONLY if it's not the
227 # is specified in RhodeCode config, but ONLY if it's not the
227 # FIRST call
228 # FIRST call
228 custom_response_klass = self._get_response_from_code(
229 custom_response_klass = self._get_response_from_code(
229 self._rc_auth_http_code)
230 self._rc_auth_http_code)
230 return custom_response_klass(headers=head)
231 return custom_response_klass(headers=head)
231 return HTTPUnauthorized(headers=head)
232 return HTTPUnauthorized(headers=head)
232
233
233 def authenticate(self, environ):
234 def authenticate(self, environ):
234 authorization = AUTHORIZATION(environ)
235 authorization = AUTHORIZATION(environ)
235 if not authorization:
236 if not authorization:
236 return self.build_authentication()
237 return self.build_authentication()
237 (authmeth, auth) = authorization.split(' ', 1)
238 (authmeth, auth) = authorization.split(' ', 1)
238 if 'basic' != authmeth.lower():
239 if 'basic' != authmeth.lower():
239 return self.build_authentication()
240 return self.build_authentication()
240 auth = auth.strip().decode('base64')
241 auth = auth.strip().decode('base64')
241 _parts = auth.split(':', 1)
242 _parts = auth.split(':', 1)
242 if len(_parts) == 2:
243 if len(_parts) == 2:
243 username, password = _parts
244 username, password = _parts
244 if self.authfunc(
245 if self.authfunc(
245 username, password, environ, VCS_TYPE):
246 username, password, environ, VCS_TYPE,
247 registry=self.registry):
246 return username
248 return username
247 if username and password:
249 if username and password:
248 # we mark that we actually executed authentication once, at
250 # we mark that we actually executed authentication once, at
249 # that point we can use the alternative auth code
251 # that point we can use the alternative auth code
250 self.initial_call = False
252 self.initial_call = False
251
253
252 return self.build_authentication()
254 return self.build_authentication()
253
255
254 __call__ = authenticate
256 __call__ = authenticate
255
257
256
258
257 def attach_context_attributes(context, request):
259 def attach_context_attributes(context, request):
258 """
260 """
259 Attach variables into template context called `c`, please note that
261 Attach variables into template context called `c`, please note that
260 request could be pylons or pyramid request in here.
262 request could be pylons or pyramid request in here.
261 """
263 """
262 rc_config = SettingsModel().get_all_settings(cache=True)
264 rc_config = SettingsModel().get_all_settings(cache=True)
263
265
264 context.rhodecode_version = rhodecode.__version__
266 context.rhodecode_version = rhodecode.__version__
265 context.rhodecode_edition = config.get('rhodecode.edition')
267 context.rhodecode_edition = config.get('rhodecode.edition')
266 # unique secret + version does not leak the version but keep consistency
268 # unique secret + version does not leak the version but keep consistency
267 context.rhodecode_version_hash = md5(
269 context.rhodecode_version_hash = md5(
268 config.get('beaker.session.secret', '') +
270 config.get('beaker.session.secret', '') +
269 rhodecode.__version__)[:8]
271 rhodecode.__version__)[:8]
270
272
271 # Default language set for the incoming request
273 # Default language set for the incoming request
272 context.language = translation.get_lang()[0]
274 context.language = translation.get_lang()[0]
273
275
274 # Visual options
276 # Visual options
275 context.visual = AttributeDict({})
277 context.visual = AttributeDict({})
276
278
277 # DB store
279 # DB store
278 context.visual.show_public_icon = str2bool(
280 context.visual.show_public_icon = str2bool(
279 rc_config.get('rhodecode_show_public_icon'))
281 rc_config.get('rhodecode_show_public_icon'))
280 context.visual.show_private_icon = str2bool(
282 context.visual.show_private_icon = str2bool(
281 rc_config.get('rhodecode_show_private_icon'))
283 rc_config.get('rhodecode_show_private_icon'))
282 context.visual.stylify_metatags = str2bool(
284 context.visual.stylify_metatags = str2bool(
283 rc_config.get('rhodecode_stylify_metatags'))
285 rc_config.get('rhodecode_stylify_metatags'))
284 context.visual.dashboard_items = safe_int(
286 context.visual.dashboard_items = safe_int(
285 rc_config.get('rhodecode_dashboard_items', 100))
287 rc_config.get('rhodecode_dashboard_items', 100))
286 context.visual.admin_grid_items = safe_int(
288 context.visual.admin_grid_items = safe_int(
287 rc_config.get('rhodecode_admin_grid_items', 100))
289 rc_config.get('rhodecode_admin_grid_items', 100))
288 context.visual.repository_fields = str2bool(
290 context.visual.repository_fields = str2bool(
289 rc_config.get('rhodecode_repository_fields'))
291 rc_config.get('rhodecode_repository_fields'))
290 context.visual.show_version = str2bool(
292 context.visual.show_version = str2bool(
291 rc_config.get('rhodecode_show_version'))
293 rc_config.get('rhodecode_show_version'))
292 context.visual.use_gravatar = str2bool(
294 context.visual.use_gravatar = str2bool(
293 rc_config.get('rhodecode_use_gravatar'))
295 rc_config.get('rhodecode_use_gravatar'))
294 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
296 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
295 context.visual.default_renderer = rc_config.get(
297 context.visual.default_renderer = rc_config.get(
296 'rhodecode_markup_renderer', 'rst')
298 'rhodecode_markup_renderer', 'rst')
297 context.visual.rhodecode_support_url = \
299 context.visual.rhodecode_support_url = \
298 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
300 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
299
301
300 context.pre_code = rc_config.get('rhodecode_pre_code')
302 context.pre_code = rc_config.get('rhodecode_pre_code')
301 context.post_code = rc_config.get('rhodecode_post_code')
303 context.post_code = rc_config.get('rhodecode_post_code')
302 context.rhodecode_name = rc_config.get('rhodecode_title')
304 context.rhodecode_name = rc_config.get('rhodecode_title')
303 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
305 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
304 # if we have specified default_encoding in the request, it has more
306 # if we have specified default_encoding in the request, it has more
305 # priority
307 # priority
306 if request.GET.get('default_encoding'):
308 if request.GET.get('default_encoding'):
307 context.default_encodings.insert(0, request.GET.get('default_encoding'))
309 context.default_encodings.insert(0, request.GET.get('default_encoding'))
308 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
310 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
309
311
310 # INI stored
312 # INI stored
311 context.labs_active = str2bool(
313 context.labs_active = str2bool(
312 config.get('labs_settings_active', 'false'))
314 config.get('labs_settings_active', 'false'))
313 context.visual.allow_repo_location_change = str2bool(
315 context.visual.allow_repo_location_change = str2bool(
314 config.get('allow_repo_location_change', True))
316 config.get('allow_repo_location_change', True))
315 context.visual.allow_custom_hooks_settings = str2bool(
317 context.visual.allow_custom_hooks_settings = str2bool(
316 config.get('allow_custom_hooks_settings', True))
318 config.get('allow_custom_hooks_settings', True))
317 context.debug_style = str2bool(config.get('debug_style', False))
319 context.debug_style = str2bool(config.get('debug_style', False))
318
320
319 context.rhodecode_instanceid = config.get('instance_id')
321 context.rhodecode_instanceid = config.get('instance_id')
320
322
321 # AppEnlight
323 # AppEnlight
322 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
324 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
323 context.appenlight_api_public_key = config.get(
325 context.appenlight_api_public_key = config.get(
324 'appenlight.api_public_key', '')
326 'appenlight.api_public_key', '')
325 context.appenlight_server_url = config.get('appenlight.server_url', '')
327 context.appenlight_server_url = config.get('appenlight.server_url', '')
326
328
327 # JS template context
329 # JS template context
328 context.template_context = {
330 context.template_context = {
329 'repo_name': None,
331 'repo_name': None,
330 'repo_type': None,
332 'repo_type': None,
331 'repo_landing_commit': None,
333 'repo_landing_commit': None,
332 'rhodecode_user': {
334 'rhodecode_user': {
333 'username': None,
335 'username': None,
334 'email': None,
336 'email': None,
335 'notification_status': False
337 'notification_status': False
336 },
338 },
337 'visual': {
339 'visual': {
338 'default_renderer': None
340 'default_renderer': None
339 },
341 },
340 'commit_data': {
342 'commit_data': {
341 'commit_id': None
343 'commit_id': None
342 },
344 },
343 'pull_request_data': {'pull_request_id': None},
345 'pull_request_data': {'pull_request_id': None},
344 'timeago': {
346 'timeago': {
345 'refresh_time': 120 * 1000,
347 'refresh_time': 120 * 1000,
346 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
348 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
347 },
349 },
348 'pylons_dispatch': {
350 'pylons_dispatch': {
349 # 'controller': request.environ['pylons.routes_dict']['controller'],
351 # 'controller': request.environ['pylons.routes_dict']['controller'],
350 # 'action': request.environ['pylons.routes_dict']['action'],
352 # 'action': request.environ['pylons.routes_dict']['action'],
351 },
353 },
352 'pyramid_dispatch': {
354 'pyramid_dispatch': {
353
355
354 },
356 },
355 'extra': {'plugins': {}}
357 'extra': {'plugins': {}}
356 }
358 }
357 # END CONFIG VARS
359 # END CONFIG VARS
358
360
359 # TODO: This dosn't work when called from pylons compatibility tween.
361 # TODO: This dosn't work when called from pylons compatibility tween.
360 # Fix this and remove it from base controller.
362 # Fix this and remove it from base controller.
361 # context.repo_name = get_repo_slug(request) # can be empty
363 # context.repo_name = get_repo_slug(request) # can be empty
362
364
363 context.csrf_token = auth.get_csrf_token()
365 context.csrf_token = auth.get_csrf_token()
364 context.backends = rhodecode.BACKENDS.keys()
366 context.backends = rhodecode.BACKENDS.keys()
365 context.backends.sort()
367 context.backends.sort()
366 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
368 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
367 context.rhodecode_user.user_id)
369 context.rhodecode_user.user_id)
368
370
369
371
370 def get_auth_user(environ):
372 def get_auth_user(environ):
371 ip_addr = get_ip_addr(environ)
373 ip_addr = get_ip_addr(environ)
372 # make sure that we update permissions each time we call controller
374 # make sure that we update permissions each time we call controller
373 _auth_token = (request.GET.get('auth_token', '') or
375 _auth_token = (request.GET.get('auth_token', '') or
374 request.GET.get('api_key', ''))
376 request.GET.get('api_key', ''))
375
377
376 if _auth_token:
378 if _auth_token:
377 # when using API_KEY we are sure user exists.
379 # when using API_KEY we are sure user exists.
378 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
380 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
379 authenticated = False
381 authenticated = False
380 else:
382 else:
381 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
383 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
382 try:
384 try:
383 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
385 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
384 ip_addr=ip_addr)
386 ip_addr=ip_addr)
385 except UserCreationError as e:
387 except UserCreationError as e:
386 h.flash(e, 'error')
388 h.flash(e, 'error')
387 # container auth or other auth functions that create users
389 # container auth or other auth functions that create users
388 # on the fly can throw this exception signaling that there's
390 # on the fly can throw this exception signaling that there's
389 # issue with user creation, explanation should be provided
391 # issue with user creation, explanation should be provided
390 # in Exception itself. We then create a simple blank
392 # in Exception itself. We then create a simple blank
391 # AuthUser
393 # AuthUser
392 auth_user = AuthUser(ip_addr=ip_addr)
394 auth_user = AuthUser(ip_addr=ip_addr)
393
395
394 if password_changed(auth_user, session):
396 if password_changed(auth_user, session):
395 session.invalidate()
397 session.invalidate()
396 cookie_store = CookieStoreWrapper(
398 cookie_store = CookieStoreWrapper(
397 session.get('rhodecode_user'))
399 session.get('rhodecode_user'))
398 auth_user = AuthUser(ip_addr=ip_addr)
400 auth_user = AuthUser(ip_addr=ip_addr)
399
401
400 authenticated = cookie_store.get('is_authenticated')
402 authenticated = cookie_store.get('is_authenticated')
401
403
402 if not auth_user.is_authenticated and auth_user.is_user_object:
404 if not auth_user.is_authenticated and auth_user.is_user_object:
403 # user is not authenticated and not empty
405 # user is not authenticated and not empty
404 auth_user.set_authenticated(authenticated)
406 auth_user.set_authenticated(authenticated)
405
407
406 return auth_user
408 return auth_user
407
409
408
410
409 class BaseController(WSGIController):
411 class BaseController(WSGIController):
410
412
411 def __before__(self):
413 def __before__(self):
412 """
414 """
413 __before__ is called before controller methods and after __call__
415 __before__ is called before controller methods and after __call__
414 """
416 """
415 # on each call propagate settings calls into global settings.
417 # on each call propagate settings calls into global settings.
416 set_rhodecode_config(config)
418 set_rhodecode_config(config)
417 attach_context_attributes(c, request)
419 attach_context_attributes(c, request)
418
420
419 # TODO: Remove this when fixed in attach_context_attributes()
421 # TODO: Remove this when fixed in attach_context_attributes()
420 c.repo_name = get_repo_slug(request) # can be empty
422 c.repo_name = get_repo_slug(request) # can be empty
421
423
422 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
424 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
423 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
425 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
424 self.sa = meta.Session
426 self.sa = meta.Session
425 self.scm_model = ScmModel(self.sa)
427 self.scm_model = ScmModel(self.sa)
426
428
427 default_lang = c.language
429 default_lang = c.language
428 user_lang = c.language
430 user_lang = c.language
429 try:
431 try:
430 user_obj = self._rhodecode_user.get_instance()
432 user_obj = self._rhodecode_user.get_instance()
431 if user_obj:
433 if user_obj:
432 user_lang = user_obj.user_data.get('language')
434 user_lang = user_obj.user_data.get('language')
433 except Exception:
435 except Exception:
434 log.exception('Failed to fetch user language for user %s',
436 log.exception('Failed to fetch user language for user %s',
435 self._rhodecode_user)
437 self._rhodecode_user)
436
438
437 if user_lang and user_lang != default_lang:
439 if user_lang and user_lang != default_lang:
438 log.debug('set language to %s for user %s', user_lang,
440 log.debug('set language to %s for user %s', user_lang,
439 self._rhodecode_user)
441 self._rhodecode_user)
440 translation.set_lang(user_lang)
442 translation.set_lang(user_lang)
441
443
442 def _dispatch_redirect(self, with_url, environ, start_response):
444 def _dispatch_redirect(self, with_url, environ, start_response):
443 resp = HTTPFound(with_url)
445 resp = HTTPFound(with_url)
444 environ['SCRIPT_NAME'] = '' # handle prefix middleware
446 environ['SCRIPT_NAME'] = '' # handle prefix middleware
445 environ['PATH_INFO'] = with_url
447 environ['PATH_INFO'] = with_url
446 return resp(environ, start_response)
448 return resp(environ, start_response)
447
449
448 def __call__(self, environ, start_response):
450 def __call__(self, environ, start_response):
449 """Invoke the Controller"""
451 """Invoke the Controller"""
450 # WSGIController.__call__ dispatches to the Controller method
452 # WSGIController.__call__ dispatches to the Controller method
451 # the request is routed to. This routing information is
453 # the request is routed to. This routing information is
452 # available in environ['pylons.routes_dict']
454 # available in environ['pylons.routes_dict']
453 from rhodecode.lib import helpers as h
455 from rhodecode.lib import helpers as h
454
456
455 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
457 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
456 if environ.get('debugtoolbar.wants_pylons_context', False):
458 if environ.get('debugtoolbar.wants_pylons_context', False):
457 environ['debugtoolbar.pylons_context'] = c._current_obj()
459 environ['debugtoolbar.pylons_context'] = c._current_obj()
458
460
459 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
461 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
460 environ['pylons.routes_dict']['action']])
462 environ['pylons.routes_dict']['action']])
461
463
462 self.rc_config = SettingsModel().get_all_settings(cache=True)
464 self.rc_config = SettingsModel().get_all_settings(cache=True)
463 self.ip_addr = get_ip_addr(environ)
465 self.ip_addr = get_ip_addr(environ)
464
466
465 # The rhodecode auth user is looked up and passed through the
467 # The rhodecode auth user is looked up and passed through the
466 # environ by the pylons compatibility tween in pyramid.
468 # environ by the pylons compatibility tween in pyramid.
467 # So we can just grab it from there.
469 # So we can just grab it from there.
468 auth_user = environ['rc_auth_user']
470 auth_user = environ['rc_auth_user']
469
471
470 # set globals for auth user
472 # set globals for auth user
471 request.user = auth_user
473 request.user = auth_user
472 c.rhodecode_user = self._rhodecode_user = auth_user
474 c.rhodecode_user = self._rhodecode_user = auth_user
473
475
474 log.info('IP: %s User: %s accessed %s [%s]' % (
476 log.info('IP: %s User: %s accessed %s [%s]' % (
475 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
477 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
476 _route_name)
478 _route_name)
477 )
479 )
478
480
479 # TODO: Maybe this should be move to pyramid to cover all views.
481 # TODO: Maybe this should be move to pyramid to cover all views.
480 # check user attributes for password change flag
482 # check user attributes for password change flag
481 user_obj = auth_user.get_instance()
483 user_obj = auth_user.get_instance()
482 if user_obj and user_obj.user_data.get('force_password_change'):
484 if user_obj and user_obj.user_data.get('force_password_change'):
483 h.flash('You are required to change your password', 'warning',
485 h.flash('You are required to change your password', 'warning',
484 ignore_duplicate=True)
486 ignore_duplicate=True)
485
487
486 skip_user_check_urls = [
488 skip_user_check_urls = [
487 'error.document', 'login.logout', 'login.index',
489 'error.document', 'login.logout', 'login.index',
488 'admin/my_account.my_account_password',
490 'admin/my_account.my_account_password',
489 'admin/my_account.my_account_password_update'
491 'admin/my_account.my_account_password_update'
490 ]
492 ]
491 if _route_name not in skip_user_check_urls:
493 if _route_name not in skip_user_check_urls:
492 return self._dispatch_redirect(
494 return self._dispatch_redirect(
493 url('my_account_password'), environ, start_response)
495 url('my_account_password'), environ, start_response)
494
496
495 return WSGIController.__call__(self, environ, start_response)
497 return WSGIController.__call__(self, environ, start_response)
496
498
497
499
498 class BaseRepoController(BaseController):
500 class BaseRepoController(BaseController):
499 """
501 """
500 Base class for controllers responsible for loading all needed data for
502 Base class for controllers responsible for loading all needed data for
501 repository loaded items are
503 repository loaded items are
502
504
503 c.rhodecode_repo: instance of scm repository
505 c.rhodecode_repo: instance of scm repository
504 c.rhodecode_db_repo: instance of db
506 c.rhodecode_db_repo: instance of db
505 c.repository_requirements_missing: shows that repository specific data
507 c.repository_requirements_missing: shows that repository specific data
506 could not be displayed due to the missing requirements
508 could not be displayed due to the missing requirements
507 c.repository_pull_requests: show number of open pull requests
509 c.repository_pull_requests: show number of open pull requests
508 """
510 """
509
511
510 def __before__(self):
512 def __before__(self):
511 super(BaseRepoController, self).__before__()
513 super(BaseRepoController, self).__before__()
512 if c.repo_name: # extracted from routes
514 if c.repo_name: # extracted from routes
513 db_repo = Repository.get_by_repo_name(c.repo_name)
515 db_repo = Repository.get_by_repo_name(c.repo_name)
514 if not db_repo:
516 if not db_repo:
515 return
517 return
516
518
517 log.debug(
519 log.debug(
518 'Found repository in database %s with state `%s`',
520 'Found repository in database %s with state `%s`',
519 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
521 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
520 route = getattr(request.environ.get('routes.route'), 'name', '')
522 route = getattr(request.environ.get('routes.route'), 'name', '')
521
523
522 # allow to delete repos that are somehow damages in filesystem
524 # allow to delete repos that are somehow damages in filesystem
523 if route in ['delete_repo']:
525 if route in ['delete_repo']:
524 return
526 return
525
527
526 if db_repo.repo_state in [Repository.STATE_PENDING]:
528 if db_repo.repo_state in [Repository.STATE_PENDING]:
527 if route in ['repo_creating_home']:
529 if route in ['repo_creating_home']:
528 return
530 return
529 check_url = url('repo_creating_home', repo_name=c.repo_name)
531 check_url = url('repo_creating_home', repo_name=c.repo_name)
530 return redirect(check_url)
532 return redirect(check_url)
531
533
532 self.rhodecode_db_repo = db_repo
534 self.rhodecode_db_repo = db_repo
533
535
534 missing_requirements = False
536 missing_requirements = False
535 try:
537 try:
536 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
538 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
537 except RepositoryRequirementError as e:
539 except RepositoryRequirementError as e:
538 missing_requirements = True
540 missing_requirements = True
539 self._handle_missing_requirements(e)
541 self._handle_missing_requirements(e)
540
542
541 if self.rhodecode_repo is None and not missing_requirements:
543 if self.rhodecode_repo is None and not missing_requirements:
542 log.error('%s this repository is present in database but it '
544 log.error('%s this repository is present in database but it '
543 'cannot be created as an scm instance', c.repo_name)
545 'cannot be created as an scm instance', c.repo_name)
544
546
545 h.flash(_(
547 h.flash(_(
546 "The repository at %(repo_name)s cannot be located.") %
548 "The repository at %(repo_name)s cannot be located.") %
547 {'repo_name': c.repo_name},
549 {'repo_name': c.repo_name},
548 category='error', ignore_duplicate=True)
550 category='error', ignore_duplicate=True)
549 redirect(url('home'))
551 redirect(url('home'))
550
552
551 # update last change according to VCS data
553 # update last change according to VCS data
552 if not missing_requirements:
554 if not missing_requirements:
553 commit = db_repo.get_commit(
555 commit = db_repo.get_commit(
554 pre_load=["author", "date", "message", "parents"])
556 pre_load=["author", "date", "message", "parents"])
555 db_repo.update_commit_cache(commit)
557 db_repo.update_commit_cache(commit)
556
558
557 # Prepare context
559 # Prepare context
558 c.rhodecode_db_repo = db_repo
560 c.rhodecode_db_repo = db_repo
559 c.rhodecode_repo = self.rhodecode_repo
561 c.rhodecode_repo = self.rhodecode_repo
560 c.repository_requirements_missing = missing_requirements
562 c.repository_requirements_missing = missing_requirements
561
563
562 self._update_global_counters(self.scm_model, db_repo)
564 self._update_global_counters(self.scm_model, db_repo)
563
565
564 def _update_global_counters(self, scm_model, db_repo):
566 def _update_global_counters(self, scm_model, db_repo):
565 """
567 """
566 Base variables that are exposed to every page of repository
568 Base variables that are exposed to every page of repository
567 """
569 """
568 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
570 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
569
571
570 def _handle_missing_requirements(self, error):
572 def _handle_missing_requirements(self, error):
571 self.rhodecode_repo = None
573 self.rhodecode_repo = None
572 log.error(
574 log.error(
573 'Requirements are missing for repository %s: %s',
575 'Requirements are missing for repository %s: %s',
574 c.repo_name, error.message)
576 c.repo_name, error.message)
575
577
576 summary_url = url('summary_home', repo_name=c.repo_name)
578 summary_url = url('summary_home', repo_name=c.repo_name)
577 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
579 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
578 settings_update_url = url('repo', repo_name=c.repo_name)
580 settings_update_url = url('repo', repo_name=c.repo_name)
579 path = request.path
581 path = request.path
580 should_redirect = (
582 should_redirect = (
581 path not in (summary_url, settings_update_url)
583 path not in (summary_url, settings_update_url)
582 and '/settings' not in path or path == statistics_url
584 and '/settings' not in path or path == statistics_url
583 )
585 )
584 if should_redirect:
586 if should_redirect:
585 redirect(summary_url)
587 redirect(summary_url)
@@ -1,442 +1,444 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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 from functools import wraps
29 from functools import wraps
30
30
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from webob.exc import (
32 from webob.exc import (
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34
34
35 import rhodecode
35 import rhodecode
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 NotAllowedToCreateUserError)
41 NotAllowedToCreateUserError)
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.middleware import appenlight
43 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware.utils import scm_app
44 from rhodecode.lib.middleware.utils import scm_app
45 from rhodecode.lib.utils import (
45 from rhodecode.lib.utils import (
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import User, Repository
50 from rhodecode.model.db import User, Repository
51 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.settings import SettingsModel
52 from rhodecode.model.settings import SettingsModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def initialize_generator(factory):
57 def initialize_generator(factory):
58 """
58 """
59 Initializes the returned generator by draining its first element.
59 Initializes the returned generator by draining its first element.
60
60
61 This can be used to give a generator an initializer, which is the code
61 This can be used to give a generator an initializer, which is the code
62 up to the first yield statement. This decorator enforces that the first
62 up to the first yield statement. This decorator enforces that the first
63 produced element has the value ``"__init__"`` to make its special
63 produced element has the value ``"__init__"`` to make its special
64 purpose very explicit in the using code.
64 purpose very explicit in the using code.
65 """
65 """
66
66
67 @wraps(factory)
67 @wraps(factory)
68 def wrapper(*args, **kwargs):
68 def wrapper(*args, **kwargs):
69 gen = factory(*args, **kwargs)
69 gen = factory(*args, **kwargs)
70 try:
70 try:
71 init = gen.next()
71 init = gen.next()
72 except StopIteration:
72 except StopIteration:
73 raise ValueError('Generator must yield at least one element.')
73 raise ValueError('Generator must yield at least one element.')
74 if init != "__init__":
74 if init != "__init__":
75 raise ValueError('First yielded element must be "__init__".')
75 raise ValueError('First yielded element must be "__init__".')
76 return gen
76 return gen
77 return wrapper
77 return wrapper
78
78
79
79
80 class SimpleVCS(object):
80 class SimpleVCS(object):
81 """Common functionality for SCM HTTP handlers."""
81 """Common functionality for SCM HTTP handlers."""
82
82
83 SCM = 'unknown'
83 SCM = 'unknown'
84
84
85 def __init__(self, application, config):
85 def __init__(self, application, config, registry):
86 self.registry = registry
86 self.application = application
87 self.application = application
87 self.config = config
88 self.config = config
88 # base path of repo locations
89 # base path of repo locations
89 self.basepath = get_rhodecode_base_path()
90 self.basepath = get_rhodecode_base_path()
90 # authenticate this VCS request using authfunc
91 # authenticate this VCS request using authfunc
91 auth_ret_code_detection = \
92 auth_ret_code_detection = \
92 str2bool(self.config.get('auth_ret_code_detection', False))
93 str2bool(self.config.get('auth_ret_code_detection', False))
93 self.authenticate = BasicAuth('', authenticate,
94 self.authenticate = BasicAuth(
94 config.get('auth_ret_code'),
95 '', authenticate, registry, config.get('auth_ret_code'),
95 auth_ret_code_detection)
96 auth_ret_code_detection)
96 self.ip_addr = '0.0.0.0'
97 self.ip_addr = '0.0.0.0'
97
98
98 @property
99 @property
99 def scm_app(self):
100 def scm_app(self):
100 custom_implementation = self.config.get('vcs.scm_app_implementation')
101 custom_implementation = self.config.get('vcs.scm_app_implementation')
101 if custom_implementation:
102 if custom_implementation:
102 log.info(
103 log.info(
103 "Using custom implementation of scm_app: %s",
104 "Using custom implementation of scm_app: %s",
104 custom_implementation)
105 custom_implementation)
105 scm_app_impl = importlib.import_module(custom_implementation)
106 scm_app_impl = importlib.import_module(custom_implementation)
106 else:
107 else:
107 scm_app_impl = scm_app
108 scm_app_impl = scm_app
108 return scm_app_impl
109 return scm_app_impl
109
110
110 def _get_by_id(self, repo_name):
111 def _get_by_id(self, repo_name):
111 """
112 """
112 Gets a special pattern _<ID> from clone url and tries to replace it
113 Gets a special pattern _<ID> from clone url and tries to replace it
113 with a repository_name for support of _<ID> non changable urls
114 with a repository_name for support of _<ID> non changable urls
114
115
115 :param repo_name:
116 :param repo_name:
116 """
117 """
117
118
118 data = repo_name.split('/')
119 data = repo_name.split('/')
119 if len(data) >= 2:
120 if len(data) >= 2:
120 from rhodecode.model.repo import RepoModel
121 from rhodecode.model.repo import RepoModel
121 by_id_match = RepoModel().get_repo_by_id(repo_name)
122 by_id_match = RepoModel().get_repo_by_id(repo_name)
122 if by_id_match:
123 if by_id_match:
123 data[1] = by_id_match.repo_name
124 data[1] = by_id_match.repo_name
124
125
125 return safe_str('/'.join(data))
126 return safe_str('/'.join(data))
126
127
127 def _invalidate_cache(self, repo_name):
128 def _invalidate_cache(self, repo_name):
128 """
129 """
129 Set's cache for this repository for invalidation on next access
130 Set's cache for this repository for invalidation on next access
130
131
131 :param repo_name: full repo name, also a cache key
132 :param repo_name: full repo name, also a cache key
132 """
133 """
133 ScmModel().mark_for_invalidation(repo_name)
134 ScmModel().mark_for_invalidation(repo_name)
134
135
135 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
136 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
136 db_repo = Repository.get_by_repo_name(repo_name)
137 db_repo = Repository.get_by_repo_name(repo_name)
137 if not db_repo:
138 if not db_repo:
138 log.debug('Repository `%s` not found inside the database.',
139 log.debug('Repository `%s` not found inside the database.',
139 repo_name)
140 repo_name)
140 return False
141 return False
141
142
142 if db_repo.repo_type != scm_type:
143 if db_repo.repo_type != scm_type:
143 log.warning(
144 log.warning(
144 'Repository `%s` have incorrect scm_type, expected %s got %s',
145 'Repository `%s` have incorrect scm_type, expected %s got %s',
145 repo_name, db_repo.repo_type, scm_type)
146 repo_name, db_repo.repo_type, scm_type)
146 return False
147 return False
147
148
148 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
149 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
149
150
150 def valid_and_active_user(self, user):
151 def valid_and_active_user(self, user):
151 """
152 """
152 Checks if that user is not empty, and if it's actually object it checks
153 Checks if that user is not empty, and if it's actually object it checks
153 if he's active.
154 if he's active.
154
155
155 :param user: user object or None
156 :param user: user object or None
156 :return: boolean
157 :return: boolean
157 """
158 """
158 if user is None:
159 if user is None:
159 return False
160 return False
160
161
161 elif user.active:
162 elif user.active:
162 return True
163 return True
163
164
164 return False
165 return False
165
166
166 def _check_permission(self, action, user, repo_name, ip_addr=None):
167 def _check_permission(self, action, user, repo_name, ip_addr=None):
167 """
168 """
168 Checks permissions using action (push/pull) user and repository
169 Checks permissions using action (push/pull) user and repository
169 name
170 name
170
171
171 :param action: push or pull action
172 :param action: push or pull action
172 :param user: user instance
173 :param user: user instance
173 :param repo_name: repository name
174 :param repo_name: repository name
174 """
175 """
175 # check IP
176 # check IP
176 inherit = user.inherit_default_permissions
177 inherit = user.inherit_default_permissions
177 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
178 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
178 inherit_from_default=inherit)
179 inherit_from_default=inherit)
179 if ip_allowed:
180 if ip_allowed:
180 log.info('Access for IP:%s allowed', ip_addr)
181 log.info('Access for IP:%s allowed', ip_addr)
181 else:
182 else:
182 return False
183 return False
183
184
184 if action == 'push':
185 if action == 'push':
185 if not HasPermissionAnyMiddleware('repository.write',
186 if not HasPermissionAnyMiddleware('repository.write',
186 'repository.admin')(user,
187 'repository.admin')(user,
187 repo_name):
188 repo_name):
188 return False
189 return False
189
190
190 else:
191 else:
191 # any other action need at least read permission
192 # any other action need at least read permission
192 if not HasPermissionAnyMiddleware('repository.read',
193 if not HasPermissionAnyMiddleware('repository.read',
193 'repository.write',
194 'repository.write',
194 'repository.admin')(user,
195 'repository.admin')(user,
195 repo_name):
196 repo_name):
196 return False
197 return False
197
198
198 return True
199 return True
199
200
200 def _check_ssl(self, environ, start_response):
201 def _check_ssl(self, environ, start_response):
201 """
202 """
202 Checks the SSL check flag and returns False if SSL is not present
203 Checks the SSL check flag and returns False if SSL is not present
203 and required True otherwise
204 and required True otherwise
204 """
205 """
205 org_proto = environ['wsgi._org_proto']
206 org_proto = environ['wsgi._org_proto']
206 # check if we have SSL required ! if not it's a bad request !
207 # check if we have SSL required ! if not it's a bad request !
207 require_ssl = str2bool(
208 require_ssl = str2bool(
208 SettingsModel().get_ui_by_key('push_ssl').ui_value)
209 SettingsModel().get_ui_by_key('push_ssl').ui_value)
209 if require_ssl and org_proto == 'http':
210 if require_ssl and org_proto == 'http':
210 log.debug('proto is %s and SSL is required BAD REQUEST !',
211 log.debug('proto is %s and SSL is required BAD REQUEST !',
211 org_proto)
212 org_proto)
212 return False
213 return False
213 return True
214 return True
214
215
215 def __call__(self, environ, start_response):
216 def __call__(self, environ, start_response):
216 try:
217 try:
217 return self._handle_request(environ, start_response)
218 return self._handle_request(environ, start_response)
218 except Exception:
219 except Exception:
219 log.exception("Exception while handling request")
220 log.exception("Exception while handling request")
220 appenlight.track_exception(environ)
221 appenlight.track_exception(environ)
221 return HTTPInternalServerError()(environ, start_response)
222 return HTTPInternalServerError()(environ, start_response)
222 finally:
223 finally:
223 meta.Session.remove()
224 meta.Session.remove()
224
225
225 def _handle_request(self, environ, start_response):
226 def _handle_request(self, environ, start_response):
226
227
227 if not self._check_ssl(environ, start_response):
228 if not self._check_ssl(environ, start_response):
228 reason = ('SSL required, while RhodeCode was unable '
229 reason = ('SSL required, while RhodeCode was unable '
229 'to detect this as SSL request')
230 'to detect this as SSL request')
230 log.debug('User not allowed to proceed, %s', reason)
231 log.debug('User not allowed to proceed, %s', reason)
231 return HTTPNotAcceptable(reason)(environ, start_response)
232 return HTTPNotAcceptable(reason)(environ, start_response)
232
233
233 ip_addr = get_ip_addr(environ)
234 ip_addr = get_ip_addr(environ)
234 username = None
235 username = None
235
236
236 # skip passing error to error controller
237 # skip passing error to error controller
237 environ['pylons.status_code_redirect'] = True
238 environ['pylons.status_code_redirect'] = True
238
239
239 # ======================================================================
240 # ======================================================================
240 # EXTRACT REPOSITORY NAME FROM ENV
241 # EXTRACT REPOSITORY NAME FROM ENV
241 # ======================================================================
242 # ======================================================================
242 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
243 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
243 repo_name = self._get_repository_name(environ)
244 repo_name = self._get_repository_name(environ)
244 environ['REPO_NAME'] = repo_name
245 environ['REPO_NAME'] = repo_name
245 log.debug('Extracted repo name is %s', repo_name)
246 log.debug('Extracted repo name is %s', repo_name)
246
247
247 # check for type, presence in database and on filesystem
248 # check for type, presence in database and on filesystem
248 if not self.is_valid_and_existing_repo(
249 if not self.is_valid_and_existing_repo(
249 repo_name, self.basepath, self.SCM):
250 repo_name, self.basepath, self.SCM):
250 return HTTPNotFound()(environ, start_response)
251 return HTTPNotFound()(environ, start_response)
251
252
252 # ======================================================================
253 # ======================================================================
253 # GET ACTION PULL or PUSH
254 # GET ACTION PULL or PUSH
254 # ======================================================================
255 # ======================================================================
255 action = self._get_action(environ)
256 action = self._get_action(environ)
256
257
257 # ======================================================================
258 # ======================================================================
258 # CHECK ANONYMOUS PERMISSION
259 # CHECK ANONYMOUS PERMISSION
259 # ======================================================================
260 # ======================================================================
260 if action in ['pull', 'push']:
261 if action in ['pull', 'push']:
261 anonymous_user = User.get_default_user()
262 anonymous_user = User.get_default_user()
262 username = anonymous_user.username
263 username = anonymous_user.username
263 if anonymous_user.active:
264 if anonymous_user.active:
264 # ONLY check permissions if the user is activated
265 # ONLY check permissions if the user is activated
265 anonymous_perm = self._check_permission(
266 anonymous_perm = self._check_permission(
266 action, anonymous_user, repo_name, ip_addr)
267 action, anonymous_user, repo_name, ip_addr)
267 else:
268 else:
268 anonymous_perm = False
269 anonymous_perm = False
269
270
270 if not anonymous_user.active or not anonymous_perm:
271 if not anonymous_user.active or not anonymous_perm:
271 if not anonymous_user.active:
272 if not anonymous_user.active:
272 log.debug('Anonymous access is disabled, running '
273 log.debug('Anonymous access is disabled, running '
273 'authentication')
274 'authentication')
274
275
275 if not anonymous_perm:
276 if not anonymous_perm:
276 log.debug('Not enough credentials to access this '
277 log.debug('Not enough credentials to access this '
277 'repository as anonymous user')
278 'repository as anonymous user')
278
279
279 username = None
280 username = None
280 # ==============================================================
281 # ==============================================================
281 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
282 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
282 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
283 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
283 # ==============================================================
284 # ==============================================================
284
285
285 # try to auth based on environ, container auth methods
286 # try to auth based on environ, container auth methods
286 log.debug('Running PRE-AUTH for container based authentication')
287 log.debug('Running PRE-AUTH for container based authentication')
287 pre_auth = authenticate('', '', environ,VCS_TYPE)
288 pre_auth = authenticate(
289 '', '', environ, VCS_TYPE, registry=self.registry)
288 if pre_auth and pre_auth.get('username'):
290 if pre_auth and pre_auth.get('username'):
289 username = pre_auth['username']
291 username = pre_auth['username']
290 log.debug('PRE-AUTH got %s as username', username)
292 log.debug('PRE-AUTH got %s as username', username)
291
293
292 # If not authenticated by the container, running basic auth
294 # If not authenticated by the container, running basic auth
293 if not username:
295 if not username:
294 self.authenticate.realm = get_rhodecode_realm()
296 self.authenticate.realm = get_rhodecode_realm()
295
297
296 try:
298 try:
297 result = self.authenticate(environ)
299 result = self.authenticate(environ)
298 except (UserCreationError, NotAllowedToCreateUserError) as e:
300 except (UserCreationError, NotAllowedToCreateUserError) as e:
299 log.error(e)
301 log.error(e)
300 reason = safe_str(e)
302 reason = safe_str(e)
301 return HTTPNotAcceptable(reason)(environ, start_response)
303 return HTTPNotAcceptable(reason)(environ, start_response)
302
304
303 if isinstance(result, str):
305 if isinstance(result, str):
304 AUTH_TYPE.update(environ, 'basic')
306 AUTH_TYPE.update(environ, 'basic')
305 REMOTE_USER.update(environ, result)
307 REMOTE_USER.update(environ, result)
306 username = result
308 username = result
307 else:
309 else:
308 return result.wsgi_application(environ, start_response)
310 return result.wsgi_application(environ, start_response)
309
311
310 # ==============================================================
312 # ==============================================================
311 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
313 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
312 # ==============================================================
314 # ==============================================================
313 user = User.get_by_username(username)
315 user = User.get_by_username(username)
314 if not self.valid_and_active_user(user):
316 if not self.valid_and_active_user(user):
315 return HTTPForbidden()(environ, start_response)
317 return HTTPForbidden()(environ, start_response)
316 username = user.username
318 username = user.username
317 user.update_lastactivity()
319 user.update_lastactivity()
318 meta.Session().commit()
320 meta.Session().commit()
319
321
320 # check user attributes for password change flag
322 # check user attributes for password change flag
321 user_obj = user
323 user_obj = user
322 if user_obj and user_obj.username != User.DEFAULT_USER and \
324 if user_obj and user_obj.username != User.DEFAULT_USER and \
323 user_obj.user_data.get('force_password_change'):
325 user_obj.user_data.get('force_password_change'):
324 reason = 'password change required'
326 reason = 'password change required'
325 log.debug('User not allowed to authenticate, %s', reason)
327 log.debug('User not allowed to authenticate, %s', reason)
326 return HTTPNotAcceptable(reason)(environ, start_response)
328 return HTTPNotAcceptable(reason)(environ, start_response)
327
329
328 # check permissions for this repository
330 # check permissions for this repository
329 perm = self._check_permission(action, user, repo_name, ip_addr)
331 perm = self._check_permission(action, user, repo_name, ip_addr)
330 if not perm:
332 if not perm:
331 return HTTPForbidden()(environ, start_response)
333 return HTTPForbidden()(environ, start_response)
332
334
333 # extras are injected into UI object and later available
335 # extras are injected into UI object and later available
334 # in hooks executed by rhodecode
336 # in hooks executed by rhodecode
335 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
337 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
336 extras = vcs_operation_context(
338 extras = vcs_operation_context(
337 environ, repo_name=repo_name, username=username,
339 environ, repo_name=repo_name, username=username,
338 action=action, scm=self.SCM,
340 action=action, scm=self.SCM,
339 check_locking=check_locking)
341 check_locking=check_locking)
340
342
341 # ======================================================================
343 # ======================================================================
342 # REQUEST HANDLING
344 # REQUEST HANDLING
343 # ======================================================================
345 # ======================================================================
344 str_repo_name = safe_str(repo_name)
346 str_repo_name = safe_str(repo_name)
345 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
347 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
346 log.debug('Repository path is %s', repo_path)
348 log.debug('Repository path is %s', repo_path)
347
349
348 fix_PATH()
350 fix_PATH()
349
351
350 log.info(
352 log.info(
351 '%s action on %s repo "%s" by "%s" from %s',
353 '%s action on %s repo "%s" by "%s" from %s',
352 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
354 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
353 return self._generate_vcs_response(
355 return self._generate_vcs_response(
354 environ, start_response, repo_path, repo_name, extras, action)
356 environ, start_response, repo_path, repo_name, extras, action)
355
357
356 @initialize_generator
358 @initialize_generator
357 def _generate_vcs_response(
359 def _generate_vcs_response(
358 self, environ, start_response, repo_path, repo_name, extras,
360 self, environ, start_response, repo_path, repo_name, extras,
359 action):
361 action):
360 """
362 """
361 Returns a generator for the response content.
363 Returns a generator for the response content.
362
364
363 This method is implemented as a generator, so that it can trigger
365 This method is implemented as a generator, so that it can trigger
364 the cache validation after all content sent back to the client. It
366 the cache validation after all content sent back to the client. It
365 also handles the locking exceptions which will be triggered when
367 also handles the locking exceptions which will be triggered when
366 the first chunk is produced by the underlying WSGI application.
368 the first chunk is produced by the underlying WSGI application.
367 """
369 """
368 callback_daemon, extras = self._prepare_callback_daemon(extras)
370 callback_daemon, extras = self._prepare_callback_daemon(extras)
369 config = self._create_config(extras, repo_name)
371 config = self._create_config(extras, repo_name)
370 log.debug('HOOKS extras is %s', extras)
372 log.debug('HOOKS extras is %s', extras)
371 app = self._create_wsgi_app(repo_path, repo_name, config)
373 app = self._create_wsgi_app(repo_path, repo_name, config)
372
374
373 try:
375 try:
374 with callback_daemon:
376 with callback_daemon:
375 try:
377 try:
376 response = app(environ, start_response)
378 response = app(environ, start_response)
377 finally:
379 finally:
378 # This statement works together with the decorator
380 # This statement works together with the decorator
379 # "initialize_generator" above. The decorator ensures that
381 # "initialize_generator" above. The decorator ensures that
380 # we hit the first yield statement before the generator is
382 # we hit the first yield statement before the generator is
381 # returned back to the WSGI server. This is needed to
383 # returned back to the WSGI server. This is needed to
382 # ensure that the call to "app" above triggers the
384 # ensure that the call to "app" above triggers the
383 # needed callback to "start_response" before the
385 # needed callback to "start_response" before the
384 # generator is actually used.
386 # generator is actually used.
385 yield "__init__"
387 yield "__init__"
386
388
387 for chunk in response:
389 for chunk in response:
388 yield chunk
390 yield chunk
389 except Exception as exc:
391 except Exception as exc:
390 # TODO: johbo: Improve "translating" back the exception.
392 # TODO: johbo: Improve "translating" back the exception.
391 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
393 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
392 exc = HTTPLockedRC(*exc.args)
394 exc = HTTPLockedRC(*exc.args)
393 _code = rhodecode.CONFIG.get('lock_ret_code')
395 _code = rhodecode.CONFIG.get('lock_ret_code')
394 log.debug('Repository LOCKED ret code %s!', (_code,))
396 log.debug('Repository LOCKED ret code %s!', (_code,))
395 elif getattr(exc, '_vcs_kind', None) == 'requirement':
397 elif getattr(exc, '_vcs_kind', None) == 'requirement':
396 log.debug(
398 log.debug(
397 'Repository requires features unknown to this Mercurial')
399 'Repository requires features unknown to this Mercurial')
398 exc = HTTPRequirementError(*exc.args)
400 exc = HTTPRequirementError(*exc.args)
399 else:
401 else:
400 raise
402 raise
401
403
402 for chunk in exc(environ, start_response):
404 for chunk in exc(environ, start_response):
403 yield chunk
405 yield chunk
404 finally:
406 finally:
405 # invalidate cache on push
407 # invalidate cache on push
406 if action == 'push':
408 if action == 'push':
407 self._invalidate_cache(repo_name)
409 self._invalidate_cache(repo_name)
408
410
409 def _get_repository_name(self, environ):
411 def _get_repository_name(self, environ):
410 """Get repository name out of the environmnent
412 """Get repository name out of the environmnent
411
413
412 :param environ: WSGI environment
414 :param environ: WSGI environment
413 """
415 """
414 raise NotImplementedError()
416 raise NotImplementedError()
415
417
416 def _get_action(self, environ):
418 def _get_action(self, environ):
417 """Map request commands into a pull or push command.
419 """Map request commands into a pull or push command.
418
420
419 :param environ: WSGI environment
421 :param environ: WSGI environment
420 """
422 """
421 raise NotImplementedError()
423 raise NotImplementedError()
422
424
423 def _create_wsgi_app(self, repo_path, repo_name, config):
425 def _create_wsgi_app(self, repo_path, repo_name, config):
424 """Return the WSGI app that will finally handle the request."""
426 """Return the WSGI app that will finally handle the request."""
425 raise NotImplementedError()
427 raise NotImplementedError()
426
428
427 def _create_config(self, extras, repo_name):
429 def _create_config(self, extras, repo_name):
428 """Create a Pyro safe config representation."""
430 """Create a Pyro safe config representation."""
429 raise NotImplementedError()
431 raise NotImplementedError()
430
432
431 def _prepare_callback_daemon(self, extras):
433 def _prepare_callback_daemon(self, extras):
432 return prepare_callback_daemon(
434 return prepare_callback_daemon(
433 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
435 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
434 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
436 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
435
437
436
438
437 def _should_check_locking(query_string):
439 def _should_check_locking(query_string):
438 # this is kind of hacky, but due to how mercurial handles client-server
440 # this is kind of hacky, but due to how mercurial handles client-server
439 # server see all operation on commit; bookmarks, phases and
441 # server see all operation on commit; bookmarks, phases and
440 # obsolescence marker in different transaction, we don't want to check
442 # obsolescence marker in different transaction, we don't want to check
441 # locking on those
443 # locking on those
442 return query_string not in ['cmd=listkeys']
444 return query_string not in ['cmd=listkeys']
@@ -1,160 +1,161 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 gzip
21 import gzip
22 import shutil
22 import shutil
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import urlparse
25 import urlparse
26
26
27 import rhodecode
27 import rhodecode
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
30 from rhodecode.lib.middleware.simplehg import SimpleHg
30 from rhodecode.lib.middleware.simplehg import SimpleHg
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def is_git(environ):
37 def is_git(environ):
38 """
38 """
39 Returns True if requests should be handled by GIT wsgi middleware
39 Returns True if requests should be handled by GIT wsgi middleware
40 """
40 """
41 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
41 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
42 log.debug(
42 log.debug(
43 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
43 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
44 is_git_path is not None)
44 is_git_path is not None)
45
45
46 return is_git_path
46 return is_git_path
47
47
48
48
49 def is_hg(environ):
49 def is_hg(environ):
50 """
50 """
51 Returns True if requests target is mercurial server - header
51 Returns True if requests target is mercurial server - header
52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
53 """
53 """
54 is_hg_path = False
54 is_hg_path = False
55
55
56 http_accept = environ.get('HTTP_ACCEPT')
56 http_accept = environ.get('HTTP_ACCEPT')
57
57
58 if http_accept and http_accept.startswith('application/mercurial'):
58 if http_accept and http_accept.startswith('application/mercurial'):
59 query = urlparse.parse_qs(environ['QUERY_STRING'])
59 query = urlparse.parse_qs(environ['QUERY_STRING'])
60 if 'cmd' in query:
60 if 'cmd' in query:
61 is_hg_path = True
61 is_hg_path = True
62
62
63 log.debug(
63 log.debug(
64 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
64 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
65 is_hg_path)
65 is_hg_path)
66
66
67 return is_hg_path
67 return is_hg_path
68
68
69
69
70 def is_svn(environ):
70 def is_svn(environ):
71 """
71 """
72 Returns True if requests target is Subversion server
72 Returns True if requests target is Subversion server
73 """
73 """
74 http_dav = environ.get('HTTP_DAV', '')
74 http_dav = environ.get('HTTP_DAV', '')
75 magic_path_segment = rhodecode.CONFIG.get(
75 magic_path_segment = rhodecode.CONFIG.get(
76 'rhodecode_subversion_magic_path', '/!svn')
76 'rhodecode_subversion_magic_path', '/!svn')
77 is_svn_path = (
77 is_svn_path = (
78 'subversion' in http_dav or
78 'subversion' in http_dav or
79 magic_path_segment in environ['PATH_INFO'])
79 magic_path_segment in environ['PATH_INFO'])
80 log.debug(
80 log.debug(
81 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
81 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
82 is_svn_path)
82 is_svn_path)
83
83
84 return is_svn_path
84 return is_svn_path
85
85
86
86
87 class GunzipMiddleware(object):
87 class GunzipMiddleware(object):
88 """
88 """
89 WSGI middleware that unzips gzip-encoded requests before
89 WSGI middleware that unzips gzip-encoded requests before
90 passing on to the underlying application.
90 passing on to the underlying application.
91 """
91 """
92
92
93 def __init__(self, application):
93 def __init__(self, application):
94 self.app = application
94 self.app = application
95
95
96 def __call__(self, environ, start_response):
96 def __call__(self, environ, start_response):
97 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
97 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
98
98
99 if b'gzip' in accepts_encoding_header:
99 if b'gzip' in accepts_encoding_header:
100 log.debug('gzip detected, now running gunzip wrapper')
100 log.debug('gzip detected, now running gunzip wrapper')
101 wsgi_input = environ['wsgi.input']
101 wsgi_input = environ['wsgi.input']
102
102
103 if not hasattr(environ['wsgi.input'], 'seek'):
103 if not hasattr(environ['wsgi.input'], 'seek'):
104 # The gzip implementation in the standard library of Python 2.x
104 # The gzip implementation in the standard library of Python 2.x
105 # requires the '.seek()' and '.tell()' methods to be available
105 # requires the '.seek()' and '.tell()' methods to be available
106 # on the input stream. Read the data into a temporary file to
106 # on the input stream. Read the data into a temporary file to
107 # work around this limitation.
107 # work around this limitation.
108
108
109 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
109 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
110 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
110 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
111 wsgi_input.seek(0)
111 wsgi_input.seek(0)
112
112
113 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
113 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
114 # since we "Ungzipped" the content we say now it's no longer gzip
114 # since we "Ungzipped" the content we say now it's no longer gzip
115 # content encoding
115 # content encoding
116 del environ['HTTP_CONTENT_ENCODING']
116 del environ['HTTP_CONTENT_ENCODING']
117
117
118 # content length has changes ? or i'm not sure
118 # content length has changes ? or i'm not sure
119 if 'CONTENT_LENGTH' in environ:
119 if 'CONTENT_LENGTH' in environ:
120 del environ['CONTENT_LENGTH']
120 del environ['CONTENT_LENGTH']
121 else:
121 else:
122 log.debug('content not gzipped, gzipMiddleware passing '
122 log.debug('content not gzipped, gzipMiddleware passing '
123 'request further')
123 'request further')
124 return self.app(environ, start_response)
124 return self.app(environ, start_response)
125
125
126
126
127 class VCSMiddleware(object):
127 class VCSMiddleware(object):
128
128
129 def __init__(self, app, config, appenlight_client):
129 def __init__(self, app, config, appenlight_client, registry):
130 self.application = app
130 self.application = app
131 self.config = config
131 self.config = config
132 self.appenlight_client = appenlight_client
132 self.appenlight_client = appenlight_client
133 self.registry = registry
133
134
134 def _get_handler_app(self, environ):
135 def _get_handler_app(self, environ):
135 app = None
136 app = None
136 if is_hg(environ):
137 if is_hg(environ):
137 app = SimpleHg(self.application, self.config)
138 app = SimpleHg(self.application, self.config, self.registry)
138
139
139 if is_git(environ):
140 if is_git(environ):
140 app = SimpleGit(self.application, self.config)
141 app = SimpleGit(self.application, self.config, self.registry)
141
142
142 proxy_svn = rhodecode.CONFIG.get(
143 proxy_svn = rhodecode.CONFIG.get(
143 'rhodecode_proxy_subversion_http_requests', False)
144 'rhodecode_proxy_subversion_http_requests', False)
144 if proxy_svn and is_svn(environ):
145 if proxy_svn and is_svn(environ):
145 app = SimpleSvn(self.application, self.config)
146 app = SimpleSvn(self.application, self.config, self.registry)
146
147
147 if app:
148 if app:
148 app = GunzipMiddleware(app)
149 app = GunzipMiddleware(app)
149 app, _ = wrap_in_appenlight_if_enabled(
150 app, _ = wrap_in_appenlight_if_enabled(
150 app, self.config, self.appenlight_client)
151 app, self.config, self.appenlight_client)
151
152
152 return app
153 return app
153
154
154 def __call__(self, environ, start_response):
155 def __call__(self, environ, start_response):
155 # check if we handle one of interesting protocols ?
156 # check if we handle one of interesting protocols ?
156 vcs_handler = self._get_handler_app(environ)
157 vcs_handler = self._get_handler_app(environ)
157 if vcs_handler:
158 if vcs_handler:
158 return vcs_handler(environ, start_response)
159 return vcs_handler(environ, start_response)
159
160
160 return self.application(environ, start_response)
161 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now