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