Merge "Make sure Horizon is treating passwords securely."

This commit is contained in:
Jenkins 2012-05-26 22:24:20 +00:00 committed by Gerrit Code Review
commit 1a31f36d30
8 changed files with 110 additions and 5 deletions

View File

@ -5,6 +5,34 @@ Deploying Horizon
This guide aims to cover some common questions, concerns and pitfalls you This guide aims to cover some common questions, concerns and pitfalls you
may encounter when deploying Horizon in a production environment. may encounter when deploying Horizon in a production environment.
Logging
=======
Logging is an important concern for prouction deployments, and the intricacies
of good logging configuration go far beyond what can be covered here. However
there are a few points worth noting about the logging included with Horizon,
how to customize it, and where other components may take over:
* Horizon's logging uses Django's logging configuration mechanism, which
can be customized in your ``local_settings.py`` file through the
``LOGGING`` dictionary.
* Horizon's default logging example sets the log level to ``"INFO"``, which is
a reasonable choice for production deployments. For development, however,
you may want to change the log level to ``"DEBUG"``.
* Horizon also uses a number of 3rd-party clients which log separately. The
log level for these can still be controlled through Horizon's ``LOGGING``
config, however behaviors may vary beyond Horizon's control.
.. warning::
At this time there is `a known bug in python-keystoneclient`_ where it will
log the complete request body of any request sent to Keystone through it
(including logging passwords in plain text) when the log level is set to
``"DEBUG"``. If this behavior is not desired, make sure your log level is
``"INFO"`` or higher.
.. _a known bug in python-keystoneclient: https://bugs.launchpad.net/keystone/+bug/1004114
Session Storage Session Storage
=============== ===============

View File

@ -22,8 +22,9 @@ import logging
from django import shortcuts from django import shortcuts
from django.contrib import messages from django.contrib import messages
from django.utils.translation import force_unicode, ugettext_lazy as _
from django.forms import ValidationError from django.forms import ValidationError
from django.utils.translation import force_unicode, ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables
from horizon import api from horizon import api
from horizon import exceptions from horizon import exceptions
@ -72,6 +73,9 @@ class CreateUserForm(BaseUserForm):
widget=forms.PasswordInput(render_value=False)) widget=forms.PasswordInput(render_value=False))
tenant_id = forms.ChoiceField(label=_("Primary Project")) tenant_id = forms.ChoiceField(label=_("Primary Project"))
# We have to protect the entire "data" dict because it contains the
# password and confirm_password strings.
@sensitive_variables('data')
def handle(self, request, data): def handle(self, request, data):
try: try:
LOG.info('Creating user with name "%s"' % data['name']) LOG.info('Creating user with name "%s"' % data['name'])
@ -123,6 +127,9 @@ class UpdateUserForm(BaseUserForm):
for field in ('name', 'email', 'password', 'confirm_password'): for field in ('name', 'email', 'password', 'confirm_password'):
self.fields.pop(field) self.fields.pop(field)
# We have to protect the entire "data" dict because it contains the
# password and confirm_password strings.
@sensitive_variables('data', 'password')
def handle(self, request, data): def handle(self, request, data):
failed, succeeded = [], [] failed, succeeded = [], []
user_is_editable = api.keystone_can_edit_user() user_is_editable = api.keystone_can_edit_user()

View File

@ -21,7 +21,9 @@
import logging import logging
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from horizon import api from horizon import api
from horizon import exceptions from horizon import exceptions
@ -53,6 +55,11 @@ class UpdateView(forms.ModalFormView):
template_name = 'syspanel/users/update.html' template_name = 'syspanel/users/update.html'
context_object_name = 'user' context_object_name = 'user'
@method_decorator(sensitive_post_parameters('password',
'confirm_password'))
def dispatch(self, *args, **kwargs):
return super(UpdateView, self).dispatch(*args, **kwargs)
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
user_id = kwargs['user_id'] user_id = kwargs['user_id']
try: try:
@ -73,3 +80,8 @@ class UpdateView(forms.ModalFormView):
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = CreateUserForm form_class = CreateUserForm
template_name = 'syspanel/users/create.html' template_name = 'syspanel/users/create.html'
@method_decorator(sensitive_post_parameters('password',
'confirm_password'))
def dispatch(self, *args, **kwargs):
return super(CreateView, self).dispatch(*args, **kwargs)

View File

@ -24,8 +24,11 @@ import sys
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest
from django.utils import termcolors from django.utils import termcolors
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.debug import SafeExceptionReporterFilter, CLEANSED_SUBSTITUTE
from cloudfiles import errors as swiftclient from cloudfiles import errors as swiftclient
from glanceclient.common import exceptions as glanceclient from glanceclient.common import exceptions as glanceclient
from keystoneclient import exceptions as keystoneclient from keystoneclient import exceptions as keystoneclient
@ -36,6 +39,51 @@ LOG = logging.getLogger(__name__)
PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE] PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE]
class HorizonReporterFilter(SafeExceptionReporterFilter):
""" Error report filter that's always active, even in DEBUG mode. """
def is_active(self, request):
return True
# TODO(gabriel): When Django bug #18379 is fixed, this whole method
# can be removed: https://code.djangoproject.com/ticket/18379
def get_traceback_frame_variables(self, request, tb_frame):
"""
Replaces the values of variables marked as sensitive with
stars (*********).
"""
func_name = tb_frame.f_code.co_name
func = tb_frame.f_globals.get(func_name)
# Methods won't be in the global namespace, func could be None here...
if func is None and "self" in tb_frame.f_locals:
func = getattr(tb_frame.f_locals.get('self'), func_name, None)
sensitive_variables = getattr(func, 'sensitive_variables', [])
cleansed = []
if self.is_active(request) and sensitive_variables:
if sensitive_variables == '__ALL__':
# Cleanse all variables
for name, value in tb_frame.f_locals.items():
cleansed.append((name, CLEANSED_SUBSTITUTE))
return cleansed
else:
# Cleanse specified variables
for name, value in tb_frame.f_locals.items():
if name in sensitive_variables:
value = CLEANSED_SUBSTITUTE
elif isinstance(value, HttpRequest):
# Cleanse the request's POST parameters.
value = self.get_request_repr(value)
cleansed.append((name, value))
return cleansed
else:
# Cleanse only the request if it's one of the frame variables.
for name, value in tb_frame.f_locals.items():
if isinstance(value, HttpRequest):
# Cleanse the request's POST parameters.
value = self.get_request_repr(value)
cleansed.append((name, value))
return cleansed
class HorizonException(Exception): class HorizonException(Exception):
""" Base exception class for distinguishing our own exception classes. """ """ Base exception class for distinguishing our own exception classes. """
pass pass

View File

@ -23,7 +23,9 @@ import logging
from django import shortcuts from django import shortcuts
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_post_parameters
import horizon import horizon
from horizon import api from horizon import api
@ -50,6 +52,10 @@ class LoginView(forms.ModalFormView):
form_class = Login form_class = Login
template_name = "horizon/auth/login.html" template_name = "horizon/auth/login.html"
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super(LoginView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(LoginView, self).get_context_data(**kwargs) context = super(LoginView, self).get_context_data(**kwargs)
redirect_to = self.request.REQUEST.get(REDIRECT_FIELD_NAME, "") redirect_to = self.request.REQUEST.get(REDIRECT_FIELD_NAME, "")
@ -67,6 +73,7 @@ class LoginView(forms.ModalFormView):
return initial return initial
@sensitive_post_parameters("password")
def switch_tenants(request, tenant_id): def switch_tenants(request, tenant_id):
""" """
Swaps a user from one tenant to another using the unscoped token from Swaps a user from one tenant to another using the unscoped token from

View File

@ -29,6 +29,8 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_variables
from keystoneclient import exceptions as keystone_exceptions from keystoneclient import exceptions as keystone_exceptions
from horizon import api from horizon import api
@ -76,6 +78,7 @@ class Login(forms.SelfHandlingForm):
self.fields['region'].initial = default_region[0] self.fields['region'].initial = default_region[0]
self.fields['region'].widget = forms.widgets.HiddenInput() self.fields['region'].widget = forms.widgets.HiddenInput()
@sensitive_variables("data")
def handle(self, request, data): def handle(self, request, data):
if 'user_name' in request.session: if 'user_name' in request.session:
if request.session['user_name'] != data['username']: if request.session['user_name'] != data['username']:

View File

@ -127,6 +127,8 @@ USE_I18N = True
OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'
DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter'
try: try:
from local.local_settings import * from local.local_settings import *
except ImportError: except ImportError:

View File

@ -25,7 +25,7 @@ from django import shortcuts
from django.views.decorators import vary from django.views.decorators import vary
import horizon import horizon
from horizon.views import auth as auth_views from horizon.views import auth_forms
def qunit_tests(request): def qunit_tests(request):
@ -42,8 +42,6 @@ def user_home(user):
def splash(request): def splash(request):
if request.user.is_authenticated(): if request.user.is_authenticated():
return shortcuts.redirect(user_home(request.user)) return shortcuts.redirect(user_home(request.user))
form, handled = auth_views.Login.maybe_handle(request) form = auth_forms.Login()
if handled:
return handled
request.session.clear() request.session.clear()
return shortcuts.render(request, 'splash.html', {'form': form}) return shortcuts.render(request, 'splash.html', {'form': form})