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