OpenStack Dashboard (Horizon)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # Copyright 2012 United States Government as represented by the
  2. # Administrator of the National Aeronautics and Space Administration.
  3. # All Rights Reserved.
  4. #
  5. # Copyright 2012 Nebula, Inc.
  6. #
  7. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  8. # not use this file except in compliance with the License. You may obtain
  9. # a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  15. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  16. # License for the specific language governing permissions and limitations
  17. # under the License.
  18. """
  19. Middleware provided and used by Horizon.
  20. """
  21. import json
  22. import logging
  23. from django.conf import settings
  24. from django.contrib.auth import REDIRECT_FIELD_NAME
  25. from django.contrib.auth.views import redirect_to_login
  26. from django.contrib import messages as django_messages
  27. from django import http
  28. from django import shortcuts
  29. from django.utils.encoding import iri_to_uri
  30. from django.utils import timezone
  31. from horizon import exceptions
  32. from horizon.utils import functions as utils
  33. LOG = logging.getLogger(__name__)
  34. class HorizonMiddleware(object):
  35. """The main Horizon middleware class. Required for use of Horizon."""
  36. logout_reason = None
  37. def __init__(self, get_response):
  38. self.get_response = get_response
  39. def __call__(self, request):
  40. self.process_request(request)
  41. response = self.get_response(request)
  42. response = self.process_response(request, response)
  43. return response
  44. def process_request(self, request):
  45. """Adds data necessary for Horizon to function to the request."""
  46. request.horizon = {'dashboard': None,
  47. 'panel': None,
  48. 'async_messages': []}
  49. if not hasattr(request, "user") or not request.user.is_authenticated:
  50. # proceed no further if the current request is already known
  51. # not to be authenticated
  52. # it is CRITICAL to perform this check as early as possible
  53. # to avoid creating too many sessions
  54. return None
  55. if request.is_ajax():
  56. # if the request is Ajax we do not want to proceed, as clients can
  57. # 1) create pages with constant polling, which can create race
  58. # conditions when a page navigation occurs
  59. # 2) might leave a user seemingly left logged in forever
  60. # 3) thrashes db backed session engines with tons of changes
  61. return None
  62. # If we use cookie-based sessions, check that the cookie size does not
  63. # reach the max size accepted by common web browsers.
  64. if (
  65. settings.SESSION_ENGINE ==
  66. 'django.contrib.sessions.backends.signed_cookies'
  67. ):
  68. max_cookie_size = getattr(
  69. settings, 'SESSION_COOKIE_MAX_SIZE', None)
  70. session_cookie_name = getattr(
  71. settings, 'SESSION_COOKIE_NAME', None)
  72. session_key = request.COOKIES.get(session_cookie_name)
  73. if max_cookie_size is not None and session_key is not None:
  74. cookie_size = sum((
  75. len(key) + len(value)
  76. for key, value in request.COOKIES.items()
  77. ))
  78. if cookie_size >= max_cookie_size:
  79. LOG.error(
  80. 'Total Cookie size for user_id: %(user_id)s is '
  81. '%(cookie_size)sB >= %(max_cookie_size)sB. '
  82. 'You need to configure file-based or database-backed '
  83. 'sessions instead of cookie-based sessions: '
  84. 'http://docs.openstack.org/developer/horizon/topics/'
  85. 'deployment.html#session-storage',
  86. {
  87. 'user_id': request.session.get(
  88. 'user_id', 'Unknown'),
  89. 'cookie_size': cookie_size,
  90. 'max_cookie_size': max_cookie_size,
  91. }
  92. )
  93. tz = utils.get_timezone(request)
  94. if tz:
  95. timezone.activate(tz)
  96. def process_exception(self, request, exception):
  97. """Catches internal Horizon exception classes.
  98. Exception classes such as NotAuthorized, NotFound and Http302
  99. are caught and handles them gracefully.
  100. """
  101. if isinstance(exception, (exceptions.NotAuthorized,
  102. exceptions.NotAuthenticated)):
  103. auth_url = settings.LOGIN_URL
  104. next_url = iri_to_uri(request.get_full_path())
  105. if next_url != auth_url:
  106. field_name = REDIRECT_FIELD_NAME
  107. else:
  108. field_name = None
  109. login_url = request.build_absolute_uri(auth_url)
  110. response = redirect_to_login(next_url, login_url=login_url,
  111. redirect_field_name=field_name)
  112. if isinstance(exception, exceptions.NotAuthorized):
  113. response.delete_cookie('messages')
  114. return shortcuts.render(request, 'not_authorized.html',
  115. status=403)
  116. if request.is_ajax():
  117. response_401 = http.HttpResponse(status=401)
  118. response_401['X-Horizon-Location'] = response['location']
  119. return response_401
  120. return response
  121. # If an internal "NotFound" error gets this far, return a real 404.
  122. if isinstance(exception, exceptions.NotFound):
  123. raise http.Http404(exception)
  124. if isinstance(exception, exceptions.Http302):
  125. # TODO(gabriel): Find a way to display an appropriate message to
  126. # the user *on* the login form...
  127. return shortcuts.redirect(exception.location)
  128. @staticmethod
  129. def copy_headers(src, dst, headers):
  130. for header in headers:
  131. dst[header] = src[header]
  132. def process_response(self, request, response):
  133. """Convert HttpResponseRedirect to HttpResponse if request is via ajax.
  134. This is to allow ajax request to redirect url.
  135. """
  136. if request.is_ajax() and hasattr(request, 'horizon'):
  137. queued_msgs = request.horizon['async_messages']
  138. if type(response) == http.HttpResponseRedirect:
  139. # Drop our messages back into the session as per usual so they
  140. # don't disappear during the redirect. Not that we explicitly
  141. # use django's messages methods here.
  142. for tag, message, extra_tags in queued_msgs:
  143. getattr(django_messages, tag)(request, message, extra_tags)
  144. if response['location'].startswith(settings.LOGOUT_URL):
  145. redirect_response = http.HttpResponse(status=401)
  146. # This header is used for handling the logout in JS
  147. redirect_response['logout'] = True
  148. if self.logout_reason is not None:
  149. utils.add_logout_reason(
  150. request, redirect_response, self.logout_reason,
  151. 'error')
  152. else:
  153. redirect_response = http.HttpResponse()
  154. # Use a set while checking if we want a cookie's attributes
  155. # copied
  156. cookie_keys = {'max_age', 'expires', 'path', 'domain',
  157. 'secure', 'httponly', 'logout_reason'}
  158. # Copy cookies from HttpResponseRedirect towards HttpResponse
  159. for cookie_name, cookie in response.cookies.items():
  160. cookie_kwargs = dict((
  161. (key, value) for key, value in cookie.items()
  162. if key in cookie_keys and value
  163. ))
  164. redirect_response.set_cookie(
  165. cookie_name, cookie.value, **cookie_kwargs)
  166. redirect_response['X-Horizon-Location'] = response['location']
  167. upload_url_key = 'X-File-Upload-URL'
  168. if upload_url_key in response:
  169. self.copy_headers(response, redirect_response,
  170. (upload_url_key, 'X-Auth-Token'))
  171. return redirect_response
  172. if queued_msgs:
  173. # TODO(gabriel): When we have an async connection to the
  174. # client (e.g. websockets) this should be pushed to the
  175. # socket queue rather than being sent via a header.
  176. # The header method has notable drawbacks (length limits,
  177. # etc.) and is not meant as a long-term solution.
  178. response['X-Horizon-Messages'] = json.dumps(queued_msgs)
  179. return response