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