##// END OF EJS Templates
vcs: Only allow 'pull' actions on shadow repositories....
Martin Bornhold -
r891:910a0be0 default
parent child Browse files
Show More
@@ -1,479 +1,488 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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 logging
27 import logging
28 import importlib
28 import importlib
29 import re
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
33 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 NotAllowedToCreateUserError)
42 NotAllowedToCreateUserError)
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware.utils import scm_app
45 from rhodecode.lib.middleware.utils import scm_app
46 from rhodecode.lib.utils import (
46 from rhodecode.lib.utils import (
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.backends import base
50 from rhodecode.lib.vcs.backends import base
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.db import User, Repository
52 from rhodecode.model.db import User, Repository
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def initialize_generator(factory):
59 def initialize_generator(factory):
60 """
60 """
61 Initializes the returned generator by draining its first element.
61 Initializes the returned generator by draining its first element.
62
62
63 This can be used to give a generator an initializer, which is the code
63 This can be used to give a generator an initializer, which is the code
64 up to the first yield statement. This decorator enforces that the first
64 up to the first yield statement. This decorator enforces that the first
65 produced element has the value ``"__init__"`` to make its special
65 produced element has the value ``"__init__"`` to make its special
66 purpose very explicit in the using code.
66 purpose very explicit in the using code.
67 """
67 """
68
68
69 @wraps(factory)
69 @wraps(factory)
70 def wrapper(*args, **kwargs):
70 def wrapper(*args, **kwargs):
71 gen = factory(*args, **kwargs)
71 gen = factory(*args, **kwargs)
72 try:
72 try:
73 init = gen.next()
73 init = gen.next()
74 except StopIteration:
74 except StopIteration:
75 raise ValueError('Generator must yield at least one element.')
75 raise ValueError('Generator must yield at least one element.')
76 if init != "__init__":
76 if init != "__init__":
77 raise ValueError('First yielded element must be "__init__".')
77 raise ValueError('First yielded element must be "__init__".')
78 return gen
78 return gen
79 return wrapper
79 return wrapper
80
80
81
81
82 class SimpleVCS(object):
82 class SimpleVCS(object):
83 """Common functionality for SCM HTTP handlers."""
83 """Common functionality for SCM HTTP handlers."""
84
84
85 SCM = 'unknown'
85 SCM = 'unknown'
86
86
87 acl_repo_name = None
87 acl_repo_name = None
88 url_repo_name = None
88 url_repo_name = None
89 vcs_repo_name = None
89 vcs_repo_name = None
90
90
91 def __init__(self, application, config, registry):
91 def __init__(self, application, config, registry):
92 self.registry = registry
92 self.registry = registry
93 self.application = application
93 self.application = application
94 self.config = config
94 self.config = config
95 # re-populated by specialized middleware
95 # re-populated by specialized middleware
96 self.repo_vcs_config = base.Config()
96 self.repo_vcs_config = base.Config()
97
97
98 # base path of repo locations
98 # base path of repo locations
99 self.basepath = get_rhodecode_base_path()
99 self.basepath = get_rhodecode_base_path()
100 # authenticate this VCS request using authfunc
100 # authenticate this VCS request using authfunc
101 auth_ret_code_detection = \
101 auth_ret_code_detection = \
102 str2bool(self.config.get('auth_ret_code_detection', False))
102 str2bool(self.config.get('auth_ret_code_detection', False))
103 self.authenticate = BasicAuth(
103 self.authenticate = BasicAuth(
104 '', authenticate, registry, config.get('auth_ret_code'),
104 '', authenticate, registry, config.get('auth_ret_code'),
105 auth_ret_code_detection)
105 auth_ret_code_detection)
106 self.ip_addr = '0.0.0.0'
106 self.ip_addr = '0.0.0.0'
107
107
108 def set_repo_names(self, environ):
108 def set_repo_names(self, environ):
109 """
109 """
110 This will populate the attributes acl_repo_name, url_repo_name and
110 This will populate the attributes acl_repo_name, url_repo_name and
111 vcs_repo_name on the current instance.
111 vcs_repo_name on the current instance.
112 """
112 """
113 # TODO: martinb: Move to class or module scope.
113 # TODO: martinb: Move to class or module scope.
114 pr_regex = re.compile(
114 pr_regex = re.compile(
115 '(?P<base_name>(?:[\w-]+)(?:/[\w-]+)*)/'
115 '(?P<base_name>(?:[\w-]+)(?:/[\w-]+)*)/'
116 '(?P<repo_name>[\w-]+)'
116 '(?P<repo_name>[\w-]+)'
117 '/pull-request/(?P<pr_id>\d+)/repository')
117 '/pull-request/(?P<pr_id>\d+)/repository')
118
118
119 self.url_repo_name = self._get_repository_name(environ)
119 self.url_repo_name = self._get_repository_name(environ)
120 match = pr_regex.match(self.url_repo_name)
120 match = pr_regex.match(self.url_repo_name)
121
121
122 if match:
122 if match:
123 match_dict = match.groupdict()
123 match_dict = match.groupdict()
124 self.acl_repo_name = '{base_name}/{repo_name}'.format(**match_dict)
124 self.acl_repo_name = '{base_name}/{repo_name}'.format(**match_dict)
125 self.vcs_repo_name = '{base_name}/.__shadow_{repo_name}_pr-{pr_id}'.format(
125 self.vcs_repo_name = '{base_name}/.__shadow_{repo_name}_pr-{pr_id}'.format(
126 **match_dict)
126 **match_dict)
127 self.pr_id = match_dict['pr_id']
127 self.pr_id = match_dict['pr_id']
128 else:
128 else:
129 self.acl_repo_name = self.url_repo_name
129 self.acl_repo_name = self.url_repo_name
130 self.vcs_repo_name = self.url_repo_name
130 self.vcs_repo_name = self.url_repo_name
131 self.pr_id = None
131 self.pr_id = None
132
132
133 @property
133 @property
134 def repo_name(self):
134 def repo_name(self):
135 # TODO: johbo: Remove, switch to correct repo name attribute
135 # TODO: johbo: Remove, switch to correct repo name attribute
136 return self.acl_repo_name
136 return self.acl_repo_name
137
137
138 @property
138 @property
139 def scm_app(self):
139 def scm_app(self):
140 custom_implementation = self.config.get('vcs.scm_app_implementation')
140 custom_implementation = self.config.get('vcs.scm_app_implementation')
141 if custom_implementation and custom_implementation != 'pyro4':
141 if custom_implementation and custom_implementation != 'pyro4':
142 log.info(
142 log.info(
143 "Using custom implementation of scm_app: %s",
143 "Using custom implementation of scm_app: %s",
144 custom_implementation)
144 custom_implementation)
145 scm_app_impl = importlib.import_module(custom_implementation)
145 scm_app_impl = importlib.import_module(custom_implementation)
146 else:
146 else:
147 scm_app_impl = scm_app
147 scm_app_impl = scm_app
148 return scm_app_impl
148 return scm_app_impl
149
149
150 def _get_by_id(self, repo_name):
150 def _get_by_id(self, repo_name):
151 """
151 """
152 Gets a special pattern _<ID> from clone url and tries to replace it
152 Gets a special pattern _<ID> from clone url and tries to replace it
153 with a repository_name for support of _<ID> non changeable urls
153 with a repository_name for support of _<ID> non changeable urls
154 """
154 """
155
155
156 data = repo_name.split('/')
156 data = repo_name.split('/')
157 if len(data) >= 2:
157 if len(data) >= 2:
158 from rhodecode.model.repo import RepoModel
158 from rhodecode.model.repo import RepoModel
159 by_id_match = RepoModel().get_repo_by_id(repo_name)
159 by_id_match = RepoModel().get_repo_by_id(repo_name)
160 if by_id_match:
160 if by_id_match:
161 data[1] = by_id_match.repo_name
161 data[1] = by_id_match.repo_name
162
162
163 return safe_str('/'.join(data))
163 return safe_str('/'.join(data))
164
164
165 def _invalidate_cache(self, repo_name):
165 def _invalidate_cache(self, repo_name):
166 """
166 """
167 Set's cache for this repository for invalidation on next access
167 Set's cache for this repository for invalidation on next access
168
168
169 :param repo_name: full repo name, also a cache key
169 :param repo_name: full repo name, also a cache key
170 """
170 """
171 ScmModel().mark_for_invalidation(repo_name)
171 ScmModel().mark_for_invalidation(repo_name)
172
172
173 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
173 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
174 db_repo = Repository.get_by_repo_name(repo_name)
174 db_repo = Repository.get_by_repo_name(repo_name)
175 if not db_repo:
175 if not db_repo:
176 log.debug('Repository `%s` not found inside the database.',
176 log.debug('Repository `%s` not found inside the database.',
177 repo_name)
177 repo_name)
178 return False
178 return False
179
179
180 if db_repo.repo_type != scm_type:
180 if db_repo.repo_type != scm_type:
181 log.warning(
181 log.warning(
182 'Repository `%s` have incorrect scm_type, expected %s got %s',
182 'Repository `%s` have incorrect scm_type, expected %s got %s',
183 repo_name, db_repo.repo_type, scm_type)
183 repo_name, db_repo.repo_type, scm_type)
184 return False
184 return False
185
185
186 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
186 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
187
187
188 def valid_and_active_user(self, user):
188 def valid_and_active_user(self, user):
189 """
189 """
190 Checks if that user is not empty, and if it's actually object it checks
190 Checks if that user is not empty, and if it's actually object it checks
191 if he's active.
191 if he's active.
192
192
193 :param user: user object or None
193 :param user: user object or None
194 :return: boolean
194 :return: boolean
195 """
195 """
196 if user is None:
196 if user is None:
197 return False
197 return False
198
198
199 elif user.active:
199 elif user.active:
200 return True
200 return True
201
201
202 return False
202 return False
203
203
204 def _check_permission(self, action, user, repo_name, ip_addr=None):
204 def _check_permission(self, action, user, repo_name, ip_addr=None):
205 """
205 """
206 Checks permissions using action (push/pull) user and repository
206 Checks permissions using action (push/pull) user and repository
207 name
207 name
208
208
209 :param action: push or pull action
209 :param action: push or pull action
210 :param user: user instance
210 :param user: user instance
211 :param repo_name: repository name
211 :param repo_name: repository name
212 """
212 """
213 # check IP
213 # check IP
214 inherit = user.inherit_default_permissions
214 inherit = user.inherit_default_permissions
215 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
215 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
216 inherit_from_default=inherit)
216 inherit_from_default=inherit)
217 if ip_allowed:
217 if ip_allowed:
218 log.info('Access for IP:%s allowed', ip_addr)
218 log.info('Access for IP:%s allowed', ip_addr)
219 else:
219 else:
220 return False
220 return False
221
221
222 if action == 'push':
222 if action == 'push':
223 if not HasPermissionAnyMiddleware('repository.write',
223 if not HasPermissionAnyMiddleware('repository.write',
224 'repository.admin')(user,
224 'repository.admin')(user,
225 repo_name):
225 repo_name):
226 return False
226 return False
227
227
228 else:
228 else:
229 # any other action need at least read permission
229 # any other action need at least read permission
230 if not HasPermissionAnyMiddleware('repository.read',
230 if not HasPermissionAnyMiddleware('repository.read',
231 'repository.write',
231 'repository.write',
232 'repository.admin')(user,
232 'repository.admin')(user,
233 repo_name):
233 repo_name):
234 return False
234 return False
235
235
236 return True
236 return True
237
237
238 def _check_ssl(self, environ, start_response):
238 def _check_ssl(self, environ, start_response):
239 """
239 """
240 Checks the SSL check flag and returns False if SSL is not present
240 Checks the SSL check flag and returns False if SSL is not present
241 and required True otherwise
241 and required True otherwise
242 """
242 """
243 org_proto = environ['wsgi._org_proto']
243 org_proto = environ['wsgi._org_proto']
244 # check if we have SSL required ! if not it's a bad request !
244 # check if we have SSL required ! if not it's a bad request !
245 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
245 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
246 if require_ssl and org_proto == 'http':
246 if require_ssl and org_proto == 'http':
247 log.debug('proto is %s and SSL is required BAD REQUEST !',
247 log.debug('proto is %s and SSL is required BAD REQUEST !',
248 org_proto)
248 org_proto)
249 return False
249 return False
250 return True
250 return True
251
251
252 def __call__(self, environ, start_response):
252 def __call__(self, environ, start_response):
253 try:
253 try:
254 return self._handle_request(environ, start_response)
254 return self._handle_request(environ, start_response)
255 except Exception:
255 except Exception:
256 log.exception("Exception while handling request")
256 log.exception("Exception while handling request")
257 appenlight.track_exception(environ)
257 appenlight.track_exception(environ)
258 return HTTPInternalServerError()(environ, start_response)
258 return HTTPInternalServerError()(environ, start_response)
259 finally:
259 finally:
260 meta.Session.remove()
260 meta.Session.remove()
261
261
262 def _handle_request(self, environ, start_response):
262 def _handle_request(self, environ, start_response):
263
263
264 if not self._check_ssl(environ, start_response):
264 if not self._check_ssl(environ, start_response):
265 reason = ('SSL required, while RhodeCode was unable '
265 reason = ('SSL required, while RhodeCode was unable '
266 'to detect this as SSL request')
266 'to detect this as SSL request')
267 log.debug('User not allowed to proceed, %s', reason)
267 log.debug('User not allowed to proceed, %s', reason)
268 return HTTPNotAcceptable(reason)(environ, start_response)
268 return HTTPNotAcceptable(reason)(environ, start_response)
269
269
270 if not self.repo_name:
270 if not self.repo_name:
271 log.warning('Repository name is empty: %s', self.repo_name)
271 log.warning('Repository name is empty: %s', self.repo_name)
272 # failed to get repo name, we fail now
272 # failed to get repo name, we fail now
273 return HTTPNotFound()(environ, start_response)
273 return HTTPNotFound()(environ, start_response)
274 log.debug('Extracted repo name is %s', self.repo_name)
274 log.debug('Extracted repo name is %s', self.repo_name)
275
275
276 ip_addr = get_ip_addr(environ)
276 ip_addr = get_ip_addr(environ)
277 username = None
277 username = None
278
278
279 # skip passing error to error controller
279 # skip passing error to error controller
280 environ['pylons.status_code_redirect'] = True
280 environ['pylons.status_code_redirect'] = True
281
281
282 # ======================================================================
282 # ======================================================================
283 # GET ACTION PULL or PUSH
283 # GET ACTION PULL or PUSH
284 # ======================================================================
284 # ======================================================================
285 action = self._get_action(environ)
285 action = self._get_action(environ)
286
286
287 # ======================================================================
287 # ======================================================================
288 # Check if this is a request to a shadow repository of a pull request.
289 # In this case only pull action is allowed.
290 # ======================================================================
291 if self.pr_id is not None and action != 'pull':
292 reason = 'Only pull action is allowed for shadow repositories.'
293 log.debug('User not allowed to proceed, %s', reason)
294 return HTTPNotAcceptable(reason)(environ, start_response)
295
296 # ======================================================================
288 # CHECK ANONYMOUS PERMISSION
297 # CHECK ANONYMOUS PERMISSION
289 # ======================================================================
298 # ======================================================================
290 if action in ['pull', 'push']:
299 if action in ['pull', 'push']:
291 anonymous_user = User.get_default_user()
300 anonymous_user = User.get_default_user()
292 username = anonymous_user.username
301 username = anonymous_user.username
293 if anonymous_user.active:
302 if anonymous_user.active:
294 # ONLY check permissions if the user is activated
303 # ONLY check permissions if the user is activated
295 anonymous_perm = self._check_permission(
304 anonymous_perm = self._check_permission(
296 action, anonymous_user, self.repo_name, ip_addr)
305 action, anonymous_user, self.repo_name, ip_addr)
297 else:
306 else:
298 anonymous_perm = False
307 anonymous_perm = False
299
308
300 if not anonymous_user.active or not anonymous_perm:
309 if not anonymous_user.active or not anonymous_perm:
301 if not anonymous_user.active:
310 if not anonymous_user.active:
302 log.debug('Anonymous access is disabled, running '
311 log.debug('Anonymous access is disabled, running '
303 'authentication')
312 'authentication')
304
313
305 if not anonymous_perm:
314 if not anonymous_perm:
306 log.debug('Not enough credentials to access this '
315 log.debug('Not enough credentials to access this '
307 'repository as anonymous user')
316 'repository as anonymous user')
308
317
309 username = None
318 username = None
310 # ==============================================================
319 # ==============================================================
311 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
320 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
312 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
321 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
313 # ==============================================================
322 # ==============================================================
314
323
315 # try to auth based on environ, container auth methods
324 # try to auth based on environ, container auth methods
316 log.debug('Running PRE-AUTH for container based authentication')
325 log.debug('Running PRE-AUTH for container based authentication')
317 pre_auth = authenticate(
326 pre_auth = authenticate(
318 '', '', environ, VCS_TYPE, registry=self.registry)
327 '', '', environ, VCS_TYPE, registry=self.registry)
319 if pre_auth and pre_auth.get('username'):
328 if pre_auth and pre_auth.get('username'):
320 username = pre_auth['username']
329 username = pre_auth['username']
321 log.debug('PRE-AUTH got %s as username', username)
330 log.debug('PRE-AUTH got %s as username', username)
322
331
323 # If not authenticated by the container, running basic auth
332 # If not authenticated by the container, running basic auth
324 if not username:
333 if not username:
325 self.authenticate.realm = get_rhodecode_realm()
334 self.authenticate.realm = get_rhodecode_realm()
326
335
327 try:
336 try:
328 result = self.authenticate(environ)
337 result = self.authenticate(environ)
329 except (UserCreationError, NotAllowedToCreateUserError) as e:
338 except (UserCreationError, NotAllowedToCreateUserError) as e:
330 log.error(e)
339 log.error(e)
331 reason = safe_str(e)
340 reason = safe_str(e)
332 return HTTPNotAcceptable(reason)(environ, start_response)
341 return HTTPNotAcceptable(reason)(environ, start_response)
333
342
334 if isinstance(result, str):
343 if isinstance(result, str):
335 AUTH_TYPE.update(environ, 'basic')
344 AUTH_TYPE.update(environ, 'basic')
336 REMOTE_USER.update(environ, result)
345 REMOTE_USER.update(environ, result)
337 username = result
346 username = result
338 else:
347 else:
339 return result.wsgi_application(environ, start_response)
348 return result.wsgi_application(environ, start_response)
340
349
341 # ==============================================================
350 # ==============================================================
342 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
351 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
343 # ==============================================================
352 # ==============================================================
344 user = User.get_by_username(username)
353 user = User.get_by_username(username)
345 if not self.valid_and_active_user(user):
354 if not self.valid_and_active_user(user):
346 return HTTPForbidden()(environ, start_response)
355 return HTTPForbidden()(environ, start_response)
347 username = user.username
356 username = user.username
348 user.update_lastactivity()
357 user.update_lastactivity()
349 meta.Session().commit()
358 meta.Session().commit()
350
359
351 # check user attributes for password change flag
360 # check user attributes for password change flag
352 user_obj = user
361 user_obj = user
353 if user_obj and user_obj.username != User.DEFAULT_USER and \
362 if user_obj and user_obj.username != User.DEFAULT_USER and \
354 user_obj.user_data.get('force_password_change'):
363 user_obj.user_data.get('force_password_change'):
355 reason = 'password change required'
364 reason = 'password change required'
356 log.debug('User not allowed to authenticate, %s', reason)
365 log.debug('User not allowed to authenticate, %s', reason)
357 return HTTPNotAcceptable(reason)(environ, start_response)
366 return HTTPNotAcceptable(reason)(environ, start_response)
358
367
359 # check permissions for this repository
368 # check permissions for this repository
360 perm = self._check_permission(
369 perm = self._check_permission(
361 action, user, self.repo_name, ip_addr)
370 action, user, self.repo_name, ip_addr)
362 if not perm:
371 if not perm:
363 return HTTPForbidden()(environ, start_response)
372 return HTTPForbidden()(environ, start_response)
364
373
365 # extras are injected into UI object and later available
374 # extras are injected into UI object and later available
366 # in hooks executed by rhodecode
375 # in hooks executed by rhodecode
367 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
376 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
368 extras = vcs_operation_context(
377 extras = vcs_operation_context(
369 environ, repo_name=self.repo_name, username=username,
378 environ, repo_name=self.repo_name, username=username,
370 action=action, scm=self.SCM,
379 action=action, scm=self.SCM,
371 check_locking=check_locking)
380 check_locking=check_locking)
372
381
373 # ======================================================================
382 # ======================================================================
374 # REQUEST HANDLING
383 # REQUEST HANDLING
375 # ======================================================================
384 # ======================================================================
376 str_repo_name = safe_str(self.repo_name)
385 str_repo_name = safe_str(self.repo_name)
377 repo_path = os.path.join(
386 repo_path = os.path.join(
378 safe_str(self.basepath), safe_str(self.vcs_repo_name))
387 safe_str(self.basepath), safe_str(self.vcs_repo_name))
379 log.debug('Repository path is %s', repo_path)
388 log.debug('Repository path is %s', repo_path)
380
389
381 fix_PATH()
390 fix_PATH()
382
391
383 log.info(
392 log.info(
384 '%s action on %s repo "%s" by "%s" from %s',
393 '%s action on %s repo "%s" by "%s" from %s',
385 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
394 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
386
395
387 return self._generate_vcs_response(
396 return self._generate_vcs_response(
388 environ, start_response, repo_path, self.url_repo_name, extras, action)
397 environ, start_response, repo_path, self.url_repo_name, extras, action)
389
398
390 @initialize_generator
399 @initialize_generator
391 def _generate_vcs_response(
400 def _generate_vcs_response(
392 self, environ, start_response, repo_path, repo_name, extras,
401 self, environ, start_response, repo_path, repo_name, extras,
393 action):
402 action):
394 """
403 """
395 Returns a generator for the response content.
404 Returns a generator for the response content.
396
405
397 This method is implemented as a generator, so that it can trigger
406 This method is implemented as a generator, so that it can trigger
398 the cache validation after all content sent back to the client. It
407 the cache validation after all content sent back to the client. It
399 also handles the locking exceptions which will be triggered when
408 also handles the locking exceptions which will be triggered when
400 the first chunk is produced by the underlying WSGI application.
409 the first chunk is produced by the underlying WSGI application.
401 """
410 """
402 callback_daemon, extras = self._prepare_callback_daemon(extras)
411 callback_daemon, extras = self._prepare_callback_daemon(extras)
403 config = self._create_config(extras, self.acl_repo_name)
412 config = self._create_config(extras, self.acl_repo_name)
404 log.debug('HOOKS extras is %s', extras)
413 log.debug('HOOKS extras is %s', extras)
405 app = self._create_wsgi_app(repo_path, repo_name, config)
414 app = self._create_wsgi_app(repo_path, repo_name, config)
406
415
407 try:
416 try:
408 with callback_daemon:
417 with callback_daemon:
409 try:
418 try:
410 response = app(environ, start_response)
419 response = app(environ, start_response)
411 finally:
420 finally:
412 # This statement works together with the decorator
421 # This statement works together with the decorator
413 # "initialize_generator" above. The decorator ensures that
422 # "initialize_generator" above. The decorator ensures that
414 # we hit the first yield statement before the generator is
423 # we hit the first yield statement before the generator is
415 # returned back to the WSGI server. This is needed to
424 # returned back to the WSGI server. This is needed to
416 # ensure that the call to "app" above triggers the
425 # ensure that the call to "app" above triggers the
417 # needed callback to "start_response" before the
426 # needed callback to "start_response" before the
418 # generator is actually used.
427 # generator is actually used.
419 yield "__init__"
428 yield "__init__"
420
429
421 for chunk in response:
430 for chunk in response:
422 yield chunk
431 yield chunk
423 except Exception as exc:
432 except Exception as exc:
424 # TODO: johbo: Improve "translating" back the exception.
433 # TODO: johbo: Improve "translating" back the exception.
425 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
434 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
426 exc = HTTPLockedRC(*exc.args)
435 exc = HTTPLockedRC(*exc.args)
427 _code = rhodecode.CONFIG.get('lock_ret_code')
436 _code = rhodecode.CONFIG.get('lock_ret_code')
428 log.debug('Repository LOCKED ret code %s!', (_code,))
437 log.debug('Repository LOCKED ret code %s!', (_code,))
429 elif getattr(exc, '_vcs_kind', None) == 'requirement':
438 elif getattr(exc, '_vcs_kind', None) == 'requirement':
430 log.debug(
439 log.debug(
431 'Repository requires features unknown to this Mercurial')
440 'Repository requires features unknown to this Mercurial')
432 exc = HTTPRequirementError(*exc.args)
441 exc = HTTPRequirementError(*exc.args)
433 else:
442 else:
434 raise
443 raise
435
444
436 for chunk in exc(environ, start_response):
445 for chunk in exc(environ, start_response):
437 yield chunk
446 yield chunk
438 finally:
447 finally:
439 # invalidate cache on push
448 # invalidate cache on push
440 try:
449 try:
441 if action == 'push':
450 if action == 'push':
442 self._invalidate_cache(repo_name)
451 self._invalidate_cache(repo_name)
443 finally:
452 finally:
444 meta.Session.remove()
453 meta.Session.remove()
445
454
446 def _get_repository_name(self, environ):
455 def _get_repository_name(self, environ):
447 """Get repository name out of the environmnent
456 """Get repository name out of the environmnent
448
457
449 :param environ: WSGI environment
458 :param environ: WSGI environment
450 """
459 """
451 raise NotImplementedError()
460 raise NotImplementedError()
452
461
453 def _get_action(self, environ):
462 def _get_action(self, environ):
454 """Map request commands into a pull or push command.
463 """Map request commands into a pull or push command.
455
464
456 :param environ: WSGI environment
465 :param environ: WSGI environment
457 """
466 """
458 raise NotImplementedError()
467 raise NotImplementedError()
459
468
460 def _create_wsgi_app(self, repo_path, repo_name, config):
469 def _create_wsgi_app(self, repo_path, repo_name, config):
461 """Return the WSGI app that will finally handle the request."""
470 """Return the WSGI app that will finally handle the request."""
462 raise NotImplementedError()
471 raise NotImplementedError()
463
472
464 def _create_config(self, extras, repo_name):
473 def _create_config(self, extras, repo_name):
465 """Create a Pyro safe config representation."""
474 """Create a Pyro safe config representation."""
466 raise NotImplementedError()
475 raise NotImplementedError()
467
476
468 def _prepare_callback_daemon(self, extras):
477 def _prepare_callback_daemon(self, extras):
469 return prepare_callback_daemon(
478 return prepare_callback_daemon(
470 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
479 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
471 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
480 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
472
481
473
482
474 def _should_check_locking(query_string):
483 def _should_check_locking(query_string):
475 # this is kind of hacky, but due to how mercurial handles client-server
484 # this is kind of hacky, but due to how mercurial handles client-server
476 # server see all operation on commit; bookmarks, phases and
485 # server see all operation on commit; bookmarks, phases and
477 # obsolescence marker in different transaction, we don't want to check
486 # obsolescence marker in different transaction, we don't want to check
478 # locking on those
487 # locking on those
479 return query_string not in ['cmd=listkeys']
488 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now