Skip to content

Commit

Permalink
Auth refactor.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gabrielhurley committed Jul 9, 2012
1 parent 3990985 commit c339189
Show file tree
Hide file tree
Showing 76 changed files with 11,199 additions and 11,332 deletions.
2 changes: 0 additions & 2 deletions doc/source/index.rst
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions doc/source/quickstart.rst
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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',)
...


Expand All @@ -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
Expand Down
8 changes: 1 addition & 7 deletions doc/source/ref/forms.rst
Expand Up @@ -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:
6 changes: 0 additions & 6 deletions doc/source/ref/users.rst

This file was deleted.

12 changes: 0 additions & 12 deletions doc/source/ref/views.rst

This file was deleted.

8 changes: 4 additions & 4 deletions doc/source/topics/tutorial.rst
Expand Up @@ -86,16 +86,16 @@ 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):
name = _("Visualizations")
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::
Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions horizon/api/glance.py
Expand Up @@ -37,9 +37,9 @@
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):
Expand Down
77 changes: 16 additions & 61 deletions horizon/api/keystone.py
Expand Up @@ -29,6 +29,8 @@
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

Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -161,51 +148,19 @@ 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
the service catalog for the given tenant.
'''
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:
Expand Down
12 changes: 6 additions & 6 deletions horizon/api/nova.py
Expand Up @@ -192,24 +192,24 @@ def __unicode__(self):

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

Expand Down
4 changes: 2 additions & 2 deletions horizon/api/swift.py
Expand Up @@ -44,8 +44,8 @@ def authenticate(self):
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)


Expand Down
43 changes: 14 additions & 29 deletions horizon/base.py
Expand Up @@ -39,8 +39,7 @@
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__)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -249,10 +243,8 @@ def _decorated_urls(self):
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -360,18 +352,13 @@ class Syspanel(horizon.Dashboard):
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
Expand Down Expand Up @@ -491,10 +478,8 @@ def _decorated_urls(self):
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
Expand Down

0 comments on commit c339189

Please sign in to comment.