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