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:
parent
3990985aa0
commit
c339189b44
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -1,6 +0,0 @@
|
||||
=================
|
||||
Horizon User APIs
|
||||
=================
|
||||
|
||||
.. automodule:: horizon.users
|
||||
:members:
|
@ -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:
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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', '')
|
||||
|
@ -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">
|
||||
|
@ -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.')
|
||||
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
@ -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 %}
|
||||
|
@ -31,7 +31,7 @@ class Syspanel(horizon.Dashboard):
|
||||
slug = "syspanel"
|
||||
panels = (SystemPanels,)
|
||||
default_panel = 'overview'
|
||||
roles = ('admin',)
|
||||
permissions = ('openstack.roles.admin',)
|
||||
|
||||
|
||||
horizon.register(Syspanel)
|
||||
|
@ -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([])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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',))
|
||||
|
@ -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
@ -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)
|
||||
|
@ -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('',
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)))
|
@ -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
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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...
|
||||
|
@ -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]},
|
||||
|
@ -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',
|
||||
|
@ -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)),
|
||||
)
|
||||
|
@ -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>'])
|
||||
|
164
horizon/users.py
164
horizon/users.py
@ -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
|
@ -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
|
||||
|
@ -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')
|
@ -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())
|
@ -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.
|
||||
|
||||
|
@ -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 = {}
|
||||
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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"
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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 ""
|
||||
|
@ -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…"
|
||||
#: 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 "登入"
|
||||
|
@ -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)
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -6,5 +6,5 @@
|
||||
{% block body_id %}splash{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'horizon/auth/_login.html' %}
|
||||
{% include 'auth/_login.html' %}
|
||||
{% endblock %}
|
@ -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" %}
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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})
|
||||
|
@ -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 {
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Horizon Core Requirements
|
||||
Django>=1.4
|
||||
django_compressor
|
||||
django_openstack_auth
|
||||
python-cloudfiles
|
||||
python-glanceclient
|
||||
python-keystoneclient
|
||||
|
Loading…
Reference in New Issue
Block a user