Initial commit.
This commit is contained in:
commit
9962375e0c
|
@ -0,0 +1,7 @@
|
|||
*.pyc
|
||||
*.egg
|
||||
*.egg-info
|
||||
.DS_STORE
|
||||
doc/build
|
||||
build
|
||||
dist
|
|
@ -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
|
|
@ -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
|
|
@ -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:])
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="." method="POST">{{ csrf_token }}
|
||||
{{ form.as_p }}
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -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'])
|
|
@ -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"))
|
||||
)
|
|
@ -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<tenant_id>[^/]+)/$', 'switch', name='switch_tenants')
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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'
|
||||
)
|
Loading…
Reference in New Issue