diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..31d405a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.pyc
+*.egg
+*.egg-info
+.DS_STORE
+doc/build
+build
+dist
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e4ab673
--- /dev/null
+++ b/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.
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..c9bb756
--- /dev/null
+++ b/README.rst
@@ -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.
diff --git a/openstack_auth/__init__.py b/openstack_auth/__init__.py
new file mode 100644
index 0000000..70d73c2
--- /dev/null
+++ b/openstack_auth/__init__.py
@@ -0,0 +1,2 @@
+# following PEP 386
+__version__ = "1.0"
diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py
new file mode 100644
index 0000000..4bbec4a
--- /dev/null
+++ b/openstack_auth/backend.py
@@ -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
diff --git a/openstack_auth/exceptions.py b/openstack_auth/exceptions.py
new file mode 100644
index 0000000..bc8c539
--- /dev/null
+++ b/openstack_auth/exceptions.py
@@ -0,0 +1,3 @@
+class KeystoneAuthException(Exception):
+ """ Generic error class to identify and catch our own errors. """
+ pass
diff --git a/openstack_auth/forms.py b/openstack_auth/forms.py
new file mode 100644
index 0000000..f7a24fe
--- /dev/null
+++ b/openstack_auth/forms.py
@@ -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
diff --git a/openstack_auth/tests/__init__.py b/openstack_auth/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/openstack_auth/tests/data.py b/openstack_auth/tests/data.py
new file mode 100644
index 0000000..2b6a480
--- /dev/null
+++ b/openstack_auth/tests/data.py
@@ -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
diff --git a/openstack_auth/tests/models.py b/openstack_auth/tests/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/openstack_auth/tests/run_tests.py b/openstack_auth/tests/run_tests.py
new file mode 100644
index 0000000..4f27c5a
--- /dev/null
+++ b/openstack_auth/tests/run_tests.py
@@ -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:])
diff --git a/openstack_auth/tests/templates/auth/blank.html b/openstack_auth/tests/templates/auth/blank.html
new file mode 100644
index 0000000..e69de29
diff --git a/openstack_auth/tests/templates/auth/login.html b/openstack_auth/tests/templates/auth/login.html
new file mode 100644
index 0000000..eac9a9c
--- /dev/null
+++ b/openstack_auth/tests/templates/auth/login.html
@@ -0,0 +1,11 @@
+
+
+
+ Login
+
+
+
+
+
\ No newline at end of file
diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py
new file mode 100644
index 0000000..dc2c289
--- /dev/null
+++ b/openstack_auth/tests/tests.py
@@ -0,0 +1,202 @@
+from django import test
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+from keystoneclient import exceptions as keystone_exceptions
+from keystoneclient.v2_0 import client
+
+import mox
+
+from .data import generate_test_data
+
+
+class OpenStackAuthTests(test.TestCase):
+ def setUp(self):
+ super(OpenStackAuthTests, self).setUp()
+ self.mox = mox.Mox()
+ self.data = generate_test_data()
+ endpoint = settings.OPENSTACK_KEYSTONE_URL
+ self.keystone_client = client.Client(endpoint=endpoint)
+ self.keystone_client.service_catalog = self.data.service_catalog
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.VerifyAll()
+
+ def test_login(self):
+ tenants = [self.data.tenant_one, self.data.tenant_two]
+ user = self.data.user
+ sc = self.data.service_catalog
+
+ form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
+ 'password': user.password,
+ 'username': user.name}
+
+ self.mox.StubOutWithMock(client, "Client")
+ self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
+ self.mox.StubOutWithMock(self.keystone_client.tokens, "authenticate")
+
+ client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
+ password=user.password,
+ username=user.name,
+ tenant_id=None).AndReturn(self.keystone_client)
+ self.keystone_client.tenants.list().AndReturn(tenants)
+ self.keystone_client.tokens.authenticate(tenant_id=tenants[1].id,
+ token=sc.get_token()['id'],
+ username=user.name) \
+ .AndReturn(self.data.scoped_token)
+
+ self.mox.ReplayAll()
+
+ url = reverse('login')
+
+ # GET the page to set the test cookie.
+ response = self.client.get(url, form_data)
+ self.assertEqual(response.status_code, 200)
+
+ # POST to the page to log in.
+ response = self.client.post(url, form_data)
+ self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
+
+ def test_no_tenants(self):
+ user = self.data.user
+
+ form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
+ 'password': user.password,
+ 'username': user.name}
+
+ self.mox.StubOutWithMock(client, "Client")
+ self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
+
+ client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
+ password=user.password,
+ username=user.name,
+ tenant_id=None).AndReturn(self.keystone_client)
+ self.keystone_client.tenants.list().AndReturn([])
+
+ self.mox.ReplayAll()
+
+ url = reverse('login')
+
+ # GET the page to set the test cookie.
+ response = self.client.get(url, form_data)
+ self.assertEqual(response.status_code, 200)
+
+ # POST to the page to log in.
+ response = self.client.post(url, form_data)
+ self.assertTemplateUsed(response, 'auth/login.html')
+ self.assertContains(response,
+ 'You are not authorized for any projects.')
+
+ def test_invalid_credentials(self):
+ user = self.data.user
+
+ form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
+ 'password': "invalid",
+ 'username': user.name}
+
+ self.mox.StubOutWithMock(client, "Client")
+
+ exc = keystone_exceptions.Unauthorized(401)
+ client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
+ password="invalid",
+ username=user.name,
+ tenant_id=None).AndRaise(exc)
+
+ self.mox.ReplayAll()
+
+ url = reverse('login')
+
+ # GET the page to set the test cookie.
+ response = self.client.get(url, form_data)
+ self.assertEqual(response.status_code, 200)
+
+ # POST to the page to log in.
+ response = self.client.post(url, form_data)
+ self.assertTemplateUsed(response, 'auth/login.html')
+ self.assertContains(response, "Invalid user name or password.")
+
+ def test_exception(self):
+ user = self.data.user
+
+ form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
+ 'password': user.password,
+ 'username': user.name}
+
+ self.mox.StubOutWithMock(client, "Client")
+
+ exc = keystone_exceptions.ClientException(500)
+ client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
+ password=user.password,
+ username=user.name,
+ tenant_id=None).AndRaise(exc)
+
+ self.mox.ReplayAll()
+
+ url = reverse('login')
+
+ # GET the page to set the test cookie.
+ response = self.client.get(url, form_data)
+ self.assertEqual(response.status_code, 200)
+
+ # POST to the page to log in.
+ response = self.client.post(url, form_data)
+
+ self.assertTemplateUsed(response, 'auth/login.html')
+ self.assertContains(response,
+ ("An error occurred authenticating. Please try "
+ "again later."))
+
+ def test_switch(self):
+ tenant = self.data.tenant_two
+ tenants = [self.data.tenant_one, self.data.tenant_two]
+ user = self.data.user
+ scoped = self.data.scoped_token
+ sc = self.data.service_catalog
+
+ form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
+ 'username': user.name,
+ 'password': user.password}
+
+ self.mox.StubOutWithMock(client, "Client")
+ self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
+ self.mox.StubOutWithMock(self.keystone_client.tokens, "authenticate")
+
+ client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
+ password=user.password,
+ username=user.name,
+ tenant_id=None).AndReturn(self.keystone_client)
+ self.keystone_client.tenants.list().AndReturn(tenants)
+ self.keystone_client.tokens.authenticate(tenant_id=tenants[1].id,
+ token=sc.get_token()['id'],
+ username=user.name) \
+ .AndReturn(scoped)
+
+ client.Client(endpoint=settings.OPENSTACK_KEYSTONE_URL) \
+ .AndReturn(self.keystone_client)
+
+ self.keystone_client.tokens.authenticate(tenant_id=tenant.id,
+ token=sc.get_token()['id']) \
+ .AndReturn(scoped)
+
+ self.mox.ReplayAll()
+
+ url = reverse('login')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(url, form_data)
+ self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
+
+ url = reverse('switch_tenants', args=[tenant.id])
+
+ scoped.tenant['id'] = self.data.tenant_two._info
+ sc.catalog['token']['id'] = self.data.tenant_two.id
+
+ form_data['tenant_id'] = tenant.id
+ response = self.client.get(url, form_data)
+
+ self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
+ self.assertEqual(self.client.session['tenant_id'],
+ scoped.tenant['id'])
diff --git a/openstack_auth/tests/urls.py b/openstack_auth/tests/urls.py
new file mode 100644
index 0000000..e04d067
--- /dev/null
+++ b/openstack_auth/tests/urls.py
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import patterns, include, url
+from django.views.generic import TemplateView
+
+from openstack_auth.utils import patch_middleware_get_user
+
+
+patch_middleware_get_user()
+
+
+urlpatterns = patterns('',
+ url(r"", include('openstack_auth.urls')),
+ url(r"^$", TemplateView.as_view(template_name="auth/blank.html"))
+)
diff --git a/openstack_auth/urls.py b/openstack_auth/urls.py
new file mode 100644
index 0000000..5e81a94
--- /dev/null
+++ b/openstack_auth/urls.py
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import patterns, url
+
+from .utils import patch_middleware_get_user
+
+
+patch_middleware_get_user()
+
+
+urlpatterns = patterns('openstack_auth.views',
+ url(r"^login/$", "login", name='login'),
+ url(r"^logout/$", 'logout', name='logout'),
+ url(r'^switch/(?P[^/]+)/$', 'switch', name='switch_tenants')
+)
diff --git a/openstack_auth/user.py b/openstack_auth/user.py
new file mode 100644
index 0000000..ace98c8
--- /dev/null
+++ b/openstack_auth/user.py
@@ -0,0 +1,124 @@
+from django.contrib.auth.models import AnonymousUser
+
+from keystoneclient.v2_0 import client as keystone_client
+from keystoneclient import exceptions as keystone_exceptions
+
+from .utils import check_token_expiration
+
+
+def set_session_from_user(request, user):
+ request.session['serviceCatalog'] = user.service_catalog
+ request.session['tenant'] = user.tenant_name
+ request.session['tenant_id'] = user.tenant_id
+ request.session['token'] = user.token._info
+ request.session['username'] = user.username
+ request.session['user_id'] = user.id
+ request.session['roles'] = user.roles
+ request.session['region_endpoint'] = user.endpoint
+
+
+def create_user_from_token(request, token, endpoint):
+ return User(id=token.user['id'],
+ token=token,
+ user=token.user['name'],
+ tenant_id=token.tenant['id'],
+ tenant_name=token.tenant['name'],
+ enabled=True,
+ service_catalog=token.serviceCatalog,
+ roles=token.user['roles'],
+ endpoint=endpoint)
+
+
+class User(AnonymousUser):
+ """ A User class with some extra special sauce for Keystone.
+
+ In addition to the standard Django user attributes, this class also has
+ the following:
+
+ .. attribute:: token
+
+ The Keystone token object associated with the current user/tenant.
+
+ .. 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.
+ """
+ def __init__(self, id=None, token=None, user=None, tenant_id=None,
+ service_catalog=None, tenant_name=None, roles=None,
+ authorized_tenants=None, endpoint=None, enabled=False):
+ 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.endpoint = endpoint
+ self.enabled = enabled
+ self._authorized_tenants = authorized_tenants
+
+ def __unicode__(self):
+ return self.username
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self.username)
+
+ def is_authenticated(self):
+ """ Checks for a valid token that has not yet expired. """
+ return self.token is not None and check_token_expiration(self.token)
+
+ @property
+ def is_active(self):
+ return self.enabled
+
+ @property
+ def is_superuser(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
+
+ @property
+ def authorized_tenants(self):
+ """ Returns a memoized list of tenants this user may access. """
+ if self.is_authenticated() and self._authorized_tenants is None:
+ endpoint = self.endpoint
+ token = self.token
+ try:
+ client = keystone_client.Client(username=self.username,
+ auth_url=endpoint,
+ token=token.id)
+ authd = client.tenants.list()
+ except keystone_exceptions.ClientException:
+ authd = []
+ self._authorized_tenants = authd
+ return self._authorized_tenants or []
+
+ @authorized_tenants.setter
+ def authorized_tenants(self, tenant_list):
+ self._authorized_tenants = tenant_list
+
+ def save(*args, **kwargs):
+ # Presume we can't write to Keystone.
+ pass
+
+ def delete(*args, **kwargs):
+ # Presume we can't write to Keystone.
+ pass
diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py
new file mode 100644
index 0000000..25bc093
--- /dev/null
+++ b/openstack_auth/utils.py
@@ -0,0 +1,55 @@
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.auth import middleware
+from django.utils import timezone
+from django.utils.dateparse import parse_datetime
+
+
+"""
+We need the request object to get the user, so we'll slightly modify the
+existing django.contrib.auth.get_user method. To do so we update the
+auth middleware to point to our overridden method.
+
+Calling the "patch_middleware_get_user" method somewhere like our urls.py
+file takes care of hooking it in appropriately.
+"""
+
+
+def middleware_get_user(request):
+ if not hasattr(request, '_cached_user'):
+ request._cached_user = get_user(request)
+ return request._cached_user
+
+
+def get_user(request):
+ try:
+ user_id = request.session[auth.SESSION_KEY]
+ backend_path = request.session[auth.BACKEND_SESSION_KEY]
+ backend = auth.load_backend(backend_path)
+ backend.request = request
+ user = backend.get_user(user_id) or AnonymousUser()
+ except KeyError:
+ user = AnonymousUser()
+ return user
+
+
+def patch_middleware_get_user():
+ middleware.get_user = middleware_get_user
+ auth.get_user = get_user
+
+
+""" End Monkey-Patching. """
+
+
+def check_token_expiration(token):
+ expiration = parse_datetime(token.expires)
+ if settings.USE_TZ and timezone.is_naive(expiration):
+ # Presumes that the Keystone is using UTC.
+ expiration = timezone.make_aware(expiration, timezone.utc)
+ # In case we get an unparseable expiration timestamp, return False
+ # so you can't have a "forever" token just by breaking the expires param.
+ if expiration:
+ return expiration > timezone.now()
+ else:
+ return False
diff --git a/openstack_auth/views.py b/openstack_auth/views.py
new file mode 100644
index 0000000..8bc9b6b
--- /dev/null
+++ b/openstack_auth/views.py
@@ -0,0 +1,75 @@
+import logging
+
+from django import shortcuts
+from django.conf import settings
+from django.contrib.auth.views import (login as django_login,
+ logout_then_login as django_logout)
+from django.contrib.auth.decorators import login_required
+from django.views.decorators.debug import sensitive_post_parameters
+from django.utils.functional import curry
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+
+from keystoneclient.v2_0 import client as keystone_client
+
+from .forms import Login
+from .user import set_session_from_user, create_user_from_token
+
+
+LOG = logging.getLogger(__name__)
+
+
+@sensitive_post_parameters()
+@csrf_protect
+@never_cache
+def login(request):
+ # Get our initial region for the form.
+ initial = {}
+ current_region = request.session.get('region_endpoint', None)
+ requested_region = 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})
+
+ if request.method == "POST":
+ form = curry(Login, request)
+ else:
+ form = curry(Login, initial=initial)
+
+ if request.is_ajax():
+ template_name = 'auth/_login.html'
+ extra_context = {'hide': True}
+ else:
+ template_name = 'auth/login.html'
+ extra_context = {}
+
+ res = django_login(request,
+ template_name=template_name,
+ authentication_form=form,
+ extra_context=extra_context)
+ # Set the session data here because django's session key rotation
+ # will erase it if we set it earlier.
+ if request.user.is_authenticated():
+ set_session_from_user(request, request.user)
+ region = request.user.endpoint
+ region_name = dict(Login.get_region_choices()).get(region)
+ request.session['region_endpoint'] = region
+ request.session['region_name'] = region_name
+ return res
+
+
+def logout(request):
+ return django_logout(request)
+
+
+@login_required
+def switch(request, tenant_id):
+ LOG.debug('Switching to tenant %s for user "%s".'
+ % (tenant_id, request.user.username))
+ endpoint = request.user.endpoint
+ client = keystone_client.Client(endpoint=endpoint)
+ token = client.tokens.authenticate(tenant_id=tenant_id,
+ token=request.user.token.id)
+ user = create_user_from_token(request, token, endpoint)
+ set_session_from_user(request, user)
+ return shortcuts.redirect(settings.LOGIN_REDIRECT_URL)
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..99fc94f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,52 @@
+import os
+import re
+import codecs
+from setuptools import setup, find_packages
+
+
+def read(*parts):
+ return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()
+
+
+def find_version(*file_paths):
+ version_file = read(*file_paths)
+ version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
+ version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError("Unable to find version string.")
+
+
+setup(
+ name="django_openstack_auth",
+ version=find_version("openstack_auth", "__init__.py"),
+ url='http://django_openstack_auth.readthedocs.org/',
+ license='BSD',
+ description=("A Django authentication backend for use with the "
+ "OpenStack Keystone Identity backend."),
+ long_description=read('README.rst'),
+ author='Gabriel Hurley',
+ author_email='gabriel@strikeawe.com',
+ packages=find_packages(),
+ include_package_data=True,
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Internet :: WWW/HTTP',
+ ],
+ zip_safe=False,
+ install_requires=[
+ 'django >= 1.4',
+ 'python-keystoneclient'
+ ],
+ tests_require=[
+ 'mox',
+ ],
+ test_suite='openstack_auth.tests.run_tests.run'
+)