Auth refactor.

Switch to using the self-contained django_openstack_auth
package which is a proper django.contrib.auth pluggable
backend.

Notable functional improvements include:

  * Better overall security via use of standard Django
    auth code (well-vetted by security experts).
  * Token expiration checking.
  * User "enabled" attribute checking.
  * Support for full range of Django auth attributes
    such as is_anonymous, is_active, is_superuser, etc.
  * Improved hooks for RBAC/permission-based acess control.

Regarding the RBAC/permission-based access control, this
patch moves all "role" and "service"-oriented checks to
permission checks. This will make transitioning to
policy-driven checking much easier once that fully lands
in OpenStack.

Implements blueprint move-keystone-support-to-django-auth-backend

Change-Id: I4f3112af797aff8c4c5e9930c6ca33a70e45589d
This commit is contained in:
Gabriel Hurley 2012-04-13 21:46:04 -07:00
parent 3990985aa0
commit c339189b44
76 changed files with 11680 additions and 11813 deletions

View File

@ -87,9 +87,7 @@ In-depth documentation for Horizon and its APIs.
ref/workflows
ref/tables
ref/tabs
ref/users
ref/forms
ref/views
ref/middleware
ref/context_processors
ref/decorators

View File

@ -15,7 +15,7 @@ the local host (127.0.0.1). If this is not the case change the
``openstack_dashboard/local`` folder, to the actual IP address of the
OpenStack end-point Horizon should use.
To start the Horizon development server use the Django ``manage.py`` utility
To start the Horizon development server use the Django ``manage.py`` utility
with the context of the virtual environment::
> tools/with_venv.sh ./manage.py runserver
@ -37,7 +37,7 @@ or to the IP and port the server is listening.
The minimum required set of OpenStack services running includes the
following:
* Nova (compute, api, scheduler, network, *and* volume services)
* Nova (compute, api, scheduler, and network)
* Glance
* Keystone
@ -162,7 +162,7 @@ process::
panels = ('overview', 'services', 'instances', 'flavors', 'images',
'tenants', 'users', 'quotas',)
default_panel = 'overview'
roles = ('admin',) # Provides RBAC at the dashboard-level
permissions = ('openstack.roles.admin',)
...
@ -182,7 +182,7 @@ you register it in a ``panels.py`` file like so::
class Images(horizon.Panel):
name = "Images"
slug = 'images'
roles = ('admin', 'my_other_role',) # Fine-grained RBAC per-panel
permissions = ('openstack.roles.admin', 'my.other.permission',)
# You could also register your panel with another application's dashboard

View File

@ -2,16 +2,10 @@
Horizon Forms
=============
Horizon ships with a number of form classes, some generic and some specific.
Horizon ships with a number of generic form classes.
Generic Forms
=============
.. automodule:: horizon.forms
:members:
Auth Forms
==========
.. automodule:: horizon.views.auth_forms
:members:

View File

@ -1,6 +0,0 @@
=================
Horizon User APIs
=================
.. automodule:: horizon.users
:members:

View File

@ -1,12 +0,0 @@
=============
Horizon Views
=============
Horizon ships with a number of pre-built views which are used within
Horizon and can also be reused in your applications.
Auth
====
.. automodule:: horizon.views.auth
:members:

View File

@ -86,8 +86,8 @@ defining nothing more than a name and a slug::
name = _("Visualizations")
slug = "visualizations"
In practice, a dashboard class will usually contain more information, such
as a list of panels, which panel is the default, and any roles required to
In practice, a dashboard class will usually contain more information, such as a
list of panels, which panel is the default, and any permissions required to
access this dashboard::
class VizDash(horizon.Dashboard):
@ -95,7 +95,7 @@ access this dashboard::
slug = "visualizations"
panels = ('flocking',)
default_panel = 'flocking'
roles = ('admin',)
permissions = ('openstack.roles.admin',)
Building from that previous example we may also want to define a grouping of
panels which share a common theme and have a sub-heading in the navigation::
@ -111,7 +111,7 @@ panels which share a common theme and have a sub-heading in the navigation::
slug = "visualizations"
panels = (InstanceVisualizations,)
default_panel = 'flocking'
roles = ('admin',)
permissions = ('openstack.roles.admin',)
The ``PanelGroup`` can be added to the dashboard class' ``panels`` list
just like the slug of the panel can.

View File

@ -37,9 +37,9 @@ LOG = logging.getLogger(__name__)
def glanceclient(request):
o = urlparse.urlparse(url_for(request, 'image'))
url = "://".join((o.scheme, o.netloc))
LOG.debug('glanceclient connection created using token "%s" and url "%s"' %
(request.user.token, url))
return glance_client.Client(endpoint=url, token=request.user.token)
LOG.debug('glanceclient connection created using token "%s" and url "%s"'
% (request.user.token.id, url))
return glance_client.Client(endpoint=url, token=request.user.token.id)
def image_delete(request, image_id):

View File

@ -29,6 +29,8 @@ from keystoneclient import service_catalog
from keystoneclient.v2_0 import client as keystone_client
from keystoneclient.v2_0 import tokens
from openstack_auth.backend import KEYSTONE_CLIENT_ATTR
from horizon.api import base
from horizon import exceptions
@ -69,9 +71,7 @@ def _get_endpoint_url(request, endpoint_type, catalog=None):
getattr(settings, 'OPENSTACK_KEYSTONE_URL'))
def keystoneclient(request, username=None, password=None, tenant_id=None,
token_id=None, endpoint=None, endpoint_type=None,
admin=False):
def keystoneclient(request, admin=False):
"""Returns a client connected to the Keystone backend.
Several forms of authentication are supported:
@ -95,40 +95,27 @@ def keystoneclient(request, username=None, password=None, tenant_id=None,
"""
user = request.user
if admin:
if not user.is_admin():
if not user.is_superuser:
raise exceptions.NotAuthorized
endpoint_type = 'adminURL'
else:
endpoint_type = endpoint_type or getattr(settings,
'OPENSTACK_ENDPOINT_TYPE',
'internalURL')
endpoint_type = getattr(settings,
'OPENSTACK_ENDPOINT_TYPE',
'internalURL')
# Take care of client connection caching/fetching a new client.
# Admin vs. non-admin clients are cached separately for token matching.
cache_attr = "_keystone_admin" if admin else "_keystone"
if hasattr(request, cache_attr) and (not token_id
or getattr(request, cache_attr).auth_token == token_id):
LOG.debug("Using cached client for token: %s" % user.token)
cache_attr = "_keystoneclient_admin" if admin else KEYSTONE_CLIENT_ATTR
if hasattr(request, cache_attr) and (not user.token.id
or getattr(request, cache_attr).auth_token == user.token.id):
LOG.debug("Using cached client for token: %s" % user.token.id)
conn = getattr(request, cache_attr)
else:
endpoint_lookup = _get_endpoint_url(request, endpoint_type)
auth_url = endpoint or endpoint_lookup
LOG.debug("Creating a new keystoneclient connection to %s." % auth_url)
conn = keystone_client.Client(username=username or user.username,
password=password,
tenant_id=tenant_id or user.tenant_id,
token=token_id or user.token,
auth_url=auth_url,
endpoint = _get_endpoint_url(request, endpoint_type)
LOG.debug("Creating a new keystoneclient connection to %s." % endpoint)
conn = keystone_client.Client(token=user.token.id,
endpoint=endpoint)
setattr(request, cache_attr, conn)
# Fetch the correct endpoint if we've re-scoped the token.
catalog = getattr(conn, 'service_catalog', None)
if catalog and "serviceCatalog" in catalog.catalog.keys():
catalog = catalog.catalog['serviceCatalog']
endpoint = _get_endpoint_url(request, endpoint_type, catalog)
conn.management_url = endpoint
return conn
@ -161,35 +148,6 @@ def tenant_update(request, tenant_id, tenant_name, description, enabled):
enabled)
def tenant_list_for_token(request, token, endpoint_type=None):
endpoint_type = endpoint_type or getattr(settings,
'OPENSTACK_ENDPOINT_TYPE',
'internalURL')
c = keystoneclient(request,
token_id=token,
endpoint=_get_endpoint_url(request, endpoint_type),
endpoint_type=endpoint_type)
return c.tenants.list()
def token_create(request, tenant, username, password):
'''
Creates a token using the username and password provided. If tenant
is provided it will retrieve a scoped token and the service catalog for
the given tenant. Otherwise it will return an unscoped token and without
a service catalog.
'''
c = keystoneclient(request,
username=username,
password=password,
tenant_id=tenant,
endpoint=_get_endpoint_url(request, 'internalURL'))
token = c.tokens.authenticate(username=username,
password=password,
tenant_id=tenant)
return token
def token_create_scoped(request, tenant, token):
'''
Creates a scoped token using the tenant id and unscoped token; retrieves
@ -197,15 +155,12 @@ def token_create_scoped(request, tenant, token):
'''
if hasattr(request, '_keystone'):
del request._keystone
c = keystoneclient(request,
tenant_id=tenant,
token_id=token,
endpoint=_get_endpoint_url(request, 'internalURL'))
c = keystoneclient(request)
raw_token = c.tokens.authenticate(tenant_id=tenant,
token=token,
return_raw=True)
c.service_catalog = service_catalog.ServiceCatalog(raw_token)
if request.user.is_admin():
if request.user.is_superuser:
c.management_url = c.service_catalog.url_for(service_type='identity',
endpoint_type='adminURL')
else:

View File

@ -192,24 +192,24 @@ class SecurityGroupRule(APIResourceWrapper):
def novaclient(request):
LOG.debug('novaclient connection created using token "%s" and url "%s"' %
(request.user.token, url_for(request, 'compute')))
(request.user.token.id, url_for(request, 'compute')))
c = nova_client.Client(request.user.username,
request.user.token,
request.user.token.id,
project_id=request.user.tenant_id,
auth_url=url_for(request, 'compute'))
c.client.auth_token = request.user.token
c.client.auth_token = request.user.token.id
c.client.management_url = url_for(request, 'compute')
return c
def cinderclient(request):
LOG.debug('cinderclient connection created using token "%s" and url "%s"' %
(request.user.token, url_for(request, 'volume')))
(request.user.token.id, url_for(request, 'volume')))
c = nova_client.Client(request.user.username,
request.user.token,
request.user.token.id,
project_id=request.user.tenant_id,
auth_url=url_for(request, 'volume'))
c.client.auth_token = request.user.token
c.client.auth_token = request.user.token.id
c.client.management_url = url_for(request, 'volume')
return c

View File

@ -44,8 +44,8 @@ class SwiftAuthentication(object):
def swift_api(request):
endpoint = url_for(request, 'object-store')
LOG.debug('Swift connection created using token "%s" and url "%s"'
% (request.session['token'], endpoint))
auth = SwiftAuthentication(endpoint, request.session['token'])
% (request.user.token.id, endpoint))
auth = SwiftAuthentication(endpoint, request.user.token.id)
return cloudfiles.get_connection(auth=auth)

View File

@ -39,8 +39,7 @@ from django.utils.module_loading import module_has_submodule
from django.utils.translation import ugettext as _
from horizon import loaders
from horizon.decorators import (require_auth, require_roles,
require_services, _current_component)
from horizon.decorators import require_auth, require_perms, _current_component
LOG = logging.getLogger(__name__)
@ -173,7 +172,7 @@ class Panel(HorizonComponent):
All Horizon dashboard panels should extend from this class. It provides
the appropriate hooks for automatically constructing URLconfs, and
providing role-based access control.
providing permission-based access control.
.. attribute:: name
@ -186,18 +185,13 @@ class Panel(HorizonComponent):
A unique "short name" for the panel. The slug is used as
a component of the URL path for the panel. Default: ``''``.
.. attribute:: roles
.. attribute:: permissions
A list of role names, all of which a user must possess in order
A list of permission names, all of which a user must possess in order
to access any view associated with this panel. This attribute
is combined cumulatively with any roles required on the
is combined cumulatively with any permissions required on the
``Dashboard`` class with which it is registered.
.. attribute:: services
A list of service names, all of which must be in the service catalog
in order for this panel to be available.
.. attribute:: urls
Path to a URLconf of views for this panel using dotted Python
@ -249,10 +243,8 @@ class Panel(HorizonComponent):
urlpatterns = self._get_default_urlpatterns()
# Apply access controls to all views in the patterns
roles = getattr(self, 'roles', [])
services = getattr(self, 'services', [])
_decorate_urlconf(urlpatterns, require_roles, roles)
_decorate_urlconf(urlpatterns, require_services, services)
permissions = getattr(self, 'permissions', [])
_decorate_urlconf(urlpatterns, require_perms, permissions)
_decorate_urlconf(urlpatterns, _current_component, panel=self)
# Return the three arguments to django.conf.urls.defaults.include
@ -307,8 +299,8 @@ class Dashboard(Registry, HorizonComponent):
All Horizon dashboards should extend from this base class. It provides the
appropriate hooks for automatic discovery of :class:`~horizon.Panel`
modules, automatically constructing URLconfs, and providing role-based
access control.
modules, automatically constructing URLconfs, and providing
permission-based access control.
.. attribute:: name
@ -360,18 +352,13 @@ class Dashboard(Registry, HorizonComponent):
for this dashboard, that's the panel that is displayed.
Default: ``None``.
.. attribute:: roles
.. attribute:: permissions
A list of role names, all of which a user must possess in order
A list of permission names, all of which a user must possess in order
to access any panel registered with this dashboard. This attribute
is combined cumulatively with any roles required on individual
is combined cumulatively with any permissions required on individual
:class:`~horizon.Panel` classes.
.. attribute:: services
A list of service names, all of which must be in the service catalog
in order for this dashboard to be available.
.. attribute:: urls
Optional path to a URLconf of additional views for this dashboard
@ -491,10 +478,8 @@ class Dashboard(Registry, HorizonComponent):
if not self.public:
_decorate_urlconf(urlpatterns, require_auth)
# Apply access controls to all views in the patterns
roles = getattr(self, 'roles', [])
services = getattr(self, 'services', [])
_decorate_urlconf(urlpatterns, require_roles, roles)
_decorate_urlconf(urlpatterns, require_services, services)
permissions = getattr(self, 'permissions', [])
_decorate_urlconf(urlpatterns, require_perms, permissions)
_decorate_urlconf(urlpatterns, _current_component, dashboard=self)
# Return the three arguments to django.conf.urls.defaults.include

View File

@ -27,6 +27,6 @@ from horizon.dashboards.nova import dashboard
class Containers(horizon.Panel):
name = _("Containers")
slug = 'containers'
services = ('object-store',)
permissions = ('openstack.services.object-store',)
dashboard.Nova.register(Containers)

View File

@ -45,7 +45,7 @@ class SelectProjectUserAction(workflows.Action):
class Meta:
name = _("Project & User")
roles = ("admin",)
permissions = ("openstack.roles.admin",)
help_text = _("Admin users may optionally select the project and "
"user for whom the instance should be created.")
@ -82,7 +82,7 @@ class VolumeOptionsAction(workflows.Action):
class Meta:
name = _("Volume Options")
services = ('volume',)
permissions = ('openstack.services.volume',)
help_text_template = ("nova/instances/"
"_launch_volumes_help.html")
@ -392,8 +392,8 @@ class LaunchInstance(workflows.Workflow):
slug = "launch_instance"
name = _("Launch Instance")
finalize_button_name = _("Launch")
success_message = _('Launched %s named "%s".')
failure_message = _('Unable to launch %s named "%s".')
success_message = _('Launched %(count)s named "%(name)s".')
failure_message = _('Unable to launch %(count)s named "%(name)s".')
success_url = "horizon:nova:instances:index"
default_steps = (SelectProjectUser,
SetInstanceDetails,
@ -405,9 +405,10 @@ class LaunchInstance(workflows.Workflow):
name = self.context.get('name', 'unknown instance')
count = self.context.get('count', 1)
if int(count) > 1:
return message % (_("%s instances") % count, name)
return message % {"count": _("%s instances") % count,
"name": name}
else:
return message % (_("instance"), name)
return message % {"count": _("instance"), "name": name}
def handle(self, request, context):
custom_script = context.get('customization_script', '')

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% block content %}
{% if request.user.is_admin %}
{% if request.user.is_superuser %}
<div class="warning">
<div class="warning-text">
<h3 class="alert-error">

View File

@ -37,11 +37,12 @@ class CreateForm(forms.SelfHandlingForm):
data['size'] = int(data['size'])
if usages['gigabytes']['available'] < data['size']:
error_message = _('A volume of %iGB cannot be created as you'
' only have %iGB of your quota available.'
% (data['size'],
usages['gigabytes']['available'],))
raise ValidationError(error_message)
error_message = _('A volume of %(req)iGB cannot be created as '
'you only have %(avail)iGB of your quota '
'available.')
params = {'req': data['size'],
'avail': usages['gigabytes']['available']}
raise ValidationError(error_message % params)
elif usages['volumes']['available'] <= 0:
error_message = _('You are already using all of your available'
' volumes.')

View File

@ -23,7 +23,7 @@ from horizon.dashboards.nova import dashboard
class Volumes(horizon.Panel):
name = _("Volumes")
slug = 'volumes'
services = ('volume',)
permissions = ('openstack.services.volume',)
dashboard.Nova.register(Volumes)

View File

@ -72,7 +72,7 @@ class DownloadX509Credentials(forms.SelfHandlingForm):
# the token to tenant before making the call.
api.keystone.token_create_scoped(request,
data.get('tenant'),
request.user.token)
request.user.token.id)
credentials = api.nova.get_x509_credentials(request)
cacert = api.nova.get_x509_root_certificate(request)
keys = find_or_create_access_keys(request, data.get('tenant'))

View File

@ -27,6 +27,6 @@
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Download RC File" %}" />
<button class="btn btn-primary pull-right" type="submit">{% trans "Download RC File" %}"</button>
{% if hide %}<a href="{% url horizon:settings:project:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
{% endblock %}

View File

@ -31,7 +31,7 @@ class Syspanel(horizon.Dashboard):
slug = "syspanel"
panels = (SystemPanels,)
default_panel = 'overview'
roles = ('admin',)
permissions = ('openstack.roles.admin',)
horizon.register(Syspanel)

View File

@ -6,15 +6,8 @@ from horizon import api
from horizon import test
class FlavorsTests(test.TestCase):
class FlavorsTests(test.BaseAdminViewTests):
def test_create_new_flavor_when_none_exist(self):
# Set admin role
self.setActiveUser(token=self.token.id,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=self.request.user.service_catalog,
roles=[{'name': 'admin'}])
self.mox.StubOutWithMock(api, 'flavor_list')
# no pre-existing flavors
api.flavor_list(IsA(http.HttpRequest)).AndReturn([])

View File

@ -27,7 +27,7 @@ from horizon.dashboards.syspanel import dashboard
class Instances(horizon.Panel):
name = _("Instances")
slug = 'instances'
roles = ('admin',)
permissions = ('openstack.roles.admin',)
dashboard.Syspanel.register(Instances)

View File

@ -27,7 +27,7 @@ from horizon.dashboards.syspanel import dashboard
class Overview(horizon.Panel):
name = _("Overview")
slug = 'overview'
roles = ('admin',)
permissions = ('openstack.roles.admin',)
dashboard.Syspanel.register(Overview)

View File

@ -8,7 +8,7 @@ from horizon.dashboards.syspanel import dashboard
class Volumes(horizon.Panel):
name = _("Volumes")
slug = "volumes"
services = ('volume',)
permissions = ('openstack.services.volume',)
dashboard.Syspanel.register(Volumes)

View File

@ -26,7 +26,7 @@ import functools
from django.utils.decorators import available_attrs
from django.utils.translation import ugettext as _
from horizon.exceptions import NotAuthorized, NotFound, NotAuthenticated
from horizon.exceptions import NotAuthorized, NotAuthenticated
def _current_component(view_func, dashboard=None, panel=None):
@ -57,85 +57,38 @@ def require_auth(view_func):
return dec
def require_roles(view_func, required):
""" Enforces role-based access controls.
def require_perms(view_func, required):
""" Enforces permission-based access controls.
:param list required: A tuple of role names, all of which the request user
must possess in order access the decorated view.
:param list required: A tuple of permission names, all of which the request
user must possess in order access the decorated view.
Example usage::
from horizon.decorators import require_roles
from horizon.decorators import require_perms
@require_roles(['admin', 'member'])
@require_perms(['foo.admin', 'foo.member'])
def my_view(request):
...
Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the
requirements are not met.
"""
# We only need to check each role once for a view, so we'll use a set
current_roles = getattr(view_func, '_required_roles', set([]))
view_func._required_roles = current_roles | set(required)
# We only need to check each permission once for a view, so we'll use a set
current_perms = getattr(view_func, '_required_perms', set([]))
view_func._required_perms = current_perms | set(required)
@functools.wraps(view_func, assigned=available_attrs(view_func))
def dec(request, *args, **kwargs):
if request.user.is_authenticated():
roles = set([role['name'].lower() for role in request.user.roles])
# set operator <= tests that all members of set 1 are in set 2
if view_func._required_roles <= set(roles):
if request.user.has_perms(view_func._required_perms):
return view_func(request, *args, **kwargs)
raise NotAuthorized(_("You are not authorized to access %s")
% request.path)
# If we don't have any roles, just return the original view.
# If we don't have any permissions, just return the original view.
if required:
return dec
else:
return view_func
def require_services(view_func, required):
""" Enforces service-based access controls.
:param list required: A tuple of service type names, all of which the
must be present in the service catalog in order
access the decorated view.
Example usage::
from horizon.decorators import require_services
@require_services(['object-store'])
def my_swift_view(request):
...
Raises a :exc:`~horizon.exceptions.NotFound` exception if the
requirements are not met.
"""
# We only need to check each service once for a view, so we'll use a set
current_services = getattr(view_func, '_required_services', set([]))
view_func._required_services = current_services | set(required)
@functools.wraps(view_func, assigned=available_attrs(view_func))
def dec(request, *args, **kwargs):
if request.user.is_authenticated():
services = set([service['type'] for service in
request.user.service_catalog])
# set operator <= tests that all members of set 1 are in set 2
if view_func._required_services <= set(services):
return view_func(request, *args, **kwargs)
raise NotFound(_("The services for this view are not available."))
# If we don't have any services, just return the original view.
if required:
return dec
else:
return view_func
def enforce_admin_access(view_func):
""" Marks a view as requiring the ``"admin"`` role for access. """
return require_roles(view_func, ('admin',))

View File

@ -116,8 +116,8 @@ class Http302(HorizonException):
class NotAuthorized(HorizonException):
"""
Raised whenever a user attempts to access a resource which they do not
have role-based access to (such as when failing the
:func:`~horizon.decorators.require_roles` decorator).
have permission-based access to (such as when failing the
:func:`~horizon.decorators.require_perms` decorator).
The included :class:`~horizon.middleware.HorizonMiddleware` catches
``NotAuthorized`` and handles it gracefully by displaying an error

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,6 @@ from django.utils import timezone
from django.utils.encoding import iri_to_uri
from horizon import exceptions
from horizon import users
from horizon.openstack.common import jsonutils
@ -43,28 +42,12 @@ class HorizonMiddleware(object):
""" The main Horizon middleware class. Required for use of Horizon. """
def process_request(self, request):
""" Adds data necessary for Horizon to function to the request.
Adds the current "active" :class:`~horizon.Dashboard` and
:class:`~horizon.Panel` to ``request.horizon``.
Adds a :class:`~horizon.users.User` object to ``request.user``.
"""
""" Adds data necessary for Horizon to function to the request. """
# Activate timezone handling
tz = request.session.get('django_timezone')
if tz:
timezone.activate(tz)
# A quick and dirty way to log users out
def user_logout(request):
if hasattr(request, '_cached_user'):
del request._cached_user
# Use flush instead of clear, so we rotate session keys in
# addition to clearing all the session data
request.session.flush()
request.__class__.user_logout = user_logout
request.__class__.user = users.LazyUser()
request.horizon = {'dashboard': None,
'panel': None,
'async_messages': []}
@ -76,7 +59,7 @@ class HorizonMiddleware(object):
"""
if isinstance(exception,
(exceptions.NotAuthorized, exceptions.NotAuthenticated)):
auth_url = reverse("horizon:auth_login")
auth_url = reverse("login")
next_url = iri_to_uri(request.get_full_path())
if next_url != auth_url:
param = "?%s=%s" % (REDIRECT_FIELD_NAME, next_url)

View File

@ -22,22 +22,16 @@ from django.views.generic import TemplateView
from django.conf.urls.defaults import patterns, url, include
from django.conf import settings
from horizon.views.auth import LoginView
urlpatterns = patterns('horizon.views.auth',
url(r'home/$', 'user_home', name='user_home'),
url(r"^%s$" % settings.LOGIN_URL.lstrip('/'), LoginView.as_view(),
name='auth_login'),
url(r"^%s$" % settings.LOGOUT_URL.lstrip('/'), 'logout',
name='auth_logout'),
url(r'auth/switch/(?P<tenant_id>[^/]+)/$', 'switch_tenants',
name='auth_switch'))
urlpatterns = patterns('horizon.views',
url(r'home/$', 'user_home', name='user_home')
)
urlpatterns += patterns('',
url(r'^i18n/setlang/$', 'django.views.i18n.set_language',
name="set_language"),
url(r'^i18n/', include('django.conf.urls.i18n')))
url(r'^i18n/', include('django.conf.urls.i18n'))
)
if settings.DEBUG:
urlpatterns += patterns('',

View File

@ -7,7 +7,7 @@
<li class='divider'></li>
{% for region in regions.available %}
{% if region.name != regions.current.name %}
<li><a class="ajax-modal" href="{% url horizon:auth_login %}?region={{ region.endpoint }}">{{ region.name }}</a></li>
<li><a class="ajax-modal" href="{% url login %}?region={{ region.endpoint }}">{{ region.name }}</a></li>
{% endif %}
{% endfor %}
</ul>

View File

@ -22,7 +22,7 @@
<li class='divider'></li>
{% for tenant in authorized_tenants %}
{% if tenant.enabled and tenant.id != request.user.tenant_id %}
<li><a href="{% url horizon:auth_switch tenant.id %}">{{ tenant.name }}</a></li>
<li><a href="{% url switch_tenants tenant.id %}">{{ tenant.name }}</a></li>
{% endif %}
{% endfor %}
</ul>

View File

@ -29,24 +29,10 @@ register = template.Library()
@register.filter
def has_permissions(user, component):
"""
Checks if the given user meets the requirements for the component. This
includes both user roles and services in the service catalog.
Checks if the given user meets the permissions requirements for
the component.
"""
if hasattr(user, 'roles'):
user_roles = set([role['name'].lower() for role in user.roles])
else:
user_roles = set([])
roles_statisfied = set(getattr(component, 'roles', [])) <= user_roles
if hasattr(user, 'service_catalog'):
services = set([service['type'] for service in user.service_catalog])
else:
services = set([])
services_statisfied = set(getattr(component, 'services', [])) <= services
if roles_statisfied and services_statisfied:
return True
return False
return user.has_perms(getattr(component, 'permissions', set()))
@register.filter

View File

@ -18,26 +18,31 @@
# License for the specific language governing permissions and limitations
# under the License.
from functools import wraps
import os
import cloudfiles as swift_client
from django import http
from django import test as django_test
from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.core.handlers import wsgi
from django.test.client import RequestFactory
from functools import wraps
from glanceclient.v1 import client as glance_client
from keystoneclient.v2_0 import client as keystone_client
from novaclient.v1_1 import client as nova_client
import httplib2
import mox
from openstack_auth import utils, user
from horizon import api
from horizon import context_processors
from horizon import middleware
from horizon import users
from horizon.tests.test_data.utils import load_test_data
@ -71,12 +76,14 @@ def create_stubs(stubs_to_create={}):
class RequestFactoryWithMessages(RequestFactory):
def get(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).get(*args, **kwargs)
req.user = utils.get_user(req)
req.session = []
req._messages = default_storage(req)
return req
def post(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
req.user = utils.get_user(req)
req.session = []
req._messages = default_storage(req)
return req
@ -116,10 +123,10 @@ class TestCase(django_test.TestCase):
self._real_horizon_context_processor = context_processors.horizon
context_processors.horizon = lambda request: self.context
self._real_get_user_from_request = users.get_user_from_request
self._real_get_user = utils.get_user
tenants = self.context['authorized_tenants']
self.setActiveUser(id=self.user.id,
token=self.token.id,
token=self.token,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=self.service_catalog,
@ -128,28 +135,31 @@ class TestCase(django_test.TestCase):
self.request.session = self.client._session()
self.request.session['token'] = self.token.id
middleware.HorizonMiddleware().process_request(self.request)
AuthenticationMiddleware().process_request(self.request)
os.environ["HORIZON_TEST_RUN"] = "True"
def tearDown(self):
self.mox.UnsetStubs()
httplib2.Http._conn_request = self._real_conn_request
context_processors.horizon = self._real_horizon_context_processor
users.get_user_from_request = self._real_get_user_from_request
utils.get_user = self._real_get_user
self.mox.VerifyAll()
del os.environ["HORIZON_TEST_RUN"]
def setActiveUser(self, id=None, token=None, username=None, tenant_id=None,
service_catalog=None, tenant_name=None, roles=None,
authorized_tenants=None):
users.get_user_from_request = lambda x: \
users.User(id=id,
token=token,
user=username,
tenant_id=tenant_id,
service_catalog=service_catalog,
roles=roles,
authorized_tenants=authorized_tenants,
request=self.request)
authorized_tenants=None, enabled=True):
def get_user(request):
return user.User(id=id,
token=token,
user=username,
tenant_id=tenant_id,
service_catalog=service_catalog,
roles=roles,
enabled=enabled,
authorized_tenants=authorized_tenants,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
utils.get_user = get_user
def assertRedirectsNoFollow(self, response, expected_url):
"""
@ -264,9 +274,7 @@ class APITestCase(TestCase):
def setUp(self):
super(APITestCase, self).setUp()
def fake_keystoneclient(request, username=None, password=None,
tenant_id=None, token_id=None, endpoint=None,
admin=False):
def fake_keystoneclient(request, admin=False):
"""
Wrapper function which returns the stub keystoneclient. Only
necessary because the function takes too many arguments to

View File

@ -24,7 +24,6 @@ from keystoneclient.v2_0 import client as keystone_client
from horizon import api
from horizon import test
from horizon import users
class FakeConnection(object):
@ -35,10 +34,6 @@ class ClientConnectionTests(test.TestCase):
def setUp(self):
super(ClientConnectionTests, self).setUp()
self.mox.StubOutWithMock(keystone_client, "Client")
self.test_user = users.User(id=self.user.id,
user=self.user.name,
service_catalog=self.service_catalog)
self.request.user = self.test_user
self.internal_url = api.base.url_for(self.request,
'identity',
endpoint_type='internalURL')
@ -47,92 +42,6 @@ class ClientConnectionTests(test.TestCase):
endpoint_type='adminURL')
self.conn = FakeConnection()
def test_connect(self):
keystone_client.Client(auth_url=self.internal_url,
endpoint=None,
password=self.user.password,
tenant_id=None,
token=None,
username=self.user.name).AndReturn(self.conn)
self.mox.ReplayAll()
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password)
self.assertEqual(client.management_url, self.internal_url)
def test_connect_admin(self):
self.test_user.roles = [{'name': 'admin'}]
keystone_client.Client(auth_url=self.admin_url,
endpoint=None,
password=self.user.password,
tenant_id=None,
token=None,
username=self.user.name).AndReturn(self.conn)
self.mox.ReplayAll()
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password,
admin=True)
self.assertEqual(client.management_url, self.admin_url)
def connection_caching(self):
self.test_user.roles = [{'name': 'admin'}]
# Regular connection
keystone_client.Client(auth_url=self.internal_url,
endpoint=None,
password=self.user.password,
tenant_id=None,
token=None,
username=self.user.name).AndReturn(self.conn)
# Admin connection
keystone_client.Client(auth_url=self.admin_url,
endpoint=None,
password=self.user.password,
tenant_id=None,
token=None,
username=self.user.name).AndReturn(self.conn)
self.mox.ReplayAll()
# Request both admin and regular connections out of order,
# If the caching fails we would see UnexpectedMethodCall errors
# from mox.
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password)
self.assertEqual(client.management_url, self.internal_url)
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password,
admin=True)
self.assertEqual(client.management_url, self.admin_url)
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password)
self.assertEqual(client.management_url, self.internal_url)
client = api.keystone.keystoneclient(self.request,
username=self.user.name,
password=self.user.password,
admin=True)
self.assertEqual(client.management_url, self.admin_url)
class TokenApiTests(test.APITestCase):
def test_token_create(self):
token = self.tokens.scoped_token
keystoneclient = self.stub_keystoneclient()
keystoneclient.tokens = self.mox.CreateMockAnything()
keystoneclient.tokens.authenticate(username=self.user.name,
password=self.user.password,
tenant_id=token.tenant['id'])\
.AndReturn(token)
self.mox.ReplayAll()
ret_val = api.token_create(self.request, token.tenant['id'],
self.user.name, self.user.password)
self.assertEqual(token.tenant['id'], ret_val.tenant['id'])
class RoleAPITests(test.APITestCase):
def setUp(self):

View File

@ -1,291 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
from django import http
from django.conf import settings
from django.core.urlresolvers import reverse
from keystoneclient import exceptions as keystone_exceptions
from mox import IsA
from horizon import api
from horizon import test
SYSPANEL_INDEX_URL = reverse('horizon:syspanel:overview:index')
DASH_INDEX_URL = reverse('horizon:nova:overview:index')
class AuthViewTests(test.TestCase):
def setUp(self):
super(AuthViewTests, self).setUp()
self.setActiveUser()
def test_login_index(self):
res = self.client.get(reverse('horizon:auth_login'))
self.assertTemplateUsed(res, 'horizon/auth/login.html')
def test_login_user_logged_in(self):
self.setActiveUser(self.tokens.first().id,
self.user.name,
self.tenant.id,
False,
self.service_catalog)
# Hitting the login URL directly should always give you a login page.
res = self.client.get(reverse('horizon:auth_login'))
self.assertTemplateUsed(res, 'horizon/auth/login.html')
def test_login_no_tenants(self):
aToken = self.tokens.first()
self.mox.StubOutWithMock(api, 'token_create')
self.mox.StubOutWithMock(api, 'tenant_list_for_token')
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\
AndReturn([])
self.mox.ReplayAll()
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertTemplateUsed(res, 'horizon/auth/login.html')
@test.create_stubs({api: ('token_create', 'tenant_list_for_token',
'token_create_scoped')})
def test_login(self):
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
aToken = self.tokens.unscoped_token
bToken = self.tokens.scoped_token
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest),
aToken.id).AndReturn([self.tenants.first()])
api.token_create_scoped(IsA(http.HttpRequest),
self.tenant.id,
aToken.id).AndReturn(bToken)
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest),
aToken.id).AndReturn([self.tenants.first()])
api.token_create_scoped(IsA(http.HttpRequest),
self.tenant.id,
aToken.id).AndReturn(bToken)
self.mox.ReplayAll()
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertRedirectsNoFollow(res, DASH_INDEX_URL)
# Test default Django LOGIN_REDIRECT_URL
user_home = settings.HORIZON_CONFIG.pop('user_home')
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertRedirectsNoFollow(res, settings.LOGIN_REDIRECT_URL)
settings.HORIZON_CONFIG['user_home'] = user_home
def test_login_first_tenant_invalid(self):
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
self.mox.StubOutWithMock(api, 'token_create')
self.mox.StubOutWithMock(api, 'tenant_list_for_token')
self.mox.StubOutWithMock(api, 'token_create_scoped')
aToken = self.tokens.unscoped_token
bToken = self.tokens.scoped_token
disabled_tenant = self.tenants.get(name="disabled_tenant")
tenant = self.tenants.get(name="test_tenant")
tenants = [tenant, disabled_tenant]
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest),
aToken.id).AndReturn(tenants)
exc = keystone_exceptions.Unauthorized("Not authorized.")
exc.silence_logging = True
api.token_create_scoped(IsA(http.HttpRequest),
disabled_tenant.id,
aToken.id).AndRaise(exc)
api.token_create_scoped(IsA(http.HttpRequest),
tenant.id,
aToken.id).AndReturn(bToken)
self.mox.ReplayAll()
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertNoFormErrors(res)
self.assertNoMessages()
self.assertRedirectsNoFollow(res, DASH_INDEX_URL)
def test_login_invalid_credentials(self):
self.mox.StubOutWithMock(api, 'token_create')
unauthorized = keystone_exceptions.Unauthorized("Invalid")
unauthorized.silence_logging = True
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndRaise(unauthorized)
self.mox.ReplayAll()
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
res = self.client.post(reverse('horizon:auth_login'),
form_data,
follow=True)
self.assertTemplateUsed(res, 'horizon/auth/login.html')
# Verify that API error messages are rendered, but not using the
# messages framework.
self.assertContains(res, "Invalid user name or password.")
self.assertNotContains(res, 'class="messages"')
def test_login_exception(self):
self.mox.StubOutWithMock(api, 'token_create')
api.token_create(IsA(http.HttpRequest),
"",
self.user.name,
self.user.password).AndRaise(self.exceptions.keystone)
self.mox.ReplayAll()
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertTemplateUsed(res, 'horizon/auth/login.html')
def test_switch_tenants_index(self):
res = self.client.get(reverse('horizon:auth_switch',
args=[self.tenant.id]))
self.assertRedirects(res, reverse("horizon:auth_login"))
def test_switch_tenants(self):
tenants = self.tenants.list()
tenant = self.tenants.first()
token = self.tokens.unscoped_token
scoped_token = self.tokens.scoped_token
switch_to = scoped_token.tenant['id']
user = self.users.first()
self.setActiveUser(id=user.id,
token=token.id,
username=user.name,
tenant_id=tenant.id,
service_catalog=self.service_catalog,
authorized_tenants=tenants)
self.mox.StubOutWithMock(api, 'token_create')
self.mox.StubOutWithMock(api, 'tenant_list_for_token')
api.token_create(IsA(http.HttpRequest),
switch_to,
user.name,
user.password).AndReturn(scoped_token)
api.tenant_list_for_token(IsA(http.HttpRequest),
token.id).AndReturn(tenants)
self.mox.ReplayAll()
form_data = {'method': 'LoginWithTenant',
'region': 'http://localhost:5000/v2.0',
'username': user.name,
'password': user.password,
'tenant': switch_to}
switch_url = reverse('horizon:auth_switch', args=[switch_to])
res = self.client.post(switch_url, form_data)
self.assertRedirectsNoFollow(res, DASH_INDEX_URL)
self.assertEqual(self.client.session['tenant'],
scoped_token.tenant['name'])
def test_logout(self):
KEY = 'arbitraryKeyString'
VALUE = 'arbitraryKeyValue'
self.assertNotIn(KEY, self.client.session)
self.client.session[KEY] = VALUE
res = self.client.get(reverse('horizon:auth_logout'))
self.assertRedirectsNoFollow(res, reverse('splash'))
self.assertNotIn(KEY, self.client.session)
def test_session_fixation(self):
session_ids = []
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0',
'password': self.user.password,
'username': self.user.name}
self.mox.StubOutWithMock(api, 'token_create')
self.mox.StubOutWithMock(api, 'tenant_list_for_token')
self.mox.StubOutWithMock(api, 'token_create_scoped')
aToken = self.tokens.unscoped_token
bToken = self.tokens.scoped_token
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest),
aToken.id).AndReturn([self.tenants.first()])
api.token_create_scoped(IsA(http.HttpRequest),
self.tenant.id,
aToken.id).AndReturn(bToken)
api.token_create(IsA(http.HttpRequest), "", self.user.name,
self.user.password).AndReturn(aToken)
api.tenant_list_for_token(IsA(http.HttpRequest),
aToken.id).AndReturn([self.tenants.first()])
api.token_create_scoped(IsA(http.HttpRequest),
self.tenant.id,
aToken.id).AndReturn(bToken)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:auth_login'))
self.assertEqual(res.cookies.get('sessionid'), None)
res = self.client.post(reverse('horizon:auth_login'), form_data)
session_ids.append(res.cookies['sessionid'].value)
self.assertEquals(self.client.session['user_name'],
self.user.name)
self.client.session['foobar'] = 'MY TEST VALUE'
res = self.client.get(reverse('horizon:auth_logout'))
session_ids.append(res.cookies['sessionid'].value)
self.assertEqual(len(self.client.session.items()), 0)
# Sleep for 1 second so the session values are different if
# using the signed_cookies backend.
time.sleep(1)
res = self.client.post(reverse('horizon:auth_login'), form_data)
session_ids.append(res.cookies['sessionid'].value)
# Make sure all 3 session id values are different
self.assertEqual(len(session_ids), len(set(session_ids)))

View File

@ -21,14 +21,14 @@
from django.conf import settings
from django.core import urlresolvers
from django.test.client import Client
from django.utils.importlib import import_module
from django.utils.translation import ugettext_lazy as _
from openstack_auth import user, backend
import horizon
from horizon import base
from horizon import test
from horizon import users
from horizon.dashboards.nova.dashboard import Nova
from horizon.dashboards.syspanel.dashboard import Syspanel
from horizon.dashboards.settings.dashboard import Settings
@ -48,14 +48,14 @@ class MyDash(horizon.Dashboard):
class MyPanel(horizon.Panel):
name = _("My Panel")
slug = "myslug"
services = ("compute",)
permissions = ("openstack.services.compute",)
urls = 'horizon.tests.test_panel_urls'
class AdminPanel(horizon.Panel):
name = _("Admin Panel")
slug = "admin_panel"
roles = ("admin",)
permissions = ("openstack.roles.admin",)
urls = 'horizon.tests.test_panel_urls'
@ -166,8 +166,8 @@ class HorizonTests(BaseHorizonTests):
self.assertEqual(repr(base.Horizon), "<Site: horizon>")
dash = base.Horizon.get_dashboard('cats')
self.assertEqual(base.Horizon.get_default_dashboard(), dash)
user = users.User()
self.assertEqual(base.Horizon.get_user_home(user),
test_user = user.User()
self.assertEqual(base.Horizon.get_user_home(test_user),
dash.get_absolute_url())
def test_dashboard(self):
@ -230,24 +230,23 @@ class HorizonTests(BaseHorizonTests):
self.assertFalse(hasattr(cats, "evil"))
def test_public(self):
users.get_user_from_request = self._real_get_user_from_request
backend.get_user = self._real_get_user
dogs = horizon.get_dashboard("dogs")
# Known to have no restrictions on it other than being logged in.
puppies = dogs.get_panel("puppies")
url = puppies.get_absolute_url()
# Get a clean, logged out client instance.
client = Client()
client.logout()
resp = client.get(url)
redirect_url = "?".join([urlresolvers.reverse("horizon:auth_login"),
self.setActiveUser()
resp = self.client.get(url)
redirect_url = "?".join([urlresolvers.reverse("login"),
"next=%s" % url])
self.assertRedirectsNoFollow(resp, redirect_url)
# Simulate ajax call
resp = client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Response should be HTTP 401 with redirect header
self.assertEquals(resp.status_code, 401)
self.assertEquals(resp["X-Horizon-Location"],
"?".join([urlresolvers.reverse("horizon:auth_login"),
"?".join([urlresolvers.reverse("login"),
"next=%s" % url]))
def test_required_services(self):
@ -263,23 +262,24 @@ class HorizonTests(BaseHorizonTests):
# Remove the required service from the service catalog and we
# should get a 404.
service_name = MyPanel.permissions[0].split(".")[-1]
new_catalog = [service for service in self.request.user.service_catalog
if service['type'] != MyPanel.services[0]]
if service['type'] != service_name]
tenants = self.context['authorized_tenants']
self.setActiveUser(token=self.token.id,
self.setActiveUser(token=self.token,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=new_catalog,
authorized_tenants=tenants)
resp = self.client.get(panel.get_absolute_url())
self.assertEqual(resp.status_code, 404)
self.assertEqual(resp.status_code, 302)
def test_required_roles(self):
def test_required_permissions(self):
dash = horizon.get_dashboard("cats")
panel = dash.get_panel('tigers')
# Non-admin user
self.setActiveUser(token=self.token.id,
self.setActiveUser(token=self.token,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=self.service_catalog,
@ -294,7 +294,7 @@ class HorizonTests(BaseHorizonTests):
self.assertEqual(resp.status_code, 401)
# Set roles for admin user
self.setActiveUser(token=self.token.id,
self.setActiveUser(token=self.token,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=self.request.user.service_catalog,
@ -310,16 +310,15 @@ class HorizonTests(BaseHorizonTests):
self.assertEqual(resp.status_code, 200)
def test_ssl_redirect_by_proxy(self):
users.get_user_from_request = self._real_get_user_from_request
backend.get_user = self._real_get_user
dogs = horizon.get_dashboard("dogs")
puppies = dogs.get_panel("puppies")
url = puppies.get_absolute_url()
redirect_url = "?".join([urlresolvers.reverse("horizon:auth_login"),
redirect_url = "?".join([urlresolvers.reverse("login"),
"next=%s" % url])
client = Client()
client.logout()
resp = client.get(url)
self.setActiveUser()
resp = self.client.get(url)
self.assertRedirectsNoFollow(resp, redirect_url)
# Set SSL settings for test server
@ -327,7 +326,7 @@ class HorizonTests(BaseHorizonTests):
settings.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL',
'https')
resp = client.get(url, HTTP_X_FORWARDED_PROTOCOL="https")
resp = self.client.get(url, HTTP_X_FORWARDED_PROTOCOL="https")
self.assertRedirectsNoFollow(resp, redirect_url)
# Restore settings

View File

@ -1,61 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import http
from mox import IsA
from horizon import api
from horizon import context_processors
from horizon import middleware
from horizon import test
from horizon import Dashboard
class ContextProcessorTests(test.TestCase):
def setUp(self):
super(ContextProcessorTests, self).setUp()
self._prev_catalog = self.request.user.service_catalog
context_processors.horizon = self._real_horizon_context_processor
def tearDown(self):
super(ContextProcessorTests, self).tearDown()
self.request.user.service_catalog = self._prev_catalog
def test_authorized_tenants(self):
tenant_list = self.context['authorized_tenants']
self.request.user.authorized_tenants = None # Reset from setUp
self.mox.StubOutWithMock(api, 'tenant_list_for_token')
api.tenant_list_for_token(IsA(http.HttpRequest), self.token.id) \
.AndReturn(tenant_list)
self.mox.ReplayAll()
middleware.HorizonMiddleware().process_request(self.request)
# Without dashboard that has "supports_tenants = True"
context = context_processors.horizon(self.request)
self.assertEqual(len(context['authorized_tenants']), 0)
# With dashboard that has "supports_tenants = True"
class ProjectDash(Dashboard):
supports_tenants = True
self.request.horizon['dashboard'] = ProjectDash
self.assertTrue(self.request.user.is_authenticated())
context = context_processors.horizon(self.request)
self.assertItemsEqual(context['authorized_tenants'], tenant_list)

View File

@ -65,7 +65,7 @@ TEST_DATA_5 = (
class MyLinkAction(tables.LinkAction):
name = "login"
verbose_name = "Log In"
url = "horizon:auth_login"
url = "login"
attrs = {
"class": "ajax-modal",
}
@ -148,7 +148,7 @@ def get_name(obj):
def get_link(obj):
return reverse('horizon:auth_login')
return reverse('login')
class MyTable(tables.DataTable):

View File

@ -8,7 +8,7 @@ from horizon.tests.test_dashboards.cats import dashboard
class Kittens(horizon.Panel):
name = _("Kittens")
slug = "kittens"
require_roles = ("admin",)
permissions = ("openstack.roles.admin",)
dashboard.Cats.register(Kittens)

View File

@ -8,7 +8,7 @@ from horizon.tests.test_dashboards.cats import dashboard
class Tigers(horizon.Panel):
name = _("Tigers")
slug = "tigers"
roles = ("admin",)
permissions = ("openstack.roles.admin",)
dashboard.Cats.register(Tigers)

View File

@ -3,7 +3,7 @@ from horizon import views
class IndexView(views.APIView):
# A very simple class-based view...
template_name = 'puppies/index.html'
template_name = 'dogs/puppies/index.html'
def get_data(self, request, context, *args, **kwargs):
# Add data to the context here...

View File

@ -12,7 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from datetime import timedelta
from django.conf import settings
from django.utils import datetime_safe
from keystoneclient.v2_0 import users, tenants, tokens, roles, ec2
from .utils import TestDataContainer
@ -127,9 +131,12 @@ def data(TEST):
TEST.tenants.add(tenant, disabled_tenant)
TEST.tenant = tenant # Your "current" tenant
tomorrow = datetime_safe.datetime.now() + timedelta(days=1)
expiration = datetime_safe.datetime.isoformat(tomorrow)
scoped_token = tokens.Token(tokens.TokenManager,
dict(token={"id": "test_token_id",
"expires": "#FIXME",
"expires": expiration,
"tenant": tenant_dict,
"tenants": [tenant_dict]},
user={"id": "test_user_id",
@ -138,7 +145,7 @@ def data(TEST):
serviceCatalog=TEST.service_catalog))
unscoped_token = tokens.Token(tokens.TokenManager,
dict(token={"id": "test_token_id",
"expires": "#FIXME"},
"expires": expiration},
user={"id": "test_user_id",
"name": "test_user",
"roles": [member_role_dict]},

View File

@ -44,6 +44,7 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.humanize',
'django_nose',
'openstack_auth',
'horizon',
'horizon.tests',
'horizon.dashboards.nova',
@ -57,6 +58,7 @@ MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.middleware.locale.LocaleMiddleware',
@ -99,6 +101,8 @@ SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_SECURE = False
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
HORIZON_CONFIG = {
'dashboards': ('nova', 'syspanel', 'settings'),
'default_dashboard': 'nova',

View File

@ -29,5 +29,6 @@ import horizon
urlpatterns = patterns('',
url(r'^$', 'horizon.tests.views.fakeView', name='splash'),
url(r'^auth/', include('openstack_auth.urls')),
url(r'', include(horizon.urls)),
)

View File

@ -76,7 +76,7 @@ class AdminAction(workflows.Action):
class Meta:
name = _("Admin Action")
slug = "admin_action"
roles = ("admin",)
permissions = ("openstack.roles.admin",)
class TestStepOne(workflows.Step):
@ -239,7 +239,8 @@ class WorkflowsTests(test.TestCase):
flow = TestWorkflow(self.request)
step = AdminStep(flow)
self.assertItemsEqual(step.roles, (self.roles.admin.name,))
self.assertItemsEqual(step.permissions,
("openstack.roles.%s" % self.roles.admin.name,))
self.assertQuerysetEqual(flow.steps,
['<TestStepOne: test_action_one>',
'<TestStepTwo: test_action_two>'])

View File

@ -1,164 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Classes and methods related to user handling in Horizon.
"""
import logging
from django.utils.translation import ugettext as _
from horizon import api
from horizon import exceptions
LOG = logging.getLogger(__name__)
def get_user_from_request(request):
""" Checks the current session and returns a :class:`~horizon.users.User`.
If the session contains user data the User will be treated as
authenticated and the :class:`~horizon.users.User` will have all
its attributes set.
If not, the :class:`~horizon.users.User` will have no attributes set.
If the session contains invalid data,
:exc:`~horizon.exceptions.NotAuthorized` will be raised.
"""
if 'user_id' not in request.session:
return User()
try:
return User(id=request.session['user_id'],
token=request.session['token'],
user=request.session['user_name'],
tenant_id=request.session['tenant_id'],
tenant_name=request.session['tenant'],
service_catalog=request.session['serviceCatalog'],
roles=request.session['roles'],
request=request)
except KeyError:
# If any of those keys are missing from the session it is
# overwhelmingly likely that we're dealing with an outdated session.
LOG.exception("Error while creating User from session.")
request.user_logout()
raise exceptions.NotAuthorized(_("Your session has expired. "
"Please log in again."))
class LazyUser(object):
def __get__(self, request, obj_type=None):
if not hasattr(request, '_cached_user'):
request._cached_user = get_user_from_request(request)
return request._cached_user
class User(object):
""" The main user class which Horizon expects.
.. attribute:: token
The id of the Keystone token associated with the current user/tenant.
.. attribute:: username
The name of the current user.
.. attribute:: tenant_id
The id of the Keystone tenant for the current user/token.
.. attribute:: tenant_name
The name of the Keystone tenant for the current user/token.
.. attribute:: service_catalog
The ``ServiceCatalog`` data returned by Keystone.
.. attribute:: roles
A list of dictionaries containing role names and ids as returned
by Keystone.
.. attribute:: admin
Boolean value indicating whether or not this user has admin
privileges. Internally mapped to :meth:`horizon.users.User.is_admin`.
"""
def __init__(self, id=None, token=None, user=None, tenant_id=None,
service_catalog=None, tenant_name=None, roles=None,
authorized_tenants=None, request=None):
self.id = id
self.token = token
self.username = user
self.tenant_id = tenant_id
self.tenant_name = tenant_name
self.service_catalog = service_catalog
self.roles = roles or []
self._authorized_tenants = authorized_tenants
# Store the request for lazy fetching of auth'd tenants
self._request = request
def is_authenticated(self):
"""
Evaluates whether this :class:`.User` instance has been authenticated.
Returns ``True`` or ``False``.
"""
# TODO: deal with token expiration
return self.token
@property
def admin(self):
return self.is_admin()
def is_admin(self):
"""
Evaluates whether this user has admin privileges. Returns
``True`` or ``False``.
"""
for role in self.roles:
if role['name'].lower() == 'admin':
return True
return False
def get_and_delete_messages(self):
"""
Placeholder function for parity with
``django.contrib.auth.models.User``.
"""
return []
@property
def authorized_tenants(self):
if self.is_authenticated() and self._authorized_tenants is None:
try:
token = self._request.session.get("unscoped_token", self.token)
authd = api.tenant_list_for_token(self._request, token)
except:
authd = []
LOG.exception('Could not retrieve tenant list.')
self._authorized_tenants = authd
return self._authorized_tenants
@authorized_tenants.setter
def authorized_tenants(self, tenant_list):
self._authorized_tenants = tenant_list

View File

@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from .base import APIView
from .base import APIView, user_home

View File

@ -1,108 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django import shortcuts
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_post_parameters
import horizon
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import users
from horizon.base import Horizon
from horizon.views.auth_forms import Login, LoginWithTenant, _set_session_data
LOG = logging.getLogger(__name__)
def user_home(request):
""" Reversible named view to direct a user to the appropriate homepage. """
return shortcuts.redirect(horizon.get_user_home(request.user))
class LoginView(forms.ModalFormView):
"""
Logs in a user and redirects them to the URL specified by
:func:`horizon.get_user_home`.
"""
form_class = Login
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):
context = super(LoginView, self).get_context_data(**kwargs)
redirect_to = self.request.REQUEST.get(REDIRECT_FIELD_NAME, "")
context["redirect_field_name"] = REDIRECT_FIELD_NAME
context["next"] = redirect_to
return context
def get_initial(self):
initial = super(LoginView, self).get_initial()
current_region = self.request.session.get('region_endpoint', None)
requested_region = self.request.GET.get('region', None)
regions = dict(getattr(settings, "AVAILABLE_REGIONS", []))
if requested_region in regions and requested_region != current_region:
initial.update({'region': requested_region})
return initial
@sensitive_post_parameters("password")
def switch_tenants(request, tenant_id):
"""
Swaps a user from one tenant to another using the unscoped token from
Keystone to exchange scoped tokens for the new tenant.
"""
form, handled = LoginWithTenant.maybe_handle(
request, initial={'tenant': tenant_id,
'username': request.user.username})
if handled:
return handled
unscoped_token = request.session.get('unscoped_token', None)
if unscoped_token:
try:
token = api.token_create_scoped(request,
tenant_id,
unscoped_token)
_set_session_data(request, token)
user = users.User(users.get_user_from_request(request))
return shortcuts.redirect(Horizon.get_user_home(user))
except:
exceptions.handle(request,
_("You are not authorized for that tenant."))
return shortcuts.redirect("horizon:auth_login")
def logout(request):
""" Clears the session and logs the current user out. """
request.user_logout()
# FIXME(gabriel): we don't ship a view named splash
return shortcuts.redirect('splash')

View File

@ -1,199 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Forms used for Horizon's auth mechanisms.
"""
import logging
from django import shortcuts
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_variables
from keystoneclient import exceptions as keystone_exceptions
from horizon import api
from horizon import base
from horizon import exceptions
from horizon import forms
from horizon import users
LOG = logging.getLogger(__name__)
def _set_session_data(request, token):
request.session['serviceCatalog'] = token.serviceCatalog
request.session['tenant'] = token.tenant['name']
request.session['tenant_id'] = token.tenant['id']
request.session['token'] = token.id
request.session['user_name'] = token.user['name']
request.session['user_id'] = token.user['id']
request.session['roles'] = token.user['roles']
class Login(forms.SelfHandlingForm):
""" Form used for logging in a user.
Handles authentication with Keystone, choosing a tenant, and fetching
a scoped token token for that tenant. Redirects to the URL returned
by :meth:`horizon.get_user_home` if successful.
Subclass of :class:`~horizon.forms.SelfHandlingForm`.
"""
region = forms.ChoiceField(label=_("Region"), required=False)
username = forms.CharField(label=_("User Name"))
password = forms.CharField(label=_("Password"),
widget=forms.PasswordInput(render_value=False))
def __init__(self, *args, **kwargs):
super(Login, self).__init__(*args, **kwargs)
# FIXME(gabriel): When we switch to region-only settings, we can
# remove this default region business.
default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region")
regions = getattr(settings, 'AVAILABLE_REGIONS', [default_region])
self.fields['region'].choices = regions
if len(regions) == 1:
self.fields['region'].initial = default_region[0]
self.fields['region'].widget = forms.widgets.HiddenInput()
@sensitive_variables("data")
def handle(self, request, data):
""" Process the user's login via Keystone.
Note: We don't use the messages framework here (including messages
created by ``exceptions.handle`` beause they will not be displayed
on the login page (intentionally). Instead we add all error messages
to the form's ``non_field_errors``, causing them to appear as
errors on the form itself.
"""
if 'user_name' in request.session:
if request.session['user_name'] != data['username']:
# To avoid reusing another user's session, create a
# new, empty session if the existing session
# corresponds to a different authenticated user.
request.session.flush()
# Always cycle the session key when viewing the login form to
# prevent session fixation
request.session.cycle_key()
# For now we'll allow fallback to OPENSTACK_KEYSTONE_URL if the
# form post doesn't include a region.
endpoint = data.get('region', None) or settings.OPENSTACK_KEYSTONE_URL
if endpoint != request.session.get('region_endpoint', None):
region_name = dict(self.fields['region'].choices)[endpoint]
request.session['region_endpoint'] = endpoint
request.session['region_name'] = region_name
request.user.service_catalog = None
redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, "")
if data.get('tenant', None):
try:
token = api.token_create(request,
data.get('tenant'),
data['username'],
data['password'])
tenants = api.tenant_list_for_token(request, token.id)
except:
msg = _('Unable to authenticate for that project.')
exceptions.handle(request, ignore=True)
return self.api_error(msg)
_set_session_data(request, token)
user = users.get_user_from_request(request)
redirect = redirect_to or base.Horizon.get_user_home(user)
return shortcuts.redirect(redirect)
elif data.get('username', None):
try:
unscoped_token = api.token_create(request,
'',
data['username'],
data['password'])
except keystone_exceptions.Unauthorized:
msg = _('Invalid user name or password.')
exceptions.handle(request, ignore=True)
return self.api_error(msg)
except:
# If we get here we don't want to show a stack trace to the
# user. However, if we fail here, there may be bad session
# data that's been cached already.
request.user_logout()
msg = _("An error occurred authenticating. "
"Please try again later.")
exceptions.handle(request, ignore=True)
return self.api_error(msg)
# Unscoped token
request.session['unscoped_token'] = unscoped_token.id
request.user.username = data['username']
# Get the tenant list, and log in using first tenant
# FIXME (anthony): add tenant chooser here?
try:
tenants = api.tenant_list_for_token(request, unscoped_token.id)
except:
exceptions.handle(request, ignore=True)
tenants = []
# Abort if there are no valid tenants for this user
if not tenants:
msg = _('You are not authorized for any projects.')
return self.api_error(msg)
# Create a token.
# NOTE(gabriel): Keystone can return tenants that you're
# authorized to administer but not to log into as a user, so in
# the case of an Unauthorized error we should iterate through
# the tenants until one succeeds or we've failed them all.
while tenants:
tenant = tenants.pop()
try:
token = api.token_create_scoped(request,
tenant.id,
unscoped_token.id)
break
except:
# This will continue for recognized Unauthorized
# exceptions from keystoneclient.
exceptions.handle(request, ignore=True)
token = None
if token is None:
msg = _("You are not authorized for any available projects.")
return self.api_error(msg)
_set_session_data(request, token)
user = users.get_user_from_request(request)
redirect = redirect_to or base.Horizon.get_user_home(user)
return shortcuts.redirect(redirect)
class LoginWithTenant(Login):
"""
Exactly like :class:`.Login` but includes the tenant id as a field
so that the process of choosing a default tenant is bypassed.
"""
region = forms.ChoiceField(required=False)
username = forms.CharField(max_length="20",
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
tenant = forms.CharField(widget=forms.HiddenInput())

View File

@ -14,11 +14,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from django import shortcuts
from django.views import generic
import horizon
from horizon import exceptions
def user_home(request):
""" Reversible named view to direct a user to the appropriate homepage. """
return shortcuts.redirect(horizon.get_user_home(request.user))
class APIView(generic.TemplateView):
""" A quick class-based view for putting API data into a template.

View File

@ -63,8 +63,7 @@ class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass):
opts = attrs.pop("Meta", None)
attrs['name'] = getattr(opts, "name", name)
attrs['slug'] = getattr(opts, "slug", slugify(name))
attrs['roles'] = getattr(opts, "roles", ())
attrs['services'] = getattr(opts, "services", ())
attrs['permissions'] = getattr(opts, "permissions", ())
attrs['progress_message'] = getattr(opts,
"progress_message",
_("Processing..."))
@ -87,7 +86,7 @@ class Action(forms.Form):
controls, and thus inherit from Django's ``Form`` class. However, they
have some additional intelligence added to them:
* ``Actions`` are aware of the roles required to complete them.
* ``Actions`` are aware of the permissions required to complete them.
* ``Actions`` have a meta-level concept of "help text" which is meant to be
displayed in such a way as to give context to the action regardless of
@ -108,14 +107,9 @@ class Action(forms.Form):
A semi-unique slug for this action. Defaults to the "slugified" name
of the class.
.. attribute:: roles
.. attribute:: permissions
A list of role names which this action requires in order to be
completed. Defaults to an empty list (``[]``).
.. attribute:: services
A list of service types which this action requires in order to be
A list of permission names which this action requires in order to be
completed. Defaults to an empty list (``[]``).
.. attribute:: help_text
@ -260,11 +254,7 @@ class Step(object):
Inherited from the ``Action`` class.
.. attribute:: roles
Inherited from the ``Action`` class.
.. attribute:: services
.. attribute:: permissions
Inherited from the ``Action`` class.
"""
@ -293,8 +283,7 @@ class Step(object):
self.slug = self.action_class.slug
self.name = self.action_class.name
self.roles = self.action_class.roles
self.services = self.action_class.services
self.permissions = self.action_class.permissions
self.has_errors = False
self._handlers = {}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,46 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -103,10 +99,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,46 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -104,10 +100,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: openstack-dashboard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: 2012-05-08 00:20+0100\n"
"Last-Translator: Erwan Gallen <erwan@zinux.com>\n"
"Language-Team: French <fr@zinux.com>\n"
@ -16,46 +16,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr "Anglais"
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr "Italien"
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr "Espagnol"
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr "Français"
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr "Japonais"
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr "Portugais"
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr "Polonais"
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr "Chinois simplifié"
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr "Chinois traditionnel"
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr "Interdit"
@ -105,10 +101,15 @@ msgstr "Réglages"
msgid "Sign Out"
msgstr "Déconnexion"
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
msgstr "Se connecter au tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
#, fuzzy
msgid "Sign In"
msgstr "Déconnexion"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,46 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -104,10 +100,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -19,46 +19,42 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2)\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -105,10 +101,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,46 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -104,10 +100,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,46 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr ""
@ -103,10 +99,14 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-05-29 11:47-0700\n"
"POT-Creation-Date: 2012-07-09 02:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,46 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: settings.py:115
#: settings.py:136
msgid "English"
msgstr ""
#: settings.py:116
#: settings.py:137
msgid "Italiano"
msgstr ""
#: settings.py:117
#: settings.py:138
msgid "Spanish"
msgstr ""
#: settings.py:118
#: settings.py:139
msgid "French"
msgstr ""
#: settings.py:119
#: settings.py:140
msgid "Japanese"
msgstr ""
#: settings.py:120
#: settings.py:141
msgid "Portuguese"
msgstr ""
#: settings.py:121
#: settings.py:142
msgid "Polish"
msgstr ""
#: settings.py:122
#: settings.py:143
msgid "Simplified Chinese"
msgstr ""
#: settings.py:123
#: settings.py:144
msgid "Traditional Chinese"
msgstr ""
#: local/local_settings.py:17
msgid "Your password must be at least 6 characters long."
msgstr ""
#: templates/403.html:4 templates/403.html.py:9
msgid "Forbidden"
msgstr "禁止"
@ -103,13 +99,14 @@ msgstr "設定"
msgid "Sign Out"
msgstr "登出"
#: templates/_scripts.html:39
msgid "Loading&hellip;"
#: templates/splash.html:7 templates/auth/login.html:4
msgid "Login"
msgstr ""
#: templates/switch_tenants.html:12
msgid "Log-in to tenant"
msgstr "登入到租戶"
#: templates/auth/_login.html:4
msgid "Log In"
msgstr ""
#~ msgid "Sign In"
#~ msgstr "登入"
#: templates/auth/_login.html:17
msgid "Sign In"
msgstr "登入"

View File

@ -1,50 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django import shortcuts
from django.contrib import messages
from novaclient import exceptions as novaclient_exceptions
LOG = logging.getLogger('openstack_dashboard')
class DashboardLogUnhandledExceptionsMiddleware(object):
def process_exception(self, request, exception):
if isinstance(exception, novaclient_exceptions.Unauthorized):
try:
exception.message.index('reauthenticate')
# clear the errors
for message in messages.get_messages(request):
LOG.debug('Discarded message - %s: "%s"'
% (message.tags, message.message))
messages.info(request, 'Your session has timed out.'
' Please log back in.')
LOG.info('User "%s" auth token expired, redirecting to logout'
% request.user.username)
return shortcuts.redirect('auth_logout')
except ValueError:
pass
LOG.critical('Unhandled Exception in of type "%s" in dashboard.'
% type(exception), exc_info=True)

View File

@ -32,7 +32,7 @@ DEBUG = False
TEMPLATE_DEBUG = DEBUG
SITE_ID = 1
SITE_BRANDING = 'OpenStack'
SITE_BRANDING = 'OpenStack Dashboard'
LOGIN_URL = '/auth/login/'
LOGOUT_URL = '/auth/logout/'
@ -60,8 +60,8 @@ MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'openstack_dashboard.middleware.DashboardLogUnhandledExceptionsMiddleware',
'horizon.middleware.HorizonMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.middleware.locale.LocaleMiddleware',
@ -108,6 +108,8 @@ COMPRESS_PARSER = 'compressor.parser.HtmlParser'
INSTALLED_APPS = (
'openstack_dashboard',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
@ -117,9 +119,11 @@ INSTALLED_APPS = (
'horizon.dashboards.nova',
'horizon.dashboards.syspanel',
'horizon.dashboards.settings',
'openstack_auth',
)
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'

View File

@ -2,6 +2,6 @@
<div id="user_info" class="pull-right">
<span>{% trans "Logged in as" %}: {{ request.user.username }}</span>
<a href="{% url horizon:settings:user:index %}">{% trans "Settings" %}</a>
<a href="{% url horizon:auth_logout %}">{% trans "Sign Out" %}</a>
<a href="{% url logout %}">{% trans "Sign Out" %}</a>
{% include "horizon/common/_region_selector.html" %}
</div>

View File

@ -4,7 +4,7 @@
{% block modal-header %}{% trans "Log In" %}{% endblock %}
{% block modal_class %}login {% if hide %}modal hide{% endif %}{% endblock %}
{% block form_action %}{% url horizon:auth_login %}{% endblock %}
{% block form_action %}{% url login %}{% endblock %}
{% block modal-body %}
<fieldset>

View File

@ -6,5 +6,5 @@
{% block body_id %}splash{% endblock %}
{% block content %}
{% include 'horizon/auth/_login.html' %}
{% include 'auth/_login.html' %}
{% endblock %}

View File

@ -3,7 +3,7 @@
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<title>{% block title %}{% endblock %} {% site_branding %} Dashboard</title>
<title>{% block title %}{% endblock %} - {% site_branding %}</title>
{% include "horizon/_conf.html" %}
{% block css %}
{% include "_stylesheets.html" %}

View File

@ -1,14 +1,16 @@
{% load i18n branding %}
<!DOCTYPE html>
<html lang="en" xml:lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Login OpenStack Dashboard</title>
<title>{% trans "Login" %} - {% site_branding %}</title>
{% include "_stylesheets.html" %}
</head>
<body id="splash">
<div class="container">
<div class="row large-rounded">
{% include 'horizon/auth/_login.html' %}
{% include 'auth/_login.html' %}
</div>
</div>
</body>

View File

@ -32,6 +32,7 @@ import horizon
urlpatterns = patterns('',
url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
url(r'^auth/', include('openstack_auth.urls')),
url(r'', include(horizon.urls)))
# Development static app and project media serving using the staticfiles app.

View File

@ -25,11 +25,11 @@ from django import shortcuts
from django.views.decorators import vary
import horizon
from horizon.views import auth_forms
from openstack_auth.views import Login
def user_home(user):
if user.admin:
if user.is_superuser:
return horizon.get_dashboard('syspanel').get_absolute_url()
return horizon.get_dashboard('nova').get_absolute_url()
@ -38,6 +38,7 @@ def user_home(user):
def splash(request):
if request.user.is_authenticated():
return shortcuts.redirect(user_home(request.user))
form = auth_forms.Login()
form = Login(request)
request.session.clear()
request.session.set_test_cookie()
return shortcuts.render(request, 'splash.html', {'form': form})

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3...
environment_version=23
environment_version=24
#--------------------------------------------------------#
function usage {

View File

@ -1,6 +1,7 @@
# Horizon Core Requirements
Django>=1.4
django_compressor
django_openstack_auth
python-cloudfiles
python-glanceclient
python-keystoneclient