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