Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 9962375e0c5209ff82b7d6b5fda92bfec8dc4e1c @gabrielhurley committed Jul 3, 2012
@@ -0,0 +1,7 @@
+*.pyc
+*.egg
+*.egg-info
+.DS_STORE
+doc/build
+build
+dist
30 LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2012, Gabriel Hurley
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,31 @@
+=====================
+Django OpenStack Auth
+=====================
+
+Django OpenStack Auth is a pluggable Django authentication backend that
+works with Django's ``contrib.auth`` framework to authenticate a user against
+OpenStack's Keystone Identity API.
+
+The current version is designed to work with the Keystone V2 API.
+
+Installation
+============
+
+Installing is quick and easy:
+
+ #. Run ``pip install django_openstack_auth``.
+
+ #. Add ``openstack_auth`` to ``settings.INSTALLED_APPS``.
+
+ #. Add ``'keystone_auth.backend.KeystoneBackend'`` to your
+ ``settings.AUTHENTICATION_BACKENDS``, e.g.::
+
+ AUTHENTICATION_BACKENDS = ('keystone_auth.backend.KeystoneBackend',)
+
+ #. Configure your API endpoint(s) in ``settings.py``::
+
+ OPENSTACK_KEYSTONE_URL = "http://example.com:5000/v2.0"
+
+ #. Include ``'keystone_auth.urls'`` somewhere in your ``urls.py`` file.
+
+ #. Use it as you would any other Django auth backend.
@@ -0,0 +1,2 @@
+# following PEP 386
+__version__ = "1.0"
@@ -0,0 +1,92 @@
+""" Module defining the Django auth backend class for the Keystone API. """
+
+import logging
+
+from django.utils.translation import ugettext as _
+
+from keystoneclient.v2_0 import client as keystone_client
+from keystoneclient import exceptions as keystone_exceptions
+from keystoneclient.v2_0.tokens import Token, TokenManager
+
+from .exceptions import KeystoneAuthException
+from .user import create_user_from_token
+
+
+LOG = logging.getLogger(__name__)
+
+
+KEYSTONE_CLIENT_ATTR = "_keystoneclient"
+
+
+class KeystoneBackend(object):
+ def get_user(self, user_id):
+ if user_id == self.request.session["user_id"]:
+ token = Token(TokenManager(None),
+ self.request.session['token'],
+ loaded=True)
+ endpoint = self.request.session['region_endpoint']
+ return create_user_from_token(self.request, token, endpoint)
+ else:
+ return None
+
+ def authenticate(self, request=None, username=None, password=None,
+ tenant=None, auth_url=None):
+ """ Authenticates a user via the Keystone Identity API. """
+ LOG.debug('Beginning user authentication for user "%s".' % username)
+
+ try:
+ client = keystone_client.Client(username=username,
+ password=password,
+ tenant_id=tenant,
+ auth_url=auth_url)
+ unscoped_token_data = {"token": client.service_catalog.get_token()}
+ unscoped_token = Token(TokenManager(None),
+ unscoped_token_data,
+ loaded=True)
+ except keystone_exceptions.Unauthorized:
+ msg = _('Invalid user name or password.')
+ raise KeystoneAuthException(msg)
+ except keystone_exceptions.ClientException:
+ msg = _("An error occurred authenticating. "
+ "Please try again later.")
+ raise KeystoneAuthException(msg)
+
+ # FIXME: Log in to default tenant when the Keystone API returns it...
+ # For now we list all the user's tenants and iterate through.
+ try:
+ tenants = client.tenants.list()
+ except keystone_exceptions.ClientException:
+ msg = _('Unable to retrieve authorized projects.')
+ raise KeystoneAuthException(msg)
+
+ # Abort if there are no tenants for this user
+ if not tenants:
+ msg = _('You are not authorized for any projects.')
+ raise KeystoneAuthException(msg)
+
+ while tenants:
+ tenant = tenants.pop()
+ try:
+ token = client.tokens.authenticate(username=username,
+ token=unscoped_token.id,
+ tenant_id=tenant.id)
+ break
+ except keystone_exceptions.ClientException:
+ token = None
+
+ if token is None:
+ msg = _("Unable to authenticate to any available projects.")
+ raise KeystoneAuthException(msg)
+
+ # If we made it here we succeeded. Create our User!
+ user = create_user_from_token(request, token, client.management_url)
+
+ if request is not None:
+ request.session['unscoped_token'] = unscoped_token.id
+ request.user = user
+
+ # Support client caching to save on auth calls.
+ setattr(request, KEYSTONE_CLIENT_ATTR, client)
+
+ LOG.debug('Authentication completed for user "%s".' % username)
+ return user
@@ -0,0 +1,3 @@
+class KeystoneAuthException(Exception):
+ """ Generic error class to identify and catch our own errors. """
+ pass
@@ -0,0 +1,59 @@
+from django import forms
+from django.conf import settings
+from django.contrib.auth import authenticate
+from django.contrib.auth.forms import AuthenticationForm
+from django.utils.translation import ugettext as _
+from django.views.decorators.debug import sensitive_variables
+
+from .exceptions import KeystoneAuthException
+
+
+class Login(AuthenticationForm):
+ """ Form used for logging in a user.
+
+ Handles authentication with Keystone, choosing a tenant, and fetching
+ a scoped token token for that tenant.
+ """
+ region = forms.ChoiceField(label=_("Region"), required=False)
+ username = forms.CharField(label=_("User Name"))
+ password = forms.CharField(label=_("Password"),
+ widget=forms.PasswordInput(render_value=False))
+ tenant = forms.CharField(required=False, widget=forms.HiddenInput())
+
+ def __init__(self, *args, **kwargs):
+ super(Login, self).__init__(*args, **kwargs)
+ self.fields['region'].choices = self.get_region_choices()
+ if len(self.fields['region'].choices) == 1:
+ self.fields['region'].initial = self.fields['region'].choices[0][0]
+ self.fields['region'].widget = forms.widgets.HiddenInput()
+
+ @staticmethod
+ def get_region_choices():
+ default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region")
+ return getattr(settings, 'AVAILABLE_REGIONS', [default_region])
+
+ @sensitive_variables()
+ def clean(self):
+ username = self.cleaned_data.get('username')
+ password = self.cleaned_data.get('password')
+ region = self.cleaned_data.get('region')
+ tenant = self.cleaned_data.get('tenant')
+
+ if not tenant:
+ tenant = None
+
+ if not (username and password):
+ # Don't authenticate, just let the other validators handle it.
+ return self.cleaned_data
+
+ try:
+ self.user_cache = authenticate(request=self.request,
+ username=username,
+ password=password,
+ tenant=tenant,
+ auth_url=region)
+ except KeystoneAuthException as exc:
+ self.request.session.flush()
+ raise forms.ValidationError(exc)
+ self.check_for_test_cookie()
+ return self.cleaned_data
No changes.
@@ -0,0 +1,112 @@
+import uuid
+
+from datetime import timedelta
+
+from django.utils import datetime_safe
+
+from keystoneclient.v2_0.roles import Role, RoleManager
+from keystoneclient.v2_0.tenants import Tenant, TenantManager
+from keystoneclient.v2_0.tokens import Token, TokenManager
+from keystoneclient.v2_0.users import User, UserManager
+from keystoneclient.service_catalog import ServiceCatalog
+
+
+class TestDataContainer(object):
+ """ Arbitrary holder for test data in an object-oriented fashion. """
+ pass
+
+
+def generate_test_data():
+ ''' Builds a set of test_data data as returned by Keystone. '''
+ test_data = TestDataContainer()
+
+ keystone_service = {
+ 'type': 'identity',
+ 'name': 'keystone',
+ 'endpoints_links': [],
+ 'endpoints': [
+ {
+ 'region': 'RegionOne',
+ 'adminURL': 'http://admin.localhost:35357/v2.0',
+ 'internalURL': 'http://internal.localhost:5000/v2.0',
+ 'publicURL': 'http://public.localhost:5000/v2.0'
+ }
+ ]
+ }
+
+ # Users
+ user_dict = {'id': uuid.uuid4().hex,
+ 'name': 'gabriel',
+ 'email': 'gabriel@example.com',
+ 'password': 'swordfish',
+ 'token': '',
+ 'enabled': True}
+ test_data.user = User(UserManager(None), user_dict, loaded=True)
+
+ # Tenants
+ tenant_dict_1 = {'id': uuid.uuid4().hex,
+ 'name': 'tenant_one',
+ 'description': '',
+ 'enabled': True}
+ tenant_dict_2 = {'id': uuid.uuid4().hex,
+ 'name': '',
+ 'description': '',
+ 'enabled': False}
+ test_data.tenant_one = Tenant(TenantManager(None),
+ tenant_dict_1,
+ loaded=True)
+ test_data.tenant_two = Tenant(TenantManager(None),
+ tenant_dict_2,
+ loaded=True)
+
+ # Roles
+ role_dict = {'id': uuid.uuid4().hex,
+ 'name': 'Member'}
+ test_data.role = Role(RoleManager, role_dict)
+
+ # Tokens
+ tomorrow = datetime_safe.datetime.now() + timedelta(days=1)
+ expiration = datetime_safe.datetime.isoformat(tomorrow)
+
+ scoped_token_dict = {
+ 'token': {
+ 'id': uuid.uuid4().hex,
+ 'expires': expiration,
+ 'tenant': tenant_dict_1,
+ 'tenants': [tenant_dict_1, tenant_dict_2]},
+ 'user': {
+ 'id': user_dict['id'],
+ 'name': user_dict['name'],
+ 'roles': [role_dict]},
+ 'serviceCatalog': [keystone_service]
+ }
+ test_data.scoped_token = Token(TokenManager(None),
+ scoped_token_dict,
+ loaded=True)
+
+ unscoped_token_dict = {
+ 'token': {
+ 'id': uuid.uuid4().hex,
+ 'expires': expiration},
+ 'user': {
+ 'id': user_dict['id'],
+ 'name': user_dict['name'],
+ 'roles': [role_dict]},
+ 'serviceCatalog': [keystone_service]
+ }
+ test_data.unscoped_token = Token(TokenManager(None),
+ unscoped_token_dict,
+ loaded=True)
+
+ # Service Catalog
+ test_data.service_catalog = ServiceCatalog({
+ 'serviceCatalog': [keystone_service],
+ 'token': {
+ 'id': scoped_token_dict['token']['id'],
+ 'expires': scoped_token_dict['token']['expires'],
+ 'user_id': user_dict['id'],
+ 'tenant_id': tenant_dict_1['id']
+ }
+ })
+
+ return test_data
No changes.
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+import os
+import sys
+
+from django.conf import settings
+
+if not settings.configured:
+ settings.configure(
+ DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}},
+ INSTALLED_APPS=[
+ 'django',
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth',
+ 'django.contrib.sessions',
+ 'openstack_auth',
+ 'openstack_auth.tests'
+ ],
+ MIDDLEWARE_CLASSES=[
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware'
+ ],
+ AUTHENTICATION_BACKENDS=['openstack_auth.backend.KeystoneBackend'],
+ OPENSTACK_KEYSTONE_URL="http://localhost:5000/v2.0",
+ ROOT_URLCONF='openstack_auth.tests.urls',
+ LOGIN_REDIRECT_URL='/'
+ )
+
+from django.test.simple import DjangoTestSuiteRunner
+
+
+def run(*test_args):
+ if not test_args:
+ test_args = ['tests']
+ parent = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "..",
+ "..",
+ )
+ sys.path.insert(0, parent)
+ failures = DjangoTestSuiteRunner().run_tests(test_args)
+ sys.exit(failures)
+
+
+if __name__ == '__main__':
+ run(*sys.argv[1:])
Oops, something went wrong.

0 comments on commit 9962375

Please sign in to comment.