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