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