##// END OF EJS Templates
auth: because we use 404 for access denied too. Show proper message about it in error page
marcink -
r2115:4490c841 default
parent child Browse files
Show More
@@ -1,542 +1,545 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 import traceback
26 26 from collections import OrderedDict
27 27
28 28 from paste.registry import RegistryManager
29 29 from paste.gzipper import make_gzip_middleware
30 30 from pylons.wsgiapp import PylonsApp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.wsgi import wsgiapp
35 35 from pyramid.httpexceptions import (
36 36 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
37 37 from pyramid.events import ApplicationCreated
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import rhodecode
41 41
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config import utils as config_utils
45 45 from rhodecode.config.routing import STATIC_FILE_PREFIX
46 46 from rhodecode.config.environment import (
47 47 load_environment, load_pyramid_environment)
48 48
49 49 from rhodecode.lib.vcs import VCSCommunicationError
50 50 from rhodecode.lib.exceptions import VCSServerUnavailable
51 51 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
52 52 from rhodecode.lib.middleware.error_handling import (
53 53 PylonsErrorHandlingMiddleware)
54 54 from rhodecode.lib.middleware.https_fixup import HttpsFixup
55 55 from rhodecode.lib.middleware.vcs import VCSMiddleware
56 56 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
57 57 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
58 58 from rhodecode.subscribers import (
59 59 scan_repositories_if_enabled, write_js_routes_if_enabled,
60 60 write_metadata_if_needed, inject_app_settings)
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # this is used to avoid avoid the route lookup overhead in routesmiddleware
67 67 # for certain routes which won't go to pylons to - eg. static files, debugger
68 68 # it is only needed for the pylons migration and can be removed once complete
69 69 class SkippableRoutesMiddleware(RoutesMiddleware):
70 70 """ Routes middleware that allows you to skip prefixes """
71 71
72 72 def __init__(self, *args, **kw):
73 73 self.skip_prefixes = kw.pop('skip_prefixes', [])
74 74 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
75 75
76 76 def __call__(self, environ, start_response):
77 77 for prefix in self.skip_prefixes:
78 78 if environ['PATH_INFO'].startswith(prefix):
79 79 # added to avoid the case when a missing /_static route falls
80 80 # through to pylons and causes an exception as pylons is
81 81 # expecting wsgiorg.routingargs to be set in the environ
82 82 # by RoutesMiddleware.
83 83 if 'wsgiorg.routing_args' not in environ:
84 84 environ['wsgiorg.routing_args'] = (None, {})
85 85 return self.app(environ, start_response)
86 86
87 87 return super(SkippableRoutesMiddleware, self).__call__(
88 88 environ, start_response)
89 89
90 90
91 91 def make_app(global_conf, static_files=True, **app_conf):
92 92 """Create a Pylons WSGI application and return it
93 93
94 94 ``global_conf``
95 95 The inherited configuration for this application. Normally from
96 96 the [DEFAULT] section of the Paste ini file.
97 97
98 98 ``app_conf``
99 99 The application's local configuration. Normally specified in
100 100 the [app:<name>] section of the Paste ini file (where <name>
101 101 defaults to main).
102 102
103 103 """
104 104 # Apply compatibility patches
105 105 patches.kombu_1_5_1_python_2_7_11()
106 106 patches.inspect_getargspec()
107 107
108 108 # Configure the Pylons environment
109 109 config = load_environment(global_conf, app_conf)
110 110
111 111 # The Pylons WSGI app
112 112 app = PylonsApp(config=config)
113 113
114 114 # Establish the Registry for this application
115 115 app = RegistryManager(app)
116 116
117 117 app.config = config
118 118
119 119 return app
120 120
121 121
122 122 def make_pyramid_app(global_config, **settings):
123 123 """
124 124 Constructs the WSGI application based on Pyramid and wraps the Pylons based
125 125 application.
126 126
127 127 Specials:
128 128
129 129 * We migrate from Pylons to Pyramid. While doing this, we keep both
130 130 frameworks functional. This involves moving some WSGI middlewares around
131 131 and providing access to some data internals, so that the old code is
132 132 still functional.
133 133
134 134 * The application can also be integrated like a plugin via the call to
135 135 `includeme`. This is accompanied with the other utility functions which
136 136 are called. Changing this should be done with great care to not break
137 137 cases when these fragments are assembled from another place.
138 138
139 139 """
140 140 # The edition string should be available in pylons too, so we add it here
141 141 # before copying the settings.
142 142 settings.setdefault('rhodecode.edition', 'Community Edition')
143 143
144 144 # As long as our Pylons application does expect "unprepared" settings, make
145 145 # sure that we keep an unmodified copy. This avoids unintentional change of
146 146 # behavior in the old application.
147 147 settings_pylons = settings.copy()
148 148
149 149 sanitize_settings_and_apply_defaults(settings)
150 150
151 151 config = Configurator(settings=settings)
152 152 load_pyramid_environment(global_config, settings)
153 153
154 154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
155 155
156 156 includeme_first(config)
157 157 includeme(config)
158 158
159 159 pyramid_app = config.make_wsgi_app()
160 160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
161 161 pyramid_app.config = config
162 162
163 163 # creating the app uses a connection - return it after we are done
164 164 meta.Session.remove()
165 165
166 166 return pyramid_app
167 167
168 168
169 169 def make_not_found_view(config):
170 170 """
171 171 This creates the view which should be registered as not-found-view to
172 172 pyramid. Basically it contains of the old pylons app, converted to a view.
173 173 Additionally it is wrapped by some other middlewares.
174 174 """
175 175 settings = config.registry.settings
176 176 vcs_server_enabled = settings['vcs.server.enable']
177 177
178 178 # Make pylons app from unprepared settings.
179 179 pylons_app = make_app(
180 180 config.registry._pylons_compat_global_config,
181 181 **config.registry._pylons_compat_settings)
182 182 config.registry._pylons_compat_config = pylons_app.config
183 183
184 184 # Appenlight monitoring.
185 185 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
186 186 pylons_app, settings)
187 187
188 188 # The pylons app is executed inside of the pyramid 404 exception handler.
189 189 # Exceptions which are raised inside of it are not handled by pyramid
190 190 # again. Therefore we add a middleware that invokes the error handler in
191 191 # case of an exception or error response. This way we return proper error
192 192 # HTML pages in case of an error.
193 193 reraise = (settings.get('debugtoolbar.enabled', False) or
194 194 rhodecode.disable_error_handler)
195 195 pylons_app = PylonsErrorHandlingMiddleware(
196 196 pylons_app, error_handler, reraise)
197 197
198 198 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
199 199 # view to handle the request. Therefore it is wrapped around the pylons
200 200 # app. It has to be outside of the error handling otherwise error responses
201 201 # from the vcsserver are converted to HTML error pages. This confuses the
202 202 # command line tools and the user won't get a meaningful error message.
203 203 if vcs_server_enabled:
204 204 pylons_app = VCSMiddleware(
205 205 pylons_app, settings, appenlight_client, registry=config.registry)
206 206
207 207 # Convert WSGI app to pyramid view and return it.
208 208 return wsgiapp(pylons_app)
209 209
210 210
211 211 def add_pylons_compat_data(registry, global_config, settings):
212 212 """
213 213 Attach data to the registry to support the Pylons integration.
214 214 """
215 215 registry._pylons_compat_global_config = global_config
216 216 registry._pylons_compat_settings = settings
217 217
218 218
219 219 def error_handler(exception, request):
220 220 import rhodecode
221 221 from rhodecode.lib import helpers
222 222
223 223 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
224 224
225 225 base_response = HTTPInternalServerError()
226 226 # prefer original exception for the response since it may have headers set
227 227 if isinstance(exception, HTTPException):
228 228 base_response = exception
229 229 elif isinstance(exception, VCSCommunicationError):
230 230 base_response = VCSServerUnavailable()
231 231
232 232 def is_http_error(response):
233 233 # error which should have traceback
234 234 return response.status_code > 499
235 235
236 236 if is_http_error(base_response):
237 237 log.exception(
238 238 'error occurred handling this request for path: %s', request.path)
239 239
240 error_explanation = base_response.explanation or str(base_response)
241 if base_response.status_code == 404:
242 error_explanation += " Or you don't have permission to access it."
240 243 c = AttributeDict()
241 244 c.error_message = base_response.status
242 c.error_explanation = base_response.explanation or str(base_response)
245 c.error_explanation = error_explanation
243 246 c.visual = AttributeDict()
244 247
245 248 c.visual.rhodecode_support_url = (
246 249 request.registry.settings.get('rhodecode_support_url') or
247 250 request.route_url('rhodecode_support')
248 251 )
249 252 c.redirect_time = 0
250 253 c.rhodecode_name = rhodecode_title
251 254 if not c.rhodecode_name:
252 255 c.rhodecode_name = 'Rhodecode'
253 256
254 257 c.causes = []
255 258 if hasattr(base_response, 'causes'):
256 259 c.causes = base_response.causes
257 260 c.messages = helpers.flash.pop_messages(request=request)
258 261 c.traceback = traceback.format_exc()
259 262 response = render_to_response(
260 263 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
261 264 response=base_response)
262 265
263 266 return response
264 267
265 268
266 269 def includeme(config):
267 270 settings = config.registry.settings
268 271
269 272 # plugin information
270 273 config.registry.rhodecode_plugins = OrderedDict()
271 274
272 275 config.add_directive(
273 276 'register_rhodecode_plugin', register_rhodecode_plugin)
274 277
275 278 if asbool(settings.get('appenlight', 'false')):
276 279 config.include('appenlight_client.ext.pyramid_tween')
277 280
278 281 if 'mako.default_filters' not in settings:
279 282 # set custom default filters if we don't have it defined
280 283 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
281 284 settings['mako.default_filters'] = 'h_filter'
282 285
283 286 # Includes which are required. The application would fail without them.
284 287 config.include('pyramid_mako')
285 288 config.include('pyramid_beaker')
286 289
287 290 config.include('rhodecode.authentication')
288 291 config.include('rhodecode.integrations')
289 292
290 293 # apps
291 294 config.include('rhodecode.apps._base')
292 295 config.include('rhodecode.apps.ops')
293 296
294 297 config.include('rhodecode.apps.admin')
295 298 config.include('rhodecode.apps.channelstream')
296 299 config.include('rhodecode.apps.login')
297 300 config.include('rhodecode.apps.home')
298 301 config.include('rhodecode.apps.journal')
299 302 config.include('rhodecode.apps.repository')
300 303 config.include('rhodecode.apps.repo_group')
301 304 config.include('rhodecode.apps.user_group')
302 305 config.include('rhodecode.apps.search')
303 306 config.include('rhodecode.apps.user_profile')
304 307 config.include('rhodecode.apps.my_account')
305 308 config.include('rhodecode.apps.svn_support')
306 309 config.include('rhodecode.apps.ssh_support')
307 310 config.include('rhodecode.apps.gist')
308 311
309 312 config.include('rhodecode.apps.debug_style')
310 313 config.include('rhodecode.tweens')
311 314 config.include('rhodecode.api')
312 315
313 316 config.add_route(
314 317 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
315 318
316 319 config.add_translation_dirs('rhodecode:i18n/')
317 320 settings['default_locale_name'] = settings.get('lang', 'en')
318 321
319 322 # Add subscribers.
320 323 config.add_subscriber(inject_app_settings, ApplicationCreated)
321 324 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
322 325 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
323 326 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
324 327
325 328 config.add_request_method(
326 329 'rhodecode.lib.partial_renderer.get_partial_renderer',
327 330 'get_partial_renderer')
328 331
329 332 # events
330 333 # TODO(marcink): this should be done when pyramid migration is finished
331 334 # config.add_subscriber(
332 335 # 'rhodecode.integrations.integrations_event_handler',
333 336 # 'rhodecode.events.RhodecodeEvent')
334 337
335 338 # Set the authorization policy.
336 339 authz_policy = ACLAuthorizationPolicy()
337 340 config.set_authorization_policy(authz_policy)
338 341
339 342 # Set the default renderer for HTML templates to mako.
340 343 config.add_mako_renderer('.html')
341 344
342 345 config.add_renderer(
343 346 name='json_ext',
344 347 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
345 348
346 349 # include RhodeCode plugins
347 350 includes = aslist(settings.get('rhodecode.includes', []))
348 351 for inc in includes:
349 352 config.include(inc)
350 353
351 354 # This is the glue which allows us to migrate in chunks. By registering the
352 355 # pylons based application as the "Not Found" view in Pyramid, we will
353 356 # fallback to the old application each time the new one does not yet know
354 357 # how to handle a request.
355 358 config.add_notfound_view(make_not_found_view(config))
356 359
357 360 if not settings.get('debugtoolbar.enabled', False):
358 361 # disabled debugtoolbar handle all exceptions via the error_handlers
359 362 config.add_view(error_handler, context=Exception)
360 363
361 364 config.add_view(error_handler, context=HTTPError)
362 365
363 366
364 367 def includeme_first(config):
365 368 # redirect automatic browser favicon.ico requests to correct place
366 369 def favicon_redirect(context, request):
367 370 return HTTPFound(
368 371 request.static_path('rhodecode:public/images/favicon.ico'))
369 372
370 373 config.add_view(favicon_redirect, route_name='favicon')
371 374 config.add_route('favicon', '/favicon.ico')
372 375
373 376 def robots_redirect(context, request):
374 377 return HTTPFound(
375 378 request.static_path('rhodecode:public/robots.txt'))
376 379
377 380 config.add_view(robots_redirect, route_name='robots')
378 381 config.add_route('robots', '/robots.txt')
379 382
380 383 config.add_static_view(
381 384 '_static/deform', 'deform:static')
382 385 config.add_static_view(
383 386 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
384 387
385 388
386 389 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
387 390 """
388 391 Apply outer WSGI middlewares around the application.
389 392
390 393 Part of this has been moved up from the Pylons layer, so that the
391 394 data is also available if old Pylons code is hit through an already ported
392 395 view.
393 396 """
394 397 settings = config.registry.settings
395 398
396 399 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
397 400 pyramid_app = HttpsFixup(pyramid_app, settings)
398 401
399 402 # Add RoutesMiddleware to support the pylons compatibility tween during
400 403 # migration to pyramid.
401 404
402 405 # TODO(marcink): remove after migration to pyramid
403 406 if hasattr(config.registry, '_pylons_compat_config'):
404 407 routes_map = config.registry._pylons_compat_config['routes.map']
405 408 pyramid_app = SkippableRoutesMiddleware(
406 409 pyramid_app, routes_map,
407 410 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
408 411
409 412 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
410 413
411 414 if settings['gzip_responses']:
412 415 pyramid_app = make_gzip_middleware(
413 416 pyramid_app, settings, compress_level=1)
414 417
415 418 # this should be the outer most middleware in the wsgi stack since
416 419 # middleware like Routes make database calls
417 420 def pyramid_app_with_cleanup(environ, start_response):
418 421 try:
419 422 return pyramid_app(environ, start_response)
420 423 finally:
421 424 # Dispose current database session and rollback uncommitted
422 425 # transactions.
423 426 meta.Session.remove()
424 427
425 428 # In a single threaded mode server, on non sqlite db we should have
426 429 # '0 Current Checked out connections' at the end of a request,
427 430 # if not, then something, somewhere is leaving a connection open
428 431 pool = meta.Base.metadata.bind.engine.pool
429 432 log.debug('sa pool status: %s', pool.status())
430 433
431 434 return pyramid_app_with_cleanup
432 435
433 436
434 437 def sanitize_settings_and_apply_defaults(settings):
435 438 """
436 439 Applies settings defaults and does all type conversion.
437 440
438 441 We would move all settings parsing and preparation into this place, so that
439 442 we have only one place left which deals with this part. The remaining parts
440 443 of the application would start to rely fully on well prepared settings.
441 444
442 445 This piece would later be split up per topic to avoid a big fat monster
443 446 function.
444 447 """
445 448
446 449 # Pyramid's mako renderer has to search in the templates folder so that the
447 450 # old templates still work. Ported and new templates are expected to use
448 451 # real asset specifications for the includes.
449 452 mako_directories = settings.setdefault('mako.directories', [
450 453 # Base templates of the original Pylons application
451 454 'rhodecode:templates',
452 455 ])
453 456 log.debug(
454 457 "Using the following Mako template directories: %s",
455 458 mako_directories)
456 459
457 460 # Default includes, possible to change as a user
458 461 pyramid_includes = settings.setdefault('pyramid.includes', [
459 462 'rhodecode.lib.middleware.request_wrapper',
460 463 ])
461 464 log.debug(
462 465 "Using the following pyramid.includes: %s",
463 466 pyramid_includes)
464 467
465 468 # TODO: johbo: Re-think this, usually the call to config.include
466 469 # should allow to pass in a prefix.
467 470 settings.setdefault('rhodecode.api.url', '/_admin/api')
468 471
469 472 # Sanitize generic settings.
470 473 _list_setting(settings, 'default_encoding', 'UTF-8')
471 474 _bool_setting(settings, 'is_test', 'false')
472 475 _bool_setting(settings, 'gzip_responses', 'false')
473 476
474 477 # Call split out functions that sanitize settings for each topic.
475 478 _sanitize_appenlight_settings(settings)
476 479 _sanitize_vcs_settings(settings)
477 480
478 481 # configure instance id
479 482 config_utils.set_instance_id(settings)
480 483
481 484 return settings
482 485
483 486
484 487 def _sanitize_appenlight_settings(settings):
485 488 _bool_setting(settings, 'appenlight', 'false')
486 489
487 490
488 491 def _sanitize_vcs_settings(settings):
489 492 """
490 493 Applies settings defaults and does type conversion for all VCS related
491 494 settings.
492 495 """
493 496 _string_setting(settings, 'vcs.svn.compatible_version', '')
494 497 _string_setting(settings, 'git_rev_filter', '--all')
495 498 _string_setting(settings, 'vcs.hooks.protocol', 'http')
496 499 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
497 500 _string_setting(settings, 'vcs.server', '')
498 501 _string_setting(settings, 'vcs.server.log_level', 'debug')
499 502 _string_setting(settings, 'vcs.server.protocol', 'http')
500 503 _bool_setting(settings, 'startup.import_repos', 'false')
501 504 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
502 505 _bool_setting(settings, 'vcs.server.enable', 'true')
503 506 _bool_setting(settings, 'vcs.start_server', 'false')
504 507 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
505 508 _int_setting(settings, 'vcs.connection_timeout', 3600)
506 509
507 510 # Support legacy values of vcs.scm_app_implementation. Legacy
508 511 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
509 512 # which is now mapped to 'http'.
510 513 scm_app_impl = settings['vcs.scm_app_implementation']
511 514 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
512 515 settings['vcs.scm_app_implementation'] = 'http'
513 516
514 517
515 518 def _int_setting(settings, name, default):
516 519 settings[name] = int(settings.get(name, default))
517 520
518 521
519 522 def _bool_setting(settings, name, default):
520 523 input = settings.get(name, default)
521 524 if isinstance(input, unicode):
522 525 input = input.encode('utf8')
523 526 settings[name] = asbool(input)
524 527
525 528
526 529 def _list_setting(settings, name, default):
527 530 raw_value = settings.get(name, default)
528 531
529 532 old_separator = ','
530 533 if old_separator in raw_value:
531 534 # If we get a comma separated list, pass it to our own function.
532 535 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
533 536 else:
534 537 # Otherwise we assume it uses pyramids space/newline separation.
535 538 settings[name] = aslist(raw_value)
536 539
537 540
538 541 def _string_setting(settings, name, default, lower=True):
539 542 value = settings.get(name, default)
540 543 if lower:
541 544 value = value.lower()
542 545 settings[name] = value
General Comments 0
You need to be logged in to leave comments. Login now