##// END OF EJS Templates
vcs: also register a global config for vcs_handler....
marcink -
r2404:b82a3082 default
parent child Browse files
Show More
@@ -1,612 +1,629 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 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 re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34 # TODO(marcink): check if we should use webob.exc here ?
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 from zope.cachedescriptors.property import Lazy as LazyProperty
37 38
38 39 import rhodecode
39 40 from rhodecode.authentication.base import (
40 41 authenticate, get_perms_cache_manager, VCS_TYPE)
41 42 from rhodecode.lib import caches
42 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 44 from rhodecode.lib.base import (
44 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 46 from rhodecode.lib.exceptions import (
46 47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 48 NotAllowedToCreateUserError)
48 49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 50 from rhodecode.lib.middleware import appenlight
50 51 from rhodecode.lib.middleware.utils import scm_app_http
51 52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 55 from rhodecode.lib.vcs.backends import base
55 56 from rhodecode.model import meta
56 57 from rhodecode.model.db import User, Repository, PullRequest
57 58 from rhodecode.model.scm import ScmModel
58 59 from rhodecode.model.pull_request import PullRequestModel
59 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 61
61 62 log = logging.getLogger(__name__)
62 63
63 64
64 65 def initialize_generator(factory):
65 66 """
66 67 Initializes the returned generator by draining its first element.
67 68
68 69 This can be used to give a generator an initializer, which is the code
69 70 up to the first yield statement. This decorator enforces that the first
70 71 produced element has the value ``"__init__"`` to make its special
71 72 purpose very explicit in the using code.
72 73 """
73 74
74 75 @wraps(factory)
75 76 def wrapper(*args, **kwargs):
76 77 gen = factory(*args, **kwargs)
77 78 try:
78 79 init = gen.next()
79 80 except StopIteration:
80 81 raise ValueError('Generator must yield at least one element.')
81 82 if init != "__init__":
82 83 raise ValueError('First yielded element must be "__init__".')
83 84 return gen
84 85 return wrapper
85 86
86 87
87 88 class SimpleVCS(object):
88 89 """Common functionality for SCM HTTP handlers."""
89 90
90 91 SCM = 'unknown'
91 92
92 93 acl_repo_name = None
93 94 url_repo_name = None
94 95 vcs_repo_name = None
95 96 rc_extras = {}
96 97
97 98 # We have to handle requests to shadow repositories different than requests
98 99 # to normal repositories. Therefore we have to distinguish them. To do this
99 100 # we use this regex which will match only on URLs pointing to shadow
100 101 # repositories.
101 102 shadow_repo_re = re.compile(
102 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
103 104 '(?P<target>{slug_pat})/' # target repo
104 105 'pull-request/(?P<pr_id>\d+)/' # pull request
105 106 'repository$' # shadow repo
106 107 .format(slug_pat=SLUG_RE.pattern))
107 108
108 109 def __init__(self, config, registry):
109 110 self.registry = registry
110 111 self.config = config
111 112 # re-populated by specialized middleware
112 113 self.repo_vcs_config = base.Config()
113 114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
114 115
115 116 registry.rhodecode_settings = self.rhodecode_settings
116 117 # authenticate this VCS request using authfunc
117 118 auth_ret_code_detection = \
118 119 str2bool(self.config.get('auth_ret_code_detection', False))
119 120 self.authenticate = BasicAuth(
120 121 '', authenticate, registry, config.get('auth_ret_code'),
121 122 auth_ret_code_detection)
122 123 self.ip_addr = '0.0.0.0'
123 124
125 @LazyProperty
126 def global_vcs_config(self):
127 try:
128 return VcsSettingsModel().get_ui_settings_as_config_obj()
129 except Exception:
130 return base.Config()
131
124 132 @property
125 133 def base_path(self):
126 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
134 settings_path = self.repo_vcs_config.get(
135 *VcsSettingsModel.PATH_SETTING)
136
137 if not settings_path:
138 settings_path = self.global_vcs_config.get(
139 *VcsSettingsModel.PATH_SETTING)
140
127 141 if not settings_path:
128 142 # try, maybe we passed in explicitly as config option
129 143 settings_path = self.config.get('base_path')
144
145 if not settings_path:
146 raise ValueError('FATAL: base_path is empty')
130 147 return settings_path
131 148
132 149 def set_repo_names(self, environ):
133 150 """
134 151 This will populate the attributes acl_repo_name, url_repo_name,
135 152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
136 153 shadow) repositories all names are equal. In case of requests to a
137 154 shadow repository the acl-name points to the target repo of the pull
138 155 request and the vcs-name points to the shadow repo file system path.
139 156 The url-name is always the URL used by the vcs client program.
140 157
141 158 Example in case of a shadow repo:
142 159 acl_repo_name = RepoGroup/MyRepo
143 160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
144 161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
145 162 """
146 163 # First we set the repo name from URL for all attributes. This is the
147 164 # default if handling normal (non shadow) repo requests.
148 165 self.url_repo_name = self._get_repository_name(environ)
149 166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
150 167 self.is_shadow_repo = False
151 168
152 169 # Check if this is a request to a shadow repository.
153 170 match = self.shadow_repo_re.match(self.url_repo_name)
154 171 if match:
155 172 match_dict = match.groupdict()
156 173
157 174 # Build acl repo name from regex match.
158 175 acl_repo_name = safe_unicode('{groups}{target}'.format(
159 176 groups=match_dict['groups'] or '',
160 177 target=match_dict['target']))
161 178
162 179 # Retrieve pull request instance by ID from regex match.
163 180 pull_request = PullRequest.get(match_dict['pr_id'])
164 181
165 182 # Only proceed if we got a pull request and if acl repo name from
166 183 # URL equals the target repo name of the pull request.
167 184 if pull_request and (acl_repo_name ==
168 185 pull_request.target_repo.repo_name):
169 186 # Get file system path to shadow repository.
170 187 workspace_id = PullRequestModel()._workspace_id(pull_request)
171 188 target_vcs = pull_request.target_repo.scm_instance()
172 189 vcs_repo_name = target_vcs._get_shadow_repository_path(
173 190 workspace_id)
174 191
175 192 # Store names for later usage.
176 193 self.vcs_repo_name = vcs_repo_name
177 194 self.acl_repo_name = acl_repo_name
178 195 self.is_shadow_repo = True
179 196
180 197 log.debug('Setting all VCS repository names: %s', {
181 198 'acl_repo_name': self.acl_repo_name,
182 199 'url_repo_name': self.url_repo_name,
183 200 'vcs_repo_name': self.vcs_repo_name,
184 201 })
185 202
186 203 @property
187 204 def scm_app(self):
188 205 custom_implementation = self.config['vcs.scm_app_implementation']
189 206 if custom_implementation == 'http':
190 207 log.info('Using HTTP implementation of scm app.')
191 208 scm_app_impl = scm_app_http
192 209 else:
193 210 log.info('Using custom implementation of scm_app: "{}"'.format(
194 211 custom_implementation))
195 212 scm_app_impl = importlib.import_module(custom_implementation)
196 213 return scm_app_impl
197 214
198 215 def _get_by_id(self, repo_name):
199 216 """
200 217 Gets a special pattern _<ID> from clone url and tries to replace it
201 218 with a repository_name for support of _<ID> non changeable urls
202 219 """
203 220
204 221 data = repo_name.split('/')
205 222 if len(data) >= 2:
206 223 from rhodecode.model.repo import RepoModel
207 224 by_id_match = RepoModel().get_repo_by_id(repo_name)
208 225 if by_id_match:
209 226 data[1] = by_id_match.repo_name
210 227
211 228 return safe_str('/'.join(data))
212 229
213 230 def _invalidate_cache(self, repo_name):
214 231 """
215 232 Set's cache for this repository for invalidation on next access
216 233
217 234 :param repo_name: full repo name, also a cache key
218 235 """
219 236 ScmModel().mark_for_invalidation(repo_name)
220 237
221 238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
222 239 db_repo = Repository.get_by_repo_name(repo_name)
223 240 if not db_repo:
224 241 log.debug('Repository `%s` not found inside the database.',
225 242 repo_name)
226 243 return False
227 244
228 245 if db_repo.repo_type != scm_type:
229 246 log.warning(
230 247 'Repository `%s` have incorrect scm_type, expected %s got %s',
231 248 repo_name, db_repo.repo_type, scm_type)
232 249 return False
233 250
234 251 return is_valid_repo(repo_name, base_path,
235 252 explicit_scm=scm_type, expect_scm=scm_type)
236 253
237 254 def valid_and_active_user(self, user):
238 255 """
239 256 Checks if that user is not empty, and if it's actually object it checks
240 257 if he's active.
241 258
242 259 :param user: user object or None
243 260 :return: boolean
244 261 """
245 262 if user is None:
246 263 return False
247 264
248 265 elif user.active:
249 266 return True
250 267
251 268 return False
252 269
253 270 @property
254 271 def is_shadow_repo_dir(self):
255 272 return os.path.isdir(self.vcs_repo_name)
256 273
257 274 def _check_permission(self, action, user, repo_name, ip_addr=None,
258 275 plugin_id='', plugin_cache_active=False, cache_ttl=0):
259 276 """
260 277 Checks permissions using action (push/pull) user and repository
261 278 name. If plugin_cache and ttl is set it will use the plugin which
262 279 authenticated the user to store the cached permissions result for N
263 280 amount of seconds as in cache_ttl
264 281
265 282 :param action: push or pull action
266 283 :param user: user instance
267 284 :param repo_name: repository name
268 285 """
269 286
270 287 # get instance of cache manager configured for a namespace
271 288 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
272 289 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
273 290 plugin_id, plugin_cache_active, cache_ttl)
274 291
275 292 # for environ based password can be empty, but then the validation is
276 293 # on the server that fills in the env data needed for authentication
277 294 _perm_calc_hash = caches.compute_key_from_params(
278 295 plugin_id, action, user.user_id, repo_name, ip_addr)
279 296
280 297 # _authenticate is a wrapper for .auth() method of plugin.
281 298 # it checks if .auth() sends proper data.
282 299 # For RhodeCodeExternalAuthPlugin it also maps users to
283 300 # Database and maps the attributes returned from .auth()
284 301 # to RhodeCode database. If this function returns data
285 302 # then auth is correct.
286 303 start = time.time()
287 304 log.debug('Running plugin `%s` permissions check', plugin_id)
288 305
289 306 def perm_func():
290 307 """
291 308 This function is used internally in Cache of Beaker to calculate
292 309 Results
293 310 """
294 311 log.debug('auth: calculating permission access now...')
295 312 # check IP
296 313 inherit = user.inherit_default_permissions
297 314 ip_allowed = AuthUser.check_ip_allowed(
298 315 user.user_id, ip_addr, inherit_from_default=inherit)
299 316 if ip_allowed:
300 317 log.info('Access for IP:%s allowed', ip_addr)
301 318 else:
302 319 return False
303 320
304 321 if action == 'push':
305 322 perms = ('repository.write', 'repository.admin')
306 323 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
307 324 return False
308 325
309 326 else:
310 327 # any other action need at least read permission
311 328 perms = (
312 329 'repository.read', 'repository.write', 'repository.admin')
313 330 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
314 331 return False
315 332
316 333 return True
317 334
318 335 if plugin_cache_active:
319 336 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
320 337 perm_result = cache_manager.get(
321 338 _perm_calc_hash, createfunc=perm_func)
322 339 else:
323 340 perm_result = perm_func()
324 341
325 342 auth_time = time.time() - start
326 343 log.debug('Permissions for plugin `%s` completed in %.3fs, '
327 344 'expiration time of fetched cache %.1fs.',
328 345 plugin_id, auth_time, cache_ttl)
329 346
330 347 return perm_result
331 348
332 349 def _check_ssl(self, environ, start_response):
333 350 """
334 351 Checks the SSL check flag and returns False if SSL is not present
335 352 and required True otherwise
336 353 """
337 354 org_proto = environ['wsgi._org_proto']
338 355 # check if we have SSL required ! if not it's a bad request !
339 356 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
340 357 if require_ssl and org_proto == 'http':
341 358 log.debug('proto is %s and SSL is required BAD REQUEST !',
342 359 org_proto)
343 360 return False
344 361 return True
345 362
346 363 def __call__(self, environ, start_response):
347 364 try:
348 365 return self._handle_request(environ, start_response)
349 366 except Exception:
350 367 log.exception("Exception while handling request")
351 368 appenlight.track_exception(environ)
352 369 return HTTPInternalServerError()(environ, start_response)
353 370 finally:
354 371 meta.Session.remove()
355 372
356 373 def _handle_request(self, environ, start_response):
357 374
358 375 if not self._check_ssl(environ, start_response):
359 376 reason = ('SSL required, while RhodeCode was unable '
360 377 'to detect this as SSL request')
361 378 log.debug('User not allowed to proceed, %s', reason)
362 379 return HTTPNotAcceptable(reason)(environ, start_response)
363 380
364 381 if not self.url_repo_name:
365 382 log.warning('Repository name is empty: %s', self.url_repo_name)
366 383 # failed to get repo name, we fail now
367 384 return HTTPNotFound()(environ, start_response)
368 385 log.debug('Extracted repo name is %s', self.url_repo_name)
369 386
370 387 ip_addr = get_ip_addr(environ)
371 388 user_agent = get_user_agent(environ)
372 389 username = None
373 390
374 391 # skip passing error to error controller
375 392 environ['pylons.status_code_redirect'] = True
376 393
377 394 # ======================================================================
378 395 # GET ACTION PULL or PUSH
379 396 # ======================================================================
380 397 action = self._get_action(environ)
381 398
382 399 # ======================================================================
383 400 # Check if this is a request to a shadow repository of a pull request.
384 401 # In this case only pull action is allowed.
385 402 # ======================================================================
386 403 if self.is_shadow_repo and action != 'pull':
387 404 reason = 'Only pull action is allowed for shadow repositories.'
388 405 log.debug('User not allowed to proceed, %s', reason)
389 406 return HTTPNotAcceptable(reason)(environ, start_response)
390 407
391 408 # Check if the shadow repo actually exists, in case someone refers
392 409 # to it, and it has been deleted because of successful merge.
393 410 if self.is_shadow_repo and not self.is_shadow_repo_dir:
394 411 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
395 412 self.is_shadow_repo_dir)
396 413 return HTTPNotFound()(environ, start_response)
397 414
398 415 # ======================================================================
399 416 # CHECK ANONYMOUS PERMISSION
400 417 # ======================================================================
401 418 if action in ['pull', 'push']:
402 419 anonymous_user = User.get_default_user()
403 420 username = anonymous_user.username
404 421 if anonymous_user.active:
405 422 # ONLY check permissions if the user is activated
406 423 anonymous_perm = self._check_permission(
407 424 action, anonymous_user, self.acl_repo_name, ip_addr)
408 425 else:
409 426 anonymous_perm = False
410 427
411 428 if not anonymous_user.active or not anonymous_perm:
412 429 if not anonymous_user.active:
413 430 log.debug('Anonymous access is disabled, running '
414 431 'authentication')
415 432
416 433 if not anonymous_perm:
417 434 log.debug('Not enough credentials to access this '
418 435 'repository as anonymous user')
419 436
420 437 username = None
421 438 # ==============================================================
422 439 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
423 440 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
424 441 # ==============================================================
425 442
426 443 # try to auth based on environ, container auth methods
427 444 log.debug('Running PRE-AUTH for container based authentication')
428 445 pre_auth = authenticate(
429 446 '', '', environ, VCS_TYPE, registry=self.registry,
430 447 acl_repo_name=self.acl_repo_name)
431 448 if pre_auth and pre_auth.get('username'):
432 449 username = pre_auth['username']
433 450 log.debug('PRE-AUTH got %s as username', username)
434 451 if pre_auth:
435 452 log.debug('PRE-AUTH successful from %s',
436 453 pre_auth.get('auth_data', {}).get('_plugin'))
437 454
438 455 # If not authenticated by the container, running basic auth
439 456 # before inject the calling repo_name for special scope checks
440 457 self.authenticate.acl_repo_name = self.acl_repo_name
441 458
442 459 plugin_cache_active, cache_ttl = False, 0
443 460 plugin = None
444 461 if not username:
445 462 self.authenticate.realm = self.authenticate.get_rc_realm()
446 463
447 464 try:
448 465 auth_result = self.authenticate(environ)
449 466 except (UserCreationError, NotAllowedToCreateUserError) as e:
450 467 log.error(e)
451 468 reason = safe_str(e)
452 469 return HTTPNotAcceptable(reason)(environ, start_response)
453 470
454 471 if isinstance(auth_result, dict):
455 472 AUTH_TYPE.update(environ, 'basic')
456 473 REMOTE_USER.update(environ, auth_result['username'])
457 474 username = auth_result['username']
458 475 plugin = auth_result.get('auth_data', {}).get('_plugin')
459 476 log.info(
460 477 'MAIN-AUTH successful for user `%s` from %s plugin',
461 478 username, plugin)
462 479
463 480 plugin_cache_active, cache_ttl = auth_result.get(
464 481 'auth_data', {}).get('_ttl_cache') or (False, 0)
465 482 else:
466 483 return auth_result.wsgi_application(
467 484 environ, start_response)
468 485
469 486
470 487 # ==============================================================
471 488 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
472 489 # ==============================================================
473 490 user = User.get_by_username(username)
474 491 if not self.valid_and_active_user(user):
475 492 return HTTPForbidden()(environ, start_response)
476 493 username = user.username
477 494 user.update_lastactivity()
478 495 meta.Session().commit()
479 496
480 497 # check user attributes for password change flag
481 498 user_obj = user
482 499 if user_obj and user_obj.username != User.DEFAULT_USER and \
483 500 user_obj.user_data.get('force_password_change'):
484 501 reason = 'password change required'
485 502 log.debug('User not allowed to authenticate, %s', reason)
486 503 return HTTPNotAcceptable(reason)(environ, start_response)
487 504
488 505 # check permissions for this repository
489 506 perm = self._check_permission(
490 507 action, user, self.acl_repo_name, ip_addr,
491 508 plugin, plugin_cache_active, cache_ttl)
492 509 if not perm:
493 510 return HTTPForbidden()(environ, start_response)
494 511
495 512 # extras are injected into UI object and later available
496 513 # in hooks executed by RhodeCode
497 514 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
498 515 extras = vcs_operation_context(
499 516 environ, repo_name=self.acl_repo_name, username=username,
500 517 action=action, scm=self.SCM, check_locking=check_locking,
501 518 is_shadow_repo=self.is_shadow_repo
502 519 )
503 520
504 521 # ======================================================================
505 522 # REQUEST HANDLING
506 523 # ======================================================================
507 524 repo_path = os.path.join(
508 525 safe_str(self.base_path), safe_str(self.vcs_repo_name))
509 526 log.debug('Repository path is %s', repo_path)
510 527
511 528 fix_PATH()
512 529
513 530 log.info(
514 531 '%s action on %s repo "%s" by "%s" from %s %s',
515 532 action, self.SCM, safe_str(self.url_repo_name),
516 533 safe_str(username), ip_addr, user_agent)
517 534
518 535 return self._generate_vcs_response(
519 536 environ, start_response, repo_path, extras, action)
520 537
521 538 @initialize_generator
522 539 def _generate_vcs_response(
523 540 self, environ, start_response, repo_path, extras, action):
524 541 """
525 542 Returns a generator for the response content.
526 543
527 544 This method is implemented as a generator, so that it can trigger
528 545 the cache validation after all content sent back to the client. It
529 546 also handles the locking exceptions which will be triggered when
530 547 the first chunk is produced by the underlying WSGI application.
531 548 """
532 549 callback_daemon, extras = self._prepare_callback_daemon(extras)
533 550 config = self._create_config(extras, self.acl_repo_name)
534 551 log.debug('HOOKS extras is %s', extras)
535 552 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
536 553 app.rc_extras = extras
537 554
538 555 try:
539 556 with callback_daemon:
540 557 try:
541 558 response = app(environ, start_response)
542 559 finally:
543 560 # This statement works together with the decorator
544 561 # "initialize_generator" above. The decorator ensures that
545 562 # we hit the first yield statement before the generator is
546 563 # returned back to the WSGI server. This is needed to
547 564 # ensure that the call to "app" above triggers the
548 565 # needed callback to "start_response" before the
549 566 # generator is actually used.
550 567 yield "__init__"
551 568
552 569 for chunk in response:
553 570 yield chunk
554 571 except Exception as exc:
555 572 # TODO: martinb: Exceptions are only raised in case of the Pyro4
556 573 # backend. Refactor this except block after dropping Pyro4 support.
557 574 # TODO: johbo: Improve "translating" back the exception.
558 575 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
559 576 exc = HTTPLockedRC(*exc.args)
560 577 _code = rhodecode.CONFIG.get('lock_ret_code')
561 578 log.debug('Repository LOCKED ret code %s!', (_code,))
562 579 elif getattr(exc, '_vcs_kind', None) == 'requirement':
563 580 log.debug(
564 581 'Repository requires features unknown to this Mercurial')
565 582 exc = HTTPRequirementError(*exc.args)
566 583 else:
567 584 raise
568 585
569 586 for chunk in exc(environ, start_response):
570 587 yield chunk
571 588 finally:
572 589 # invalidate cache on push
573 590 try:
574 591 if action == 'push':
575 592 self._invalidate_cache(self.url_repo_name)
576 593 finally:
577 594 meta.Session.remove()
578 595
579 596 def _get_repository_name(self, environ):
580 597 """Get repository name out of the environmnent
581 598
582 599 :param environ: WSGI environment
583 600 """
584 601 raise NotImplementedError()
585 602
586 603 def _get_action(self, environ):
587 604 """Map request commands into a pull or push command.
588 605
589 606 :param environ: WSGI environment
590 607 """
591 608 raise NotImplementedError()
592 609
593 610 def _create_wsgi_app(self, repo_path, repo_name, config):
594 611 """Return the WSGI app that will finally handle the request."""
595 612 raise NotImplementedError()
596 613
597 614 def _create_config(self, extras, repo_name):
598 615 """Create a safe config representation."""
599 616 raise NotImplementedError()
600 617
601 618 def _prepare_callback_daemon(self, extras):
602 619 return prepare_callback_daemon(
603 620 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
604 621 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
605 622
606 623
607 624 def _should_check_locking(query_string):
608 625 # this is kind of hacky, but due to how mercurial handles client-server
609 626 # server see all operation on commit; bookmarks, phases and
610 627 # obsolescence marker in different transaction, we don't want to check
611 628 # locking on those
612 629 return query_string not in ['cmd=listkeys']
@@ -1,138 +1,151 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 'REQUEST_METHOD': 'GET',
51 52 'PATH_INFO': svn_repo_path,
52 53 'QUERY_STRING': 'cmd=',
53 54 'HTTP_ACCEPT': 'application/mercurial'
54 55 }
55 56 assert not vcs.is_hg(environ)
56 57
57 58
58 59 def test_is_svn_returns_true_if_subversion_is_in_a_dav_header():
59 60 environ = {
61 'REQUEST_METHOD': 'GET',
60 62 'PATH_INFO': svn_repo_path,
61 63 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log-revprops'
62 64 }
63 65 assert vcs.is_svn(environ) is True
64 66
65 67
66 68 def test_is_svn_returns_false_if_subversion_is_not_in_a_dav_header():
67 69 environ = {
70 'REQUEST_METHOD': 'GET',
68 71 'PATH_INFO': svn_repo_path,
69 72 'HTTP_DAV': 'http://stuff.tigris.org/xmlns/dav/svn/log-revprops'
70 73 }
71 74 assert vcs.is_svn(environ) is False
72 75
73 76
74 77 def test_is_svn_returns_false_if_no_dav_header():
75 78 environ = {
79 'REQUEST_METHOD': 'GET',
76 80 'PATH_INFO': svn_repo_path,
77 81 }
78 82 assert vcs.is_svn(environ) is False
79 83
80 84
81 85 def test_is_svn_returns_true_if_magic_path_segment():
82 86 environ = {
83 87 'PATH_INFO': '/stub-repository/!svn/rev/4',
84 88 }
85 89 assert vcs.is_svn(environ)
86 90
87 91
92 def test_is_svn_returns_true_if_propfind():
93 environ = {
94 'REQUEST_METHOD': 'PROPFIND',
95 'PATH_INFO': svn_repo_path,
96 }
97 assert vcs.is_svn(environ) is True
98
99
88 100 def test_is_svn_allows_to_configure_the_magic_path(monkeypatch):
89 101 """
90 102 This is intended as a fallback in case someone has configured his
91 103 Subversion server with a different magic path segment.
92 104 """
93 105 monkeypatch.setitem(
94 106 rhodecode.CONFIG, 'rhodecode_subversion_magic_path', '/!my-magic')
95 107 environ = {
108 'REQUEST_METHOD': 'POST',
96 109 'PATH_INFO': '/stub-repository/!my-magic/rev/4',
97 110 }
98 111 assert vcs.is_svn(environ)
99 112
100 113
101 114 class TestVCSMiddleware(object):
102 115 def test_get_handler_app_retuns_svn_app_when_proxy_enabled(self, app):
103 116 environ = {
104 117 'PATH_INFO': SVN_REPO,
105 118 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
106 119 }
107 120 application = Mock()
108 121 config = {'appenlight': False, 'vcs.backends': ['svn']}
109 122 registry = Mock()
110 123 middleware = vcs.VCSMiddleware(
111 124 application, registry, config, appenlight_client=None)
112 125 middleware.use_gzip = False
113 126
114 127 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
115 128 mock_method.return_value = True
116 129 application = middleware._get_handler_app(environ)
117 130 assert isinstance(application, SimpleSvn)
118 131 assert isinstance(application._create_wsgi_app(
119 132 Mock(), Mock(), Mock()), SimpleSvnApp)
120 133
121 134 def test_get_handler_app_retuns_dummy_svn_app_when_proxy_disabled(self, app):
122 135 environ = {
123 136 'PATH_INFO': SVN_REPO,
124 137 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
125 138 }
126 139 application = Mock()
127 140 config = {'appenlight': False, 'vcs.backends': ['svn']}
128 141 registry = Mock()
129 142 middleware = vcs.VCSMiddleware(
130 143 application, registry, config, appenlight_client=None)
131 144 middleware.use_gzip = False
132 145
133 146 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
134 147 mock_method.return_value = False
135 148 application = middleware._get_handler_app(environ)
136 149 assert isinstance(application, SimpleSvn)
137 150 assert isinstance(application._create_wsgi_app(
138 151 Mock(), Mock(), Mock()), DisabledSimpleSvnApp)
General Comments 0
You need to be logged in to leave comments. Login now