##// END OF EJS Templates
vcs: Minimal change to expose the shadow repository...
johbo -
r887:175782be default
parent child Browse files
Show More
@@ -1,444 +1,453 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.lib.vcs.backends import base
49 from rhodecode.lib.vcs.backends import base
50 from rhodecode.model import meta
50 from rhodecode.model import meta
51 from rhodecode.model.db import User, Repository
51 from rhodecode.model.db import User, Repository
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def initialize_generator(factory):
58 def initialize_generator(factory):
59 """
59 """
60 Initializes the returned generator by draining its first element.
60 Initializes the returned generator by draining its first element.
61
61
62 This can be used to give a generator an initializer, which is the code
62 This can be used to give a generator an initializer, which is the code
63 up to the first yield statement. This decorator enforces that the first
63 up to the first yield statement. This decorator enforces that the first
64 produced element has the value ``"__init__"`` to make its special
64 produced element has the value ``"__init__"`` to make its special
65 purpose very explicit in the using code.
65 purpose very explicit in the using code.
66 """
66 """
67
67
68 @wraps(factory)
68 @wraps(factory)
69 def wrapper(*args, **kwargs):
69 def wrapper(*args, **kwargs):
70 gen = factory(*args, **kwargs)
70 gen = factory(*args, **kwargs)
71 try:
71 try:
72 init = gen.next()
72 init = gen.next()
73 except StopIteration:
73 except StopIteration:
74 raise ValueError('Generator must yield at least one element.')
74 raise ValueError('Generator must yield at least one element.')
75 if init != "__init__":
75 if init != "__init__":
76 raise ValueError('First yielded element must be "__init__".')
76 raise ValueError('First yielded element must be "__init__".')
77 return gen
77 return gen
78 return wrapper
78 return wrapper
79
79
80
80
81 class SimpleVCS(object):
81 class SimpleVCS(object):
82 """Common functionality for SCM HTTP handlers."""
82 """Common functionality for SCM HTTP handlers."""
83
83
84 SCM = 'unknown'
84 SCM = 'unknown'
85
85
86 acl_repo_name = None
87 url_repo_name = None
88 vcs_repo_name = None
89
86 def __init__(self, application, config, registry):
90 def __init__(self, application, config, registry):
87 self.registry = registry
91 self.registry = registry
88 self.application = application
92 self.application = application
89 self.config = config
93 self.config = config
90 # re-populated by specialized middleware
94 # re-populated by specialized middleware
91 self.repo_name = None
92 self.repo_vcs_config = base.Config()
95 self.repo_vcs_config = base.Config()
93
96
94 # base path of repo locations
97 # base path of repo locations
95 self.basepath = get_rhodecode_base_path()
98 self.basepath = get_rhodecode_base_path()
96 # authenticate this VCS request using authfunc
99 # authenticate this VCS request using authfunc
97 auth_ret_code_detection = \
100 auth_ret_code_detection = \
98 str2bool(self.config.get('auth_ret_code_detection', False))
101 str2bool(self.config.get('auth_ret_code_detection', False))
99 self.authenticate = BasicAuth(
102 self.authenticate = BasicAuth(
100 '', authenticate, registry, config.get('auth_ret_code'),
103 '', authenticate, registry, config.get('auth_ret_code'),
101 auth_ret_code_detection)
104 auth_ret_code_detection)
102 self.ip_addr = '0.0.0.0'
105 self.ip_addr = '0.0.0.0'
103
106
104 @property
107 @property
108 def repo_name(self):
109 # TODO: johbo: Remove, switch to correct repo name attribute
110 return self.acl_repo_name
111
112 @property
105 def scm_app(self):
113 def scm_app(self):
106 custom_implementation = self.config.get('vcs.scm_app_implementation')
114 custom_implementation = self.config.get('vcs.scm_app_implementation')
107 if custom_implementation and custom_implementation != 'pyro4':
115 if custom_implementation and custom_implementation != 'pyro4':
108 log.info(
116 log.info(
109 "Using custom implementation of scm_app: %s",
117 "Using custom implementation of scm_app: %s",
110 custom_implementation)
118 custom_implementation)
111 scm_app_impl = importlib.import_module(custom_implementation)
119 scm_app_impl = importlib.import_module(custom_implementation)
112 else:
120 else:
113 scm_app_impl = scm_app
121 scm_app_impl = scm_app
114 return scm_app_impl
122 return scm_app_impl
115
123
116 def _get_by_id(self, repo_name):
124 def _get_by_id(self, repo_name):
117 """
125 """
118 Gets a special pattern _<ID> from clone url and tries to replace it
126 Gets a special pattern _<ID> from clone url and tries to replace it
119 with a repository_name for support of _<ID> non changeable urls
127 with a repository_name for support of _<ID> non changeable urls
120 """
128 """
121
129
122 data = repo_name.split('/')
130 data = repo_name.split('/')
123 if len(data) >= 2:
131 if len(data) >= 2:
124 from rhodecode.model.repo import RepoModel
132 from rhodecode.model.repo import RepoModel
125 by_id_match = RepoModel().get_repo_by_id(repo_name)
133 by_id_match = RepoModel().get_repo_by_id(repo_name)
126 if by_id_match:
134 if by_id_match:
127 data[1] = by_id_match.repo_name
135 data[1] = by_id_match.repo_name
128
136
129 return safe_str('/'.join(data))
137 return safe_str('/'.join(data))
130
138
131 def _invalidate_cache(self, repo_name):
139 def _invalidate_cache(self, repo_name):
132 """
140 """
133 Set's cache for this repository for invalidation on next access
141 Set's cache for this repository for invalidation on next access
134
142
135 :param repo_name: full repo name, also a cache key
143 :param repo_name: full repo name, also a cache key
136 """
144 """
137 ScmModel().mark_for_invalidation(repo_name)
145 ScmModel().mark_for_invalidation(repo_name)
138
146
139 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
147 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
140 db_repo = Repository.get_by_repo_name(repo_name)
148 db_repo = Repository.get_by_repo_name(repo_name)
141 if not db_repo:
149 if not db_repo:
142 log.debug('Repository `%s` not found inside the database.',
150 log.debug('Repository `%s` not found inside the database.',
143 repo_name)
151 repo_name)
144 return False
152 return False
145
153
146 if db_repo.repo_type != scm_type:
154 if db_repo.repo_type != scm_type:
147 log.warning(
155 log.warning(
148 'Repository `%s` have incorrect scm_type, expected %s got %s',
156 'Repository `%s` have incorrect scm_type, expected %s got %s',
149 repo_name, db_repo.repo_type, scm_type)
157 repo_name, db_repo.repo_type, scm_type)
150 return False
158 return False
151
159
152 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
160 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
153
161
154 def valid_and_active_user(self, user):
162 def valid_and_active_user(self, user):
155 """
163 """
156 Checks if that user is not empty, and if it's actually object it checks
164 Checks if that user is not empty, and if it's actually object it checks
157 if he's active.
165 if he's active.
158
166
159 :param user: user object or None
167 :param user: user object or None
160 :return: boolean
168 :return: boolean
161 """
169 """
162 if user is None:
170 if user is None:
163 return False
171 return False
164
172
165 elif user.active:
173 elif user.active:
166 return True
174 return True
167
175
168 return False
176 return False
169
177
170 def _check_permission(self, action, user, repo_name, ip_addr=None):
178 def _check_permission(self, action, user, repo_name, ip_addr=None):
171 """
179 """
172 Checks permissions using action (push/pull) user and repository
180 Checks permissions using action (push/pull) user and repository
173 name
181 name
174
182
175 :param action: push or pull action
183 :param action: push or pull action
176 :param user: user instance
184 :param user: user instance
177 :param repo_name: repository name
185 :param repo_name: repository name
178 """
186 """
179 # check IP
187 # check IP
180 inherit = user.inherit_default_permissions
188 inherit = user.inherit_default_permissions
181 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
189 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
182 inherit_from_default=inherit)
190 inherit_from_default=inherit)
183 if ip_allowed:
191 if ip_allowed:
184 log.info('Access for IP:%s allowed', ip_addr)
192 log.info('Access for IP:%s allowed', ip_addr)
185 else:
193 else:
186 return False
194 return False
187
195
188 if action == 'push':
196 if action == 'push':
189 if not HasPermissionAnyMiddleware('repository.write',
197 if not HasPermissionAnyMiddleware('repository.write',
190 'repository.admin')(user,
198 'repository.admin')(user,
191 repo_name):
199 repo_name):
192 return False
200 return False
193
201
194 else:
202 else:
195 # any other action need at least read permission
203 # any other action need at least read permission
196 if not HasPermissionAnyMiddleware('repository.read',
204 if not HasPermissionAnyMiddleware('repository.read',
197 'repository.write',
205 'repository.write',
198 'repository.admin')(user,
206 'repository.admin')(user,
199 repo_name):
207 repo_name):
200 return False
208 return False
201
209
202 return True
210 return True
203
211
204 def _check_ssl(self, environ, start_response):
212 def _check_ssl(self, environ, start_response):
205 """
213 """
206 Checks the SSL check flag and returns False if SSL is not present
214 Checks the SSL check flag and returns False if SSL is not present
207 and required True otherwise
215 and required True otherwise
208 """
216 """
209 org_proto = environ['wsgi._org_proto']
217 org_proto = environ['wsgi._org_proto']
210 # check if we have SSL required ! if not it's a bad request !
218 # check if we have SSL required ! if not it's a bad request !
211 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
219 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
212 if require_ssl and org_proto == 'http':
220 if require_ssl and org_proto == 'http':
213 log.debug('proto is %s and SSL is required BAD REQUEST !',
221 log.debug('proto is %s and SSL is required BAD REQUEST !',
214 org_proto)
222 org_proto)
215 return False
223 return False
216 return True
224 return True
217
225
218 def __call__(self, environ, start_response):
226 def __call__(self, environ, start_response):
219 try:
227 try:
220 return self._handle_request(environ, start_response)
228 return self._handle_request(environ, start_response)
221 except Exception:
229 except Exception:
222 log.exception("Exception while handling request")
230 log.exception("Exception while handling request")
223 appenlight.track_exception(environ)
231 appenlight.track_exception(environ)
224 return HTTPInternalServerError()(environ, start_response)
232 return HTTPInternalServerError()(environ, start_response)
225 finally:
233 finally:
226 meta.Session.remove()
234 meta.Session.remove()
227
235
228 def _handle_request(self, environ, start_response):
236 def _handle_request(self, environ, start_response):
229
237
230 if not self._check_ssl(environ, start_response):
238 if not self._check_ssl(environ, start_response):
231 reason = ('SSL required, while RhodeCode was unable '
239 reason = ('SSL required, while RhodeCode was unable '
232 'to detect this as SSL request')
240 'to detect this as SSL request')
233 log.debug('User not allowed to proceed, %s', reason)
241 log.debug('User not allowed to proceed, %s', reason)
234 return HTTPNotAcceptable(reason)(environ, start_response)
242 return HTTPNotAcceptable(reason)(environ, start_response)
235
243
236 if not self.repo_name:
244 if not self.repo_name:
237 log.warning('Repository name is empty: %s', self.repo_name)
245 log.warning('Repository name is empty: %s', self.repo_name)
238 # failed to get repo name, we fail now
246 # failed to get repo name, we fail now
239 return HTTPNotFound()(environ, start_response)
247 return HTTPNotFound()(environ, start_response)
240 log.debug('Extracted repo name is %s', self.repo_name)
248 log.debug('Extracted repo name is %s', self.repo_name)
241
249
242 ip_addr = get_ip_addr(environ)
250 ip_addr = get_ip_addr(environ)
243 username = None
251 username = None
244
252
245 # skip passing error to error controller
253 # skip passing error to error controller
246 environ['pylons.status_code_redirect'] = True
254 environ['pylons.status_code_redirect'] = True
247
255
248 # ======================================================================
256 # ======================================================================
249 # GET ACTION PULL or PUSH
257 # GET ACTION PULL or PUSH
250 # ======================================================================
258 # ======================================================================
251 action = self._get_action(environ)
259 action = self._get_action(environ)
252
260
253 # ======================================================================
261 # ======================================================================
254 # CHECK ANONYMOUS PERMISSION
262 # CHECK ANONYMOUS PERMISSION
255 # ======================================================================
263 # ======================================================================
256 if action in ['pull', 'push']:
264 if action in ['pull', 'push']:
257 anonymous_user = User.get_default_user()
265 anonymous_user = User.get_default_user()
258 username = anonymous_user.username
266 username = anonymous_user.username
259 if anonymous_user.active:
267 if anonymous_user.active:
260 # ONLY check permissions if the user is activated
268 # ONLY check permissions if the user is activated
261 anonymous_perm = self._check_permission(
269 anonymous_perm = self._check_permission(
262 action, anonymous_user, self.repo_name, ip_addr)
270 action, anonymous_user, self.repo_name, ip_addr)
263 else:
271 else:
264 anonymous_perm = False
272 anonymous_perm = False
265
273
266 if not anonymous_user.active or not anonymous_perm:
274 if not anonymous_user.active or not anonymous_perm:
267 if not anonymous_user.active:
275 if not anonymous_user.active:
268 log.debug('Anonymous access is disabled, running '
276 log.debug('Anonymous access is disabled, running '
269 'authentication')
277 'authentication')
270
278
271 if not anonymous_perm:
279 if not anonymous_perm:
272 log.debug('Not enough credentials to access this '
280 log.debug('Not enough credentials to access this '
273 'repository as anonymous user')
281 'repository as anonymous user')
274
282
275 username = None
283 username = None
276 # ==============================================================
284 # ==============================================================
277 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
285 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
278 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
286 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
279 # ==============================================================
287 # ==============================================================
280
288
281 # try to auth based on environ, container auth methods
289 # try to auth based on environ, container auth methods
282 log.debug('Running PRE-AUTH for container based authentication')
290 log.debug('Running PRE-AUTH for container based authentication')
283 pre_auth = authenticate(
291 pre_auth = authenticate(
284 '', '', environ, VCS_TYPE, registry=self.registry)
292 '', '', environ, VCS_TYPE, registry=self.registry)
285 if pre_auth and pre_auth.get('username'):
293 if pre_auth and pre_auth.get('username'):
286 username = pre_auth['username']
294 username = pre_auth['username']
287 log.debug('PRE-AUTH got %s as username', username)
295 log.debug('PRE-AUTH got %s as username', username)
288
296
289 # If not authenticated by the container, running basic auth
297 # If not authenticated by the container, running basic auth
290 if not username:
298 if not username:
291 self.authenticate.realm = get_rhodecode_realm()
299 self.authenticate.realm = get_rhodecode_realm()
292
300
293 try:
301 try:
294 result = self.authenticate(environ)
302 result = self.authenticate(environ)
295 except (UserCreationError, NotAllowedToCreateUserError) as e:
303 except (UserCreationError, NotAllowedToCreateUserError) as e:
296 log.error(e)
304 log.error(e)
297 reason = safe_str(e)
305 reason = safe_str(e)
298 return HTTPNotAcceptable(reason)(environ, start_response)
306 return HTTPNotAcceptable(reason)(environ, start_response)
299
307
300 if isinstance(result, str):
308 if isinstance(result, str):
301 AUTH_TYPE.update(environ, 'basic')
309 AUTH_TYPE.update(environ, 'basic')
302 REMOTE_USER.update(environ, result)
310 REMOTE_USER.update(environ, result)
303 username = result
311 username = result
304 else:
312 else:
305 return result.wsgi_application(environ, start_response)
313 return result.wsgi_application(environ, start_response)
306
314
307 # ==============================================================
315 # ==============================================================
308 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
316 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
309 # ==============================================================
317 # ==============================================================
310 user = User.get_by_username(username)
318 user = User.get_by_username(username)
311 if not self.valid_and_active_user(user):
319 if not self.valid_and_active_user(user):
312 return HTTPForbidden()(environ, start_response)
320 return HTTPForbidden()(environ, start_response)
313 username = user.username
321 username = user.username
314 user.update_lastactivity()
322 user.update_lastactivity()
315 meta.Session().commit()
323 meta.Session().commit()
316
324
317 # check user attributes for password change flag
325 # check user attributes for password change flag
318 user_obj = user
326 user_obj = user
319 if user_obj and user_obj.username != User.DEFAULT_USER and \
327 if user_obj and user_obj.username != User.DEFAULT_USER and \
320 user_obj.user_data.get('force_password_change'):
328 user_obj.user_data.get('force_password_change'):
321 reason = 'password change required'
329 reason = 'password change required'
322 log.debug('User not allowed to authenticate, %s', reason)
330 log.debug('User not allowed to authenticate, %s', reason)
323 return HTTPNotAcceptable(reason)(environ, start_response)
331 return HTTPNotAcceptable(reason)(environ, start_response)
324
332
325 # check permissions for this repository
333 # check permissions for this repository
326 perm = self._check_permission(
334 perm = self._check_permission(
327 action, user, self.repo_name, ip_addr)
335 action, user, self.repo_name, ip_addr)
328 if not perm:
336 if not perm:
329 return HTTPForbidden()(environ, start_response)
337 return HTTPForbidden()(environ, start_response)
330
338
331 # extras are injected into UI object and later available
339 # extras are injected into UI object and later available
332 # in hooks executed by rhodecode
340 # in hooks executed by rhodecode
333 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
341 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
334 extras = vcs_operation_context(
342 extras = vcs_operation_context(
335 environ, repo_name=self.repo_name, username=username,
343 environ, repo_name=self.repo_name, username=username,
336 action=action, scm=self.SCM,
344 action=action, scm=self.SCM,
337 check_locking=check_locking)
345 check_locking=check_locking)
338
346
339 # ======================================================================
347 # ======================================================================
340 # REQUEST HANDLING
348 # REQUEST HANDLING
341 # ======================================================================
349 # ======================================================================
342 str_repo_name = safe_str(self.repo_name)
350 str_repo_name = safe_str(self.repo_name)
343 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
351 repo_path = os.path.join(
352 safe_str(self.basepath), safe_str(self.vcs_repo_name))
344 log.debug('Repository path is %s', repo_path)
353 log.debug('Repository path is %s', repo_path)
345
354
346 fix_PATH()
355 fix_PATH()
347
356
348 log.info(
357 log.info(
349 '%s action on %s repo "%s" by "%s" from %s',
358 '%s action on %s repo "%s" by "%s" from %s',
350 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
359 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
351
360
352 return self._generate_vcs_response(
361 return self._generate_vcs_response(
353 environ, start_response, repo_path, self.repo_name, extras, action)
362 environ, start_response, repo_path, self.url_repo_name, extras, action)
354
363
355 @initialize_generator
364 @initialize_generator
356 def _generate_vcs_response(
365 def _generate_vcs_response(
357 self, environ, start_response, repo_path, repo_name, extras,
366 self, environ, start_response, repo_path, repo_name, extras,
358 action):
367 action):
359 """
368 """
360 Returns a generator for the response content.
369 Returns a generator for the response content.
361
370
362 This method is implemented as a generator, so that it can trigger
371 This method is implemented as a generator, so that it can trigger
363 the cache validation after all content sent back to the client. It
372 the cache validation after all content sent back to the client. It
364 also handles the locking exceptions which will be triggered when
373 also handles the locking exceptions which will be triggered when
365 the first chunk is produced by the underlying WSGI application.
374 the first chunk is produced by the underlying WSGI application.
366 """
375 """
367 callback_daemon, extras = self._prepare_callback_daemon(extras)
376 callback_daemon, extras = self._prepare_callback_daemon(extras)
368 config = self._create_config(extras, repo_name)
377 config = self._create_config(extras, self.acl_repo_name)
369 log.debug('HOOKS extras is %s', extras)
378 log.debug('HOOKS extras is %s', extras)
370 app = self._create_wsgi_app(repo_path, repo_name, config)
379 app = self._create_wsgi_app(repo_path, repo_name, config)
371
380
372 try:
381 try:
373 with callback_daemon:
382 with callback_daemon:
374 try:
383 try:
375 response = app(environ, start_response)
384 response = app(environ, start_response)
376 finally:
385 finally:
377 # This statement works together with the decorator
386 # This statement works together with the decorator
378 # "initialize_generator" above. The decorator ensures that
387 # "initialize_generator" above. The decorator ensures that
379 # we hit the first yield statement before the generator is
388 # we hit the first yield statement before the generator is
380 # returned back to the WSGI server. This is needed to
389 # returned back to the WSGI server. This is needed to
381 # ensure that the call to "app" above triggers the
390 # ensure that the call to "app" above triggers the
382 # needed callback to "start_response" before the
391 # needed callback to "start_response" before the
383 # generator is actually used.
392 # generator is actually used.
384 yield "__init__"
393 yield "__init__"
385
394
386 for chunk in response:
395 for chunk in response:
387 yield chunk
396 yield chunk
388 except Exception as exc:
397 except Exception as exc:
389 # TODO: johbo: Improve "translating" back the exception.
398 # TODO: johbo: Improve "translating" back the exception.
390 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
399 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
391 exc = HTTPLockedRC(*exc.args)
400 exc = HTTPLockedRC(*exc.args)
392 _code = rhodecode.CONFIG.get('lock_ret_code')
401 _code = rhodecode.CONFIG.get('lock_ret_code')
393 log.debug('Repository LOCKED ret code %s!', (_code,))
402 log.debug('Repository LOCKED ret code %s!', (_code,))
394 elif getattr(exc, '_vcs_kind', None) == 'requirement':
403 elif getattr(exc, '_vcs_kind', None) == 'requirement':
395 log.debug(
404 log.debug(
396 'Repository requires features unknown to this Mercurial')
405 'Repository requires features unknown to this Mercurial')
397 exc = HTTPRequirementError(*exc.args)
406 exc = HTTPRequirementError(*exc.args)
398 else:
407 else:
399 raise
408 raise
400
409
401 for chunk in exc(environ, start_response):
410 for chunk in exc(environ, start_response):
402 yield chunk
411 yield chunk
403 finally:
412 finally:
404 # invalidate cache on push
413 # invalidate cache on push
405 try:
414 try:
406 if action == 'push':
415 if action == 'push':
407 self._invalidate_cache(repo_name)
416 self._invalidate_cache(repo_name)
408 finally:
417 finally:
409 meta.Session.remove()
418 meta.Session.remove()
410
419
411 def _get_repository_name(self, environ):
420 def _get_repository_name(self, environ):
412 """Get repository name out of the environmnent
421 """Get repository name out of the environmnent
413
422
414 :param environ: WSGI environment
423 :param environ: WSGI environment
415 """
424 """
416 raise NotImplementedError()
425 raise NotImplementedError()
417
426
418 def _get_action(self, environ):
427 def _get_action(self, environ):
419 """Map request commands into a pull or push command.
428 """Map request commands into a pull or push command.
420
429
421 :param environ: WSGI environment
430 :param environ: WSGI environment
422 """
431 """
423 raise NotImplementedError()
432 raise NotImplementedError()
424
433
425 def _create_wsgi_app(self, repo_path, repo_name, config):
434 def _create_wsgi_app(self, repo_path, repo_name, config):
426 """Return the WSGI app that will finally handle the request."""
435 """Return the WSGI app that will finally handle the request."""
427 raise NotImplementedError()
436 raise NotImplementedError()
428
437
429 def _create_config(self, extras, repo_name):
438 def _create_config(self, extras, repo_name):
430 """Create a Pyro safe config representation."""
439 """Create a Pyro safe config representation."""
431 raise NotImplementedError()
440 raise NotImplementedError()
432
441
433 def _prepare_callback_daemon(self, extras):
442 def _prepare_callback_daemon(self, extras):
434 return prepare_callback_daemon(
443 return prepare_callback_daemon(
435 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
444 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
436 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
445 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
437
446
438
447
439 def _should_check_locking(query_string):
448 def _should_check_locking(query_string):
440 # this is kind of hacky, but due to how mercurial handles client-server
449 # this is kind of hacky, but due to how mercurial handles client-server
441 # server see all operation on commit; bookmarks, phases and
450 # server see all operation on commit; bookmarks, phases and
442 # obsolescence marker in different transaction, we don't want to check
451 # obsolescence marker in different transaction, we don't want to check
443 # locking on those
452 # locking on those
444 return query_string not in ['cmd=listkeys']
453 return query_string not in ['cmd=listkeys']
@@ -1,198 +1,224 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 from webob.exc import HTTPNotFound
27 from webob.exc import HTTPNotFound
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 from rhodecode.lib.middleware.simplehg import SimpleHg
32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 from rhodecode.model.settings import VcsSettingsModel
34 from rhodecode.model.settings import VcsSettingsModel
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 def is_git(environ):
39 def is_git(environ):
40 """
40 """
41 Returns True if requests should be handled by GIT wsgi middleware
41 Returns True if requests should be handled by GIT wsgi middleware
42 """
42 """
43 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
43 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
44 log.debug(
44 log.debug(
45 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
45 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
46 is_git_path is not None)
46 is_git_path is not None)
47
47
48 return is_git_path
48 return is_git_path
49
49
50
50
51 def is_hg(environ):
51 def is_hg(environ):
52 """
52 """
53 Returns True if requests target is mercurial server - header
53 Returns True if requests target is mercurial server - header
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
55 """
55 """
56 is_hg_path = False
56 is_hg_path = False
57
57
58 http_accept = environ.get('HTTP_ACCEPT')
58 http_accept = environ.get('HTTP_ACCEPT')
59
59
60 if http_accept and http_accept.startswith('application/mercurial'):
60 if http_accept and http_accept.startswith('application/mercurial'):
61 query = urlparse.parse_qs(environ['QUERY_STRING'])
61 query = urlparse.parse_qs(environ['QUERY_STRING'])
62 if 'cmd' in query:
62 if 'cmd' in query:
63 is_hg_path = True
63 is_hg_path = True
64
64
65 log.debug(
65 log.debug(
66 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
66 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
67 is_hg_path)
67 is_hg_path)
68
68
69 return is_hg_path
69 return is_hg_path
70
70
71
71
72 def is_svn(environ):
72 def is_svn(environ):
73 """
73 """
74 Returns True if requests target is Subversion server
74 Returns True if requests target is Subversion server
75 """
75 """
76 http_dav = environ.get('HTTP_DAV', '')
76 http_dav = environ.get('HTTP_DAV', '')
77 magic_path_segment = rhodecode.CONFIG.get(
77 magic_path_segment = rhodecode.CONFIG.get(
78 'rhodecode_subversion_magic_path', '/!svn')
78 'rhodecode_subversion_magic_path', '/!svn')
79 is_svn_path = (
79 is_svn_path = (
80 'subversion' in http_dav or
80 'subversion' in http_dav or
81 magic_path_segment in environ['PATH_INFO'])
81 magic_path_segment in environ['PATH_INFO'])
82 log.debug(
82 log.debug(
83 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
83 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
84 is_svn_path)
84 is_svn_path)
85
85
86 return is_svn_path
86 return is_svn_path
87
87
88
88
89 class GunzipMiddleware(object):
89 class GunzipMiddleware(object):
90 """
90 """
91 WSGI middleware that unzips gzip-encoded requests before
91 WSGI middleware that unzips gzip-encoded requests before
92 passing on to the underlying application.
92 passing on to the underlying application.
93 """
93 """
94
94
95 def __init__(self, application):
95 def __init__(self, application):
96 self.app = application
96 self.app = application
97
97
98 def __call__(self, environ, start_response):
98 def __call__(self, environ, start_response):
99 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
99 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
100
100
101 if b'gzip' in accepts_encoding_header:
101 if b'gzip' in accepts_encoding_header:
102 log.debug('gzip detected, now running gunzip wrapper')
102 log.debug('gzip detected, now running gunzip wrapper')
103 wsgi_input = environ['wsgi.input']
103 wsgi_input = environ['wsgi.input']
104
104
105 if not hasattr(environ['wsgi.input'], 'seek'):
105 if not hasattr(environ['wsgi.input'], 'seek'):
106 # The gzip implementation in the standard library of Python 2.x
106 # The gzip implementation in the standard library of Python 2.x
107 # requires the '.seek()' and '.tell()' methods to be available
107 # requires the '.seek()' and '.tell()' methods to be available
108 # on the input stream. Read the data into a temporary file to
108 # on the input stream. Read the data into a temporary file to
109 # work around this limitation.
109 # work around this limitation.
110
110
111 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
111 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
112 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
112 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
113 wsgi_input.seek(0)
113 wsgi_input.seek(0)
114
114
115 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
115 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
116 # since we "Ungzipped" the content we say now it's no longer gzip
116 # since we "Ungzipped" the content we say now it's no longer gzip
117 # content encoding
117 # content encoding
118 del environ['HTTP_CONTENT_ENCODING']
118 del environ['HTTP_CONTENT_ENCODING']
119
119
120 # content length has changes ? or i'm not sure
120 # content length has changes ? or i'm not sure
121 if 'CONTENT_LENGTH' in environ:
121 if 'CONTENT_LENGTH' in environ:
122 del environ['CONTENT_LENGTH']
122 del environ['CONTENT_LENGTH']
123 else:
123 else:
124 log.debug('content not gzipped, gzipMiddleware passing '
124 log.debug('content not gzipped, gzipMiddleware passing '
125 'request further')
125 'request further')
126 return self.app(environ, start_response)
126 return self.app(environ, start_response)
127
127
128
128
129 class VCSMiddleware(object):
129 class VCSMiddleware(object):
130
130
131 def __init__(self, app, config, appenlight_client, registry):
131 def __init__(self, app, config, appenlight_client, registry):
132 self.application = app
132 self.application = app
133 self.config = config
133 self.config = config
134 self.appenlight_client = appenlight_client
134 self.appenlight_client = appenlight_client
135 self.registry = registry
135 self.registry = registry
136 self.use_gzip = True
136 self.use_gzip = True
137 # order in which we check the middlewares, based on vcs.backends config
137 # order in which we check the middlewares, based on vcs.backends config
138 self.check_middlewares = config['vcs.backends']
138 self.check_middlewares = config['vcs.backends']
139 self.checks = {
139 self.checks = {
140 'hg': (is_hg, SimpleHg),
140 'hg': (is_hg, SimpleHg),
141 'git': (is_git, SimpleGit),
141 'git': (is_git, SimpleGit),
142 'svn': (is_svn, SimpleSvn),
142 'svn': (is_svn, SimpleSvn),
143 }
143 }
144
144
145 def vcs_config(self, repo_name=None):
145 def vcs_config(self, repo_name=None):
146 """
146 """
147 returns serialized VcsSettings
147 returns serialized VcsSettings
148 """
148 """
149 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
149 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
150
150
151 def wrap_in_gzip_if_enabled(self, app, config):
151 def wrap_in_gzip_if_enabled(self, app, config):
152 if self.use_gzip:
152 if self.use_gzip:
153 app = GunzipMiddleware(app)
153 app = GunzipMiddleware(app)
154 return app
154 return app
155
155
156 def _get_handler_app(self, environ):
156 def _get_handler_app(self, environ):
157 app = None
157 app = None
158 log.debug('Checking vcs types in order: %r', self.check_middlewares)
158 log.debug('Checking vcs types in order: %r', self.check_middlewares)
159 for vcs_type in self.check_middlewares:
159 for vcs_type in self.check_middlewares:
160 vcs_check, handler = self.checks[vcs_type]
160 vcs_check, handler = self.checks[vcs_type]
161 if vcs_check(environ):
161 if vcs_check(environ):
162 log.debug(
162 log.debug(
163 'Found VCS Middleware to handle the request %s', handler)
163 'Found VCS Middleware to handle the request %s', handler)
164 app = handler(self.application, self.config, self.registry)
164 app = handler(self.application, self.config, self.registry)
165 break
165 break
166
166
167 return app
167 return app
168
168
169 def __call__(self, environ, start_response):
169 def __call__(self, environ, start_response):
170 # check if we handle one of interesting protocols, optionally extract
170 # check if we handle one of interesting protocols, optionally extract
171 # specific vcsSettings and allow changes of how things are wrapped
171 # specific vcsSettings and allow changes of how things are wrapped
172 vcs_handler = self._get_handler_app(environ)
172 vcs_handler = self._get_handler_app(environ)
173 if vcs_handler:
173 if vcs_handler:
174 # translate the _REPO_ID into real repo NAME for usage
174 # translate the _REPO_ID into real repo NAME for usage
175 # in middleware
175 # in middleware
176 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
176 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
177 repo_name = vcs_handler._get_repository_name(environ)
177 repo_name = vcs_handler._get_repository_name(environ)
178
178
179 acl_repo_name = repo_name
180 vcs_repo_name = repo_name
181 url_repo_name = repo_name
182 pr_id = None
183
184 # TODO: johbo: recognize a pull request based on pattern matching
185 if '/pull-request/' in repo_name:
186 acl_repo_name, other = repo_name.split('/pull-request/')
187 # TODO: johbo: Set shadow repo path
188 basename, repo_segment = acl_repo_name.rsplit('/', 1)
189 pr_id = int(other[0:-len('/repository')])
190 vcs_repo_name = '{basename}/.__shadow_{repo_segment}_pr-{pr_id}'.format(
191 basename=basename,
192 repo_segment=repo_segment,
193 pr_id=pr_id)
194
195 log.debug('repo_names %s', {
196 'acl_repo_name': acl_repo_name,
197 'vcs_repo_name': vcs_repo_name,
198 'url_repo_name': url_repo_name,
199 })
200 log.debug('pull_request %s', pr_id)
201
179 # check for type, presence in database and on filesystem
202 # check for type, presence in database and on filesystem
180 if not vcs_handler.is_valid_and_existing_repo(
203 if not vcs_handler.is_valid_and_existing_repo(
181 repo_name, vcs_handler.basepath, vcs_handler.SCM):
204 acl_repo_name, vcs_handler.basepath, vcs_handler.SCM):
182 return HTTPNotFound()(environ, start_response)
205 return HTTPNotFound()(environ, start_response)
183
206
184 # TODO: johbo: Needed for the Pyro4 backend and Mercurial only.
207 # TODO: johbo: Needed for the Pyro4 backend and Mercurial only.
185 # Remove once we fully switched to the HTTP backend.
208 # Remove once we fully switched to the HTTP backend.
186 environ['REPO_NAME'] = repo_name
209 environ['REPO_NAME'] = url_repo_name
187
210
188 # register repo_name and it's config back to the handler
211 # register repo_name and it's config back to the handler
189 vcs_handler.repo_name = repo_name
212 vcs_handler.acl_repo_name = acl_repo_name
190 vcs_handler.repo_vcs_config = self.vcs_config(repo_name)
213 vcs_handler.url_repo_name = url_repo_name
214 vcs_handler.vcs_repo_name = vcs_repo_name
215 vcs_handler.pr_id = pr_id
216 vcs_handler.repo_vcs_config = self.vcs_config(acl_repo_name)
191
217
192 vcs_handler = self.wrap_in_gzip_if_enabled(
218 vcs_handler = self.wrap_in_gzip_if_enabled(
193 vcs_handler, self.config)
219 vcs_handler, self.config)
194 vcs_handler, _ = wrap_in_appenlight_if_enabled(
220 vcs_handler, _ = wrap_in_appenlight_if_enabled(
195 vcs_handler, self.config, self.appenlight_client)
221 vcs_handler, self.config, self.appenlight_client)
196 return vcs_handler(environ, start_response)
222 return vcs_handler(environ, start_response)
197
223
198 return self.application(environ, start_response)
224 return self.application(environ, start_response)
@@ -1,308 +1,310 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 base64
21 import base64
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import webtest.app
25 import webtest.app
26
26
27 from rhodecode.lib.caching_query import FromCache
27 from rhodecode.lib.caching_query import FromCache
28 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
28 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
29 from rhodecode.lib.middleware import simplevcs
29 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware.https_fixup import HttpsFixup
30 from rhodecode.lib.middleware.https_fixup import HttpsFixup
31 from rhodecode.lib.middleware.utils import scm_app
31 from rhodecode.lib.middleware.utils import scm_app
32 from rhodecode.model.db import User, _hash_key
32 from rhodecode.model.db import User, _hash_key
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
35 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
36 from rhodecode.tests.lib.middleware import mock_scm_app
36 from rhodecode.tests.lib.middleware import mock_scm_app
37 from rhodecode.tests.utils import set_anonymous_access
37 from rhodecode.tests.utils import set_anonymous_access
38
38
39
39
40 class StubVCSController(simplevcs.SimpleVCS):
40 class StubVCSController(simplevcs.SimpleVCS):
41
41
42 SCM = 'hg'
42 SCM = 'hg'
43 stub_response_body = tuple()
43 stub_response_body = tuple()
44
44
45 def __init__(self, *args, **kwargs):
45 def __init__(self, *args, **kwargs):
46 super(StubVCSController, self).__init__(*args, **kwargs)
46 super(StubVCSController, self).__init__(*args, **kwargs)
47 self.repo_name = HG_REPO
47 self.acl_repo_name = HG_REPO
48 self.url_repo_name = HG_REPO
49 self.vcs_repo_name = HG_REPO
48
50
49 def _get_repository_name(self, environ):
51 def _get_repository_name(self, environ):
50 return HG_REPO
52 return HG_REPO
51
53
52 def _get_action(self, environ):
54 def _get_action(self, environ):
53 return "pull"
55 return "pull"
54
56
55 def _create_wsgi_app(self, repo_path, repo_name, config):
57 def _create_wsgi_app(self, repo_path, repo_name, config):
56 def fake_app(environ, start_response):
58 def fake_app(environ, start_response):
57 start_response('200 OK', [])
59 start_response('200 OK', [])
58 return self.stub_response_body
60 return self.stub_response_body
59 return fake_app
61 return fake_app
60
62
61 def _create_config(self, extras, repo_name):
63 def _create_config(self, extras, repo_name):
62 return None
64 return None
63
65
64
66
65 @pytest.fixture
67 @pytest.fixture
66 def vcscontroller(pylonsapp, config_stub):
68 def vcscontroller(pylonsapp, config_stub):
67 config_stub.testing_securitypolicy()
69 config_stub.testing_securitypolicy()
68 config_stub.include('rhodecode.authentication')
70 config_stub.include('rhodecode.authentication')
69
71
70 set_anonymous_access(True)
72 set_anonymous_access(True)
71 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
73 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
72 app = HttpsFixup(controller, pylonsapp.config)
74 app = HttpsFixup(controller, pylonsapp.config)
73 app = webtest.app.TestApp(app)
75 app = webtest.app.TestApp(app)
74
76
75 _remove_default_user_from_query_cache()
77 _remove_default_user_from_query_cache()
76
78
77 # Sanity checks that things are set up correctly
79 # Sanity checks that things are set up correctly
78 app.get('/' + HG_REPO, status=200)
80 app.get('/' + HG_REPO, status=200)
79
81
80 app.controller = controller
82 app.controller = controller
81 return app
83 return app
82
84
83
85
84 def _remove_default_user_from_query_cache():
86 def _remove_default_user_from_query_cache():
85 user = User.get_default_user(cache=True)
87 user = User.get_default_user(cache=True)
86 query = Session().query(User).filter(User.username == user.username)
88 query = Session().query(User).filter(User.username == user.username)
87 query = query.options(FromCache(
89 query = query.options(FromCache(
88 "sql_cache_short", "get_user_%s" % _hash_key(user.username)))
90 "sql_cache_short", "get_user_%s" % _hash_key(user.username)))
89 query.invalidate()
91 query.invalidate()
90 Session().expire(user)
92 Session().expire(user)
91
93
92
94
93 @pytest.fixture
95 @pytest.fixture
94 def disable_anonymous_user(request, pylonsapp):
96 def disable_anonymous_user(request, pylonsapp):
95 set_anonymous_access(False)
97 set_anonymous_access(False)
96
98
97 @request.addfinalizer
99 @request.addfinalizer
98 def cleanup():
100 def cleanup():
99 set_anonymous_access(True)
101 set_anonymous_access(True)
100
102
101
103
102 def test_handles_exceptions_during_permissions_checks(
104 def test_handles_exceptions_during_permissions_checks(
103 vcscontroller, disable_anonymous_user):
105 vcscontroller, disable_anonymous_user):
104 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
106 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
105 auth_password = base64.encodestring(user_and_pass).strip()
107 auth_password = base64.encodestring(user_and_pass).strip()
106 extra_environ = {
108 extra_environ = {
107 'AUTH_TYPE': 'Basic',
109 'AUTH_TYPE': 'Basic',
108 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
110 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
109 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
111 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
110 }
112 }
111
113
112 # Verify that things are hooked up correctly
114 # Verify that things are hooked up correctly
113 vcscontroller.get('/', status=200, extra_environ=extra_environ)
115 vcscontroller.get('/', status=200, extra_environ=extra_environ)
114
116
115 # Simulate trouble during permission checks
117 # Simulate trouble during permission checks
116 with mock.patch('rhodecode.model.db.User.get_by_username',
118 with mock.patch('rhodecode.model.db.User.get_by_username',
117 side_effect=Exception) as get_user:
119 side_effect=Exception) as get_user:
118 # Verify that a correct 500 is returned and check that the expected
120 # Verify that a correct 500 is returned and check that the expected
119 # code path was hit.
121 # code path was hit.
120 vcscontroller.get('/', status=500, extra_environ=extra_environ)
122 vcscontroller.get('/', status=500, extra_environ=extra_environ)
121 assert get_user.called
123 assert get_user.called
122
124
123
125
124 def test_returns_forbidden_if_no_anonymous_access(
126 def test_returns_forbidden_if_no_anonymous_access(
125 vcscontroller, disable_anonymous_user):
127 vcscontroller, disable_anonymous_user):
126 vcscontroller.get('/', status=401)
128 vcscontroller.get('/', status=401)
127
129
128
130
129 class StubFailVCSController(simplevcs.SimpleVCS):
131 class StubFailVCSController(simplevcs.SimpleVCS):
130 def _handle_request(self, environ, start_response):
132 def _handle_request(self, environ, start_response):
131 raise Exception("BOOM")
133 raise Exception("BOOM")
132
134
133
135
134 @pytest.fixture(scope='module')
136 @pytest.fixture(scope='module')
135 def fail_controller(pylonsapp):
137 def fail_controller(pylonsapp):
136 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
138 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
137 controller = HttpsFixup(controller, pylonsapp.config)
139 controller = HttpsFixup(controller, pylonsapp.config)
138 controller = webtest.app.TestApp(controller)
140 controller = webtest.app.TestApp(controller)
139 return controller
141 return controller
140
142
141
143
142 def test_handles_exceptions_as_internal_server_error(fail_controller):
144 def test_handles_exceptions_as_internal_server_error(fail_controller):
143 fail_controller.get('/', status=500)
145 fail_controller.get('/', status=500)
144
146
145
147
146 def test_provides_traceback_for_appenlight(fail_controller):
148 def test_provides_traceback_for_appenlight(fail_controller):
147 response = fail_controller.get(
149 response = fail_controller.get(
148 '/', status=500, extra_environ={'appenlight.client': 'fake'})
150 '/', status=500, extra_environ={'appenlight.client': 'fake'})
149 assert 'appenlight.__traceback' in response.request.environ
151 assert 'appenlight.__traceback' in response.request.environ
150
152
151
153
152 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
154 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
153 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
155 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
154 assert controller.scm_app is scm_app
156 assert controller.scm_app is scm_app
155
157
156
158
157 def test_allows_to_override_scm_app_via_config(pylonsapp):
159 def test_allows_to_override_scm_app_via_config(pylonsapp):
158 config = pylonsapp.config.copy()
160 config = pylonsapp.config.copy()
159 config['vcs.scm_app_implementation'] = (
161 config['vcs.scm_app_implementation'] = (
160 'rhodecode.tests.lib.middleware.mock_scm_app')
162 'rhodecode.tests.lib.middleware.mock_scm_app')
161 controller = StubVCSController(pylonsapp, config, None)
163 controller = StubVCSController(pylonsapp, config, None)
162 assert controller.scm_app is mock_scm_app
164 assert controller.scm_app is mock_scm_app
163
165
164
166
165 @pytest.mark.parametrize('query_string, expected', [
167 @pytest.mark.parametrize('query_string, expected', [
166 ('cmd=stub_command', True),
168 ('cmd=stub_command', True),
167 ('cmd=listkeys', False),
169 ('cmd=listkeys', False),
168 ])
170 ])
169 def test_should_check_locking(query_string, expected):
171 def test_should_check_locking(query_string, expected):
170 result = simplevcs._should_check_locking(query_string)
172 result = simplevcs._should_check_locking(query_string)
171 assert result == expected
173 assert result == expected
172
174
173
175
174 @mock.patch.multiple(
176 @mock.patch.multiple(
175 'Pyro4.config', SERVERTYPE='multiplex', POLLTIMEOUT=0.01)
177 'Pyro4.config', SERVERTYPE='multiplex', POLLTIMEOUT=0.01)
176 class TestGenerateVcsResponse:
178 class TestGenerateVcsResponse:
177
179
178 def test_ensures_that_start_response_is_called_early_enough(self):
180 def test_ensures_that_start_response_is_called_early_enough(self):
179 self.call_controller_with_response_body(iter(['a', 'b']))
181 self.call_controller_with_response_body(iter(['a', 'b']))
180 assert self.start_response.called
182 assert self.start_response.called
181
183
182 def test_invalidates_cache_after_body_is_consumed(self):
184 def test_invalidates_cache_after_body_is_consumed(self):
183 result = self.call_controller_with_response_body(iter(['a', 'b']))
185 result = self.call_controller_with_response_body(iter(['a', 'b']))
184 assert not self.was_cache_invalidated()
186 assert not self.was_cache_invalidated()
185 # Consume the result
187 # Consume the result
186 list(result)
188 list(result)
187 assert self.was_cache_invalidated()
189 assert self.was_cache_invalidated()
188
190
189 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
191 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
190 def test_handles_locking_exception(self, http_locked_rc):
192 def test_handles_locking_exception(self, http_locked_rc):
191 result = self.call_controller_with_response_body(
193 result = self.call_controller_with_response_body(
192 self.raise_result_iter(vcs_kind='repo_locked'))
194 self.raise_result_iter(vcs_kind='repo_locked'))
193 assert not http_locked_rc.called
195 assert not http_locked_rc.called
194 # Consume the result
196 # Consume the result
195 list(result)
197 list(result)
196 assert http_locked_rc.called
198 assert http_locked_rc.called
197
199
198 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
200 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
199 def test_handles_requirement_exception(self, http_requirement):
201 def test_handles_requirement_exception(self, http_requirement):
200 result = self.call_controller_with_response_body(
202 result = self.call_controller_with_response_body(
201 self.raise_result_iter(vcs_kind='requirement'))
203 self.raise_result_iter(vcs_kind='requirement'))
202 assert not http_requirement.called
204 assert not http_requirement.called
203 # Consume the result
205 # Consume the result
204 list(result)
206 list(result)
205 assert http_requirement.called
207 assert http_requirement.called
206
208
207 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
209 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
208 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
210 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
209 app_factory_patcher = mock.patch.object(
211 app_factory_patcher = mock.patch.object(
210 StubVCSController, '_create_wsgi_app')
212 StubVCSController, '_create_wsgi_app')
211 with app_factory_patcher as app_factory:
213 with app_factory_patcher as app_factory:
212 app_factory().side_effect = self.vcs_exception()
214 app_factory().side_effect = self.vcs_exception()
213 result = self.call_controller_with_response_body(['a'])
215 result = self.call_controller_with_response_body(['a'])
214 list(result)
216 list(result)
215 assert http_locked_rc.called
217 assert http_locked_rc.called
216
218
217 def test_raises_unknown_exceptions(self):
219 def test_raises_unknown_exceptions(self):
218 result = self.call_controller_with_response_body(
220 result = self.call_controller_with_response_body(
219 self.raise_result_iter(vcs_kind='unknown'))
221 self.raise_result_iter(vcs_kind='unknown'))
220 with pytest.raises(Exception):
222 with pytest.raises(Exception):
221 list(result)
223 list(result)
222
224
223 def test_prepare_callback_daemon_is_called(self):
225 def test_prepare_callback_daemon_is_called(self):
224 def side_effect(extras):
226 def side_effect(extras):
225 return DummyHooksCallbackDaemon(), extras
227 return DummyHooksCallbackDaemon(), extras
226
228
227 prepare_patcher = mock.patch.object(
229 prepare_patcher = mock.patch.object(
228 StubVCSController, '_prepare_callback_daemon')
230 StubVCSController, '_prepare_callback_daemon')
229 with prepare_patcher as prepare_mock:
231 with prepare_patcher as prepare_mock:
230 prepare_mock.side_effect = side_effect
232 prepare_mock.side_effect = side_effect
231 self.call_controller_with_response_body(iter(['a', 'b']))
233 self.call_controller_with_response_body(iter(['a', 'b']))
232 assert prepare_mock.called
234 assert prepare_mock.called
233 assert prepare_mock.call_count == 1
235 assert prepare_mock.call_count == 1
234
236
235 def call_controller_with_response_body(self, response_body):
237 def call_controller_with_response_body(self, response_body):
236 settings = {
238 settings = {
237 'base_path': 'fake_base_path',
239 'base_path': 'fake_base_path',
238 'vcs.hooks.protocol': 'http',
240 'vcs.hooks.protocol': 'http',
239 'vcs.hooks.direct_calls': False,
241 'vcs.hooks.direct_calls': False,
240 }
242 }
241 controller = StubVCSController(None, settings, None)
243 controller = StubVCSController(None, settings, None)
242 controller._invalidate_cache = mock.Mock()
244 controller._invalidate_cache = mock.Mock()
243 controller.stub_response_body = response_body
245 controller.stub_response_body = response_body
244 self.start_response = mock.Mock()
246 self.start_response = mock.Mock()
245 result = controller._generate_vcs_response(
247 result = controller._generate_vcs_response(
246 environ={}, start_response=self.start_response,
248 environ={}, start_response=self.start_response,
247 repo_path='fake_repo_path',
249 repo_path='fake_repo_path',
248 repo_name='fake_repo_name',
250 repo_name='fake_repo_name',
249 extras={}, action='push')
251 extras={}, action='push')
250 self.controller = controller
252 self.controller = controller
251 return result
253 return result
252
254
253 def raise_result_iter(self, vcs_kind='repo_locked'):
255 def raise_result_iter(self, vcs_kind='repo_locked'):
254 """
256 """
255 Simulates an exception due to a vcs raised exception if kind vcs_kind
257 Simulates an exception due to a vcs raised exception if kind vcs_kind
256 """
258 """
257 raise self.vcs_exception(vcs_kind=vcs_kind)
259 raise self.vcs_exception(vcs_kind=vcs_kind)
258 yield "never_reached"
260 yield "never_reached"
259
261
260 def vcs_exception(self, vcs_kind='repo_locked'):
262 def vcs_exception(self, vcs_kind='repo_locked'):
261 locked_exception = Exception('TEST_MESSAGE')
263 locked_exception = Exception('TEST_MESSAGE')
262 locked_exception._vcs_kind = vcs_kind
264 locked_exception._vcs_kind = vcs_kind
263 return locked_exception
265 return locked_exception
264
266
265 def was_cache_invalidated(self):
267 def was_cache_invalidated(self):
266 return self.controller._invalidate_cache.called
268 return self.controller._invalidate_cache.called
267
269
268
270
269 class TestInitializeGenerator:
271 class TestInitializeGenerator:
270
272
271 def test_drains_first_element(self):
273 def test_drains_first_element(self):
272 gen = self.factory(['__init__', 1, 2])
274 gen = self.factory(['__init__', 1, 2])
273 result = list(gen)
275 result = list(gen)
274 assert result == [1, 2]
276 assert result == [1, 2]
275
277
276 @pytest.mark.parametrize('values', [
278 @pytest.mark.parametrize('values', [
277 [],
279 [],
278 [1, 2],
280 [1, 2],
279 ])
281 ])
280 def test_raises_value_error(self, values):
282 def test_raises_value_error(self, values):
281 with pytest.raises(ValueError):
283 with pytest.raises(ValueError):
282 self.factory(values)
284 self.factory(values)
283
285
284 @simplevcs.initialize_generator
286 @simplevcs.initialize_generator
285 def factory(self, iterable):
287 def factory(self, iterable):
286 for elem in iterable:
288 for elem in iterable:
287 yield elem
289 yield elem
288
290
289
291
290 class TestPrepareHooksDaemon(object):
292 class TestPrepareHooksDaemon(object):
291 def test_calls_imported_prepare_callback_daemon(self, app_settings):
293 def test_calls_imported_prepare_callback_daemon(self, app_settings):
292 expected_extras = {'extra1': 'value1'}
294 expected_extras = {'extra1': 'value1'}
293 daemon = DummyHooksCallbackDaemon()
295 daemon = DummyHooksCallbackDaemon()
294
296
295 controller = StubVCSController(None, app_settings, None)
297 controller = StubVCSController(None, app_settings, None)
296 prepare_patcher = mock.patch.object(
298 prepare_patcher = mock.patch.object(
297 simplevcs, 'prepare_callback_daemon',
299 simplevcs, 'prepare_callback_daemon',
298 return_value=(daemon, expected_extras))
300 return_value=(daemon, expected_extras))
299 with prepare_patcher as prepare_mock:
301 with prepare_patcher as prepare_mock:
300 callback_daemon, extras = controller._prepare_callback_daemon(
302 callback_daemon, extras = controller._prepare_callback_daemon(
301 expected_extras.copy())
303 expected_extras.copy())
302 prepare_mock.assert_called_once_with(
304 prepare_mock.assert_called_once_with(
303 expected_extras,
305 expected_extras,
304 protocol=app_settings['vcs.hooks.protocol'],
306 protocol=app_settings['vcs.hooks.protocol'],
305 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
307 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
306
308
307 assert callback_daemon == daemon
309 assert callback_daemon == daemon
308 assert extras == extras
310 assert extras == extras
General Comments 0
You need to be logged in to leave comments. Login now