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