-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9962375
Showing
20 changed files
with
928 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
*.pyc | ||
*.egg | ||
*.egg-info | ||
.DS_STORE | ||
doc/build | ||
build | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# following PEP 386 | ||
__version__ = "1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
class KeystoneAuthException(Exception): | ||
""" Generic error class to identify and catch our own errors. """ | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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': '[email protected]', | ||
'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 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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:]) |
Empty file.
Oops, something went wrong.