Add capability for Keystone V3 Authentication.
For multi-domain model, set OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT to True and the login form will prompt the user for Domain name. For single-domain model, set OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT to False. The application will use the name of the default domain in OPENSTACK_KEYSTONE_DEFAULT_DOMAIN to login. Cleanup the unused Tenant field in the login form. Implements blueprint login-domain-support
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
""" Module defining the Django auth backend class for the Keystone API. """
|
""" Module defining the Django auth backend class for the Keystone API. """
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as keystone_client
|
|
||||||
from keystoneclient import exceptions as keystone_exceptions
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
from keystoneclient.v2_0.tokens import Token, TokenManager
|
|
||||||
|
|
||||||
from .exceptions import KeystoneAuthException
|
from .exceptions import KeystoneAuthException
|
||||||
from .user import create_user_from_token
|
from .user import create_user_from_token
|
||||||
from .utils import check_token_expiration, is_ans1_token
|
from .user import Token
|
||||||
|
from .utils import check_token_expiration
|
||||||
|
from .utils import get_keystone_client
|
||||||
|
from .utils import get_keystone_version
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -22,11 +22,12 @@ KEYSTONE_CLIENT_ATTR = "_keystoneclient"
|
|||||||
|
|
||||||
|
|
||||||
class KeystoneBackend(object):
|
class KeystoneBackend(object):
|
||||||
|
"""Django authentication backend class for use with
|
||||||
|
``django.contrib.auth``.
|
||||||
"""
|
"""
|
||||||
Django authentication backend class for use with ``django.contrib.auth``.
|
|
||||||
"""
|
def check_auth_expiry(self, auth_ref):
|
||||||
def check_auth_expiry(self, token):
|
if not check_token_expiration(auth_ref):
|
||||||
if not check_token_expiration(token):
|
|
||||||
msg = _("The authentication token issued by the Identity service "
|
msg = _("The authentication token issued by the Identity service "
|
||||||
"has expired.")
|
"has expired.")
|
||||||
LOG.warning("The authentication token issued by the Identity "
|
LOG.warning("The authentication token issued by the Identity "
|
||||||
@@ -37,41 +38,41 @@ class KeystoneBackend(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_user(self, user_id):
|
def get_user(self, user_id):
|
||||||
"""
|
"""Returns the current user (if authenticated) based on the user ID
|
||||||
Returns the current user (if authenticated) based on the user ID
|
|
||||||
and session data.
|
and session data.
|
||||||
|
|
||||||
Note: this required monkey-patching the ``contrib.auth`` middleware
|
Note: this required monkey-patching the ``contrib.auth`` middleware
|
||||||
to make the ``request`` object available to the auth backend class.
|
to make the ``request`` object available to the auth backend class.
|
||||||
"""
|
"""
|
||||||
if user_id == self.request.session["user_id"]:
|
if user_id == self.request.session["user_id"]:
|
||||||
token = Token(TokenManager(None),
|
token = self.request.session['token']
|
||||||
self.request.session['token'],
|
|
||||||
loaded=True)
|
|
||||||
endpoint = self.request.session['region_endpoint']
|
endpoint = self.request.session['region_endpoint']
|
||||||
services_region = self.request.session['services_region']
|
services_region = self.request.session['services_region']
|
||||||
return create_user_from_token(self.request, token, endpoint,
|
user = create_user_from_token(self.request, token, endpoint,
|
||||||
services_region)
|
services_region)
|
||||||
|
return user
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def authenticate(self, request=None, username=None, password=None,
|
def authenticate(self, request=None, username=None, password=None,
|
||||||
tenant=None, auth_url=None):
|
user_domain_name=None, auth_url=None):
|
||||||
""" Authenticates a user via the Keystone Identity API. """
|
"""Authenticates a user via the Keystone Identity API. """
|
||||||
LOG.debug('Beginning user authentication for user "%s".' % username)
|
LOG.debug('Beginning user authentication for user "%s".' % username)
|
||||||
|
|
||||||
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
||||||
|
|
||||||
|
keystone_client = get_keystone_client()
|
||||||
try:
|
try:
|
||||||
client = keystone_client.Client(username=username,
|
client = keystone_client.Client(
|
||||||
password=password,
|
user_domain_name=user_domain_name,
|
||||||
tenant_id=tenant,
|
username=username,
|
||||||
auth_url=auth_url,
|
password=password,
|
||||||
insecure=insecure)
|
auth_url=auth_url,
|
||||||
unscoped_token_data = {"token": client.service_catalog.get_token()}
|
insecure=insecure,
|
||||||
unscoped_token = Token(TokenManager(None),
|
debug=settings.DEBUG)
|
||||||
unscoped_token_data,
|
|
||||||
loaded=True)
|
unscoped_auth_ref = client.auth_ref
|
||||||
|
unscoped_token = Token(auth_ref=unscoped_auth_ref)
|
||||||
except (keystone_exceptions.Unauthorized,
|
except (keystone_exceptions.Unauthorized,
|
||||||
keystone_exceptions.Forbidden,
|
keystone_exceptions.Forbidden,
|
||||||
keystone_exceptions.NotFound) as exc:
|
keystone_exceptions.NotFound) as exc:
|
||||||
@@ -85,54 +86,60 @@ class KeystoneBackend(object):
|
|||||||
LOG.debug(exc.message)
|
LOG.debug(exc.message)
|
||||||
raise KeystoneAuthException(msg)
|
raise KeystoneAuthException(msg)
|
||||||
|
|
||||||
# Check expiry for our unscoped token.
|
# Check expiry for our unscoped auth ref.
|
||||||
self.check_auth_expiry(unscoped_token)
|
self.check_auth_expiry(unscoped_auth_ref)
|
||||||
|
|
||||||
# FIXME: Log in to default tenant when the Keystone API returns it...
|
# Check if token is automatically scoped to default_project
|
||||||
# For now we list all the user's tenants and iterate through.
|
if unscoped_auth_ref.project_scoped:
|
||||||
try:
|
auth_ref = unscoped_auth_ref
|
||||||
tenants = client.tenants.list()
|
else:
|
||||||
except (keystone_exceptions.ClientException,
|
# For now we list all the user's projects and iterate through.
|
||||||
keystone_exceptions.AuthorizationFailure):
|
|
||||||
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:
|
try:
|
||||||
client = keystone_client.Client(tenant_id=tenant.id,
|
if get_keystone_version() < 3:
|
||||||
token=unscoped_token.id,
|
projects = client.tenants.list()
|
||||||
auth_url=auth_url,
|
else:
|
||||||
insecure=insecure)
|
client.management_url = auth_url
|
||||||
token = client.tokens.authenticate(username=username,
|
projects = client.projects.list(
|
||||||
token=unscoped_token.id,
|
user=unscoped_auth_ref.user_id)
|
||||||
tenant_id=tenant.id)
|
|
||||||
break
|
|
||||||
except (keystone_exceptions.ClientException,
|
except (keystone_exceptions.ClientException,
|
||||||
keystone_exceptions.AuthorizationFailure):
|
keystone_exceptions.AuthorizationFailure) as exc:
|
||||||
token = None
|
msg = _('Unable to retrieve authorized projects.')
|
||||||
|
raise KeystoneAuthException(msg)
|
||||||
|
|
||||||
if token is None:
|
# Abort if there are no projects for this user
|
||||||
msg = _("Unable to authenticate to any available projects.")
|
if not projects:
|
||||||
raise KeystoneAuthException(msg)
|
msg = _('You are not authorized for any projects.')
|
||||||
|
raise KeystoneAuthException(msg)
|
||||||
|
|
||||||
|
while projects:
|
||||||
|
project = projects.pop()
|
||||||
|
try:
|
||||||
|
client = keystone_client.Client(
|
||||||
|
tenant_id=project.id,
|
||||||
|
token=unscoped_auth_ref.auth_token,
|
||||||
|
auth_url=auth_url,
|
||||||
|
insecure=insecure,
|
||||||
|
debug=settings.DEBUG)
|
||||||
|
auth_ref = client.auth_ref
|
||||||
|
break
|
||||||
|
except (keystone_exceptions.ClientException,
|
||||||
|
keystone_exceptions.AuthorizationFailure):
|
||||||
|
auth_ref = None
|
||||||
|
|
||||||
|
if auth_ref is None:
|
||||||
|
msg = _("Unable to authenticate to any available"
|
||||||
|
" projects.")
|
||||||
|
raise KeystoneAuthException(msg)
|
||||||
|
|
||||||
# Check expiry for our new scoped token.
|
# Check expiry for our new scoped token.
|
||||||
self.check_auth_expiry(token)
|
self.check_auth_expiry(auth_ref)
|
||||||
|
|
||||||
# If we made it here we succeeded. Create our User!
|
# If we made it here we succeeded. Create our User!
|
||||||
user = create_user_from_token(request,
|
user = create_user_from_token(request,
|
||||||
token,
|
Token(auth_ref),
|
||||||
client.service_catalog.url_for())
|
client.service_catalog.url_for())
|
||||||
|
|
||||||
if request is not None:
|
if request is not None:
|
||||||
if is_ans1_token(unscoped_token.id):
|
|
||||||
hashed_token = hashlib.md5(unscoped_token.id).hexdigest()
|
|
||||||
unscoped_token._info['token']['id'] = hashed_token
|
|
||||||
request.session['unscoped_token'] = unscoped_token.id
|
request.session['unscoped_token'] = unscoped_token.id
|
||||||
request.user = user
|
request.user = user
|
||||||
|
|
||||||
@@ -143,15 +150,17 @@ class KeystoneBackend(object):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
def get_group_permissions(self, user, obj=None):
|
def get_group_permissions(self, user, obj=None):
|
||||||
""" Returns an empty set since Keystone doesn't support "groups". """
|
"""Returns an empty set since Keystone doesn't support "groups". """
|
||||||
|
# Keystone V3 added "groups". The Auth token response includes the
|
||||||
|
# roles from the user's Group assignment. It should be fine just
|
||||||
|
# returning an empty set here.
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def get_all_permissions(self, user, obj=None):
|
def get_all_permissions(self, user, obj=None):
|
||||||
"""
|
"""Returns a set of permission strings that this user has through
|
||||||
Returns a set of permission strings that this user has through his/her
|
his/her Keystone "roles".
|
||||||
Keystone "roles".
|
|
||||||
|
|
||||||
The permissions are returned as ``"openstack.{{ role.name }}"``.
|
The permissions are returned as ``"openstack.{{ role.name }}"``.
|
||||||
"""
|
"""
|
||||||
if user.is_anonymous() or obj is not None:
|
if user.is_anonymous() or obj is not None:
|
||||||
return set()
|
return set()
|
||||||
@@ -163,16 +172,15 @@ class KeystoneBackend(object):
|
|||||||
return role_perms | service_perms
|
return role_perms | service_perms
|
||||||
|
|
||||||
def has_perm(self, user, perm, obj=None):
|
def has_perm(self, user, perm, obj=None):
|
||||||
""" Returns True if the given user has the specified permission. """
|
"""Returns True if the given user has the specified permission. """
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return False
|
return False
|
||||||
return perm in self.get_all_permissions(user, obj)
|
return perm in self.get_all_permissions(user, obj)
|
||||||
|
|
||||||
def has_module_perms(self, user, app_label):
|
def has_module_perms(self, user, app_label):
|
||||||
"""
|
"""Returns True if user has any permissions in the given app_label.
|
||||||
Returns True if user has any permissions in the given app_label.
|
|
||||||
|
|
||||||
Currently this matches for the app_label ``"openstack"``.
|
Currently this matches for the app_label ``"openstack"``.
|
||||||
"""
|
"""
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -16,8 +16,18 @@ LOG = logging.getLogger(__name__)
|
|||||||
class Login(AuthenticationForm):
|
class Login(AuthenticationForm):
|
||||||
""" Form used for logging in a user.
|
""" Form used for logging in a user.
|
||||||
|
|
||||||
Handles authentication with Keystone, choosing a tenant, and fetching
|
Handles authentication with Keystone by providing the domain name, username
|
||||||
a scoped token token for that tenant.
|
and password. A scoped token is fetched after successful authentication.
|
||||||
|
|
||||||
|
A domain name is required if authenticating with Keystone V3 running
|
||||||
|
multi-domain configuration.
|
||||||
|
|
||||||
|
If the user authenticated has a default project set, the token will be
|
||||||
|
automatically scoped to their default project.
|
||||||
|
|
||||||
|
If the user authenticated has no default project set, the authentication
|
||||||
|
backend will try to scope to the projects returned from the user's assigned
|
||||||
|
projects. The first successful project scoped will be returned.
|
||||||
|
|
||||||
Inherits from the base ``django.contrib.auth.forms.AuthenticationForm``
|
Inherits from the base ``django.contrib.auth.forms.AuthenticationForm``
|
||||||
class for added security features.
|
class for added security features.
|
||||||
@@ -26,10 +36,16 @@ class Login(AuthenticationForm):
|
|||||||
username = forms.CharField(label=_("User Name"))
|
username = forms.CharField(label=_("User Name"))
|
||||||
password = forms.CharField(label=_("Password"),
|
password = forms.CharField(label=_("Password"),
|
||||||
widget=forms.PasswordInput(render_value=False))
|
widget=forms.PasswordInput(render_value=False))
|
||||||
tenant = forms.CharField(required=False, widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Login, self).__init__(*args, **kwargs)
|
super(Login, self).__init__(*args, **kwargs)
|
||||||
|
self.fields.keyOrder = ['username', 'password', 'region']
|
||||||
|
if getattr(settings,
|
||||||
|
'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT',
|
||||||
|
False):
|
||||||
|
self.fields['domain'] = forms.CharField(label=_("Domain"),
|
||||||
|
required=True)
|
||||||
|
self.fields.keyOrder = ['domain', 'username', 'password', 'region']
|
||||||
self.fields['region'].choices = self.get_region_choices()
|
self.fields['region'].choices = self.get_region_choices()
|
||||||
if len(self.fields['region'].choices) == 1:
|
if len(self.fields['region'].choices) == 1:
|
||||||
self.fields['region'].initial = self.fields['region'].choices[0][0]
|
self.fields['region'].initial = self.fields['region'].choices[0][0]
|
||||||
@@ -42,13 +58,13 @@ class Login(AuthenticationForm):
|
|||||||
|
|
||||||
@sensitive_variables()
|
@sensitive_variables()
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
default_domain = getattr(settings,
|
||||||
|
'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN',
|
||||||
|
'Default')
|
||||||
username = self.cleaned_data.get('username')
|
username = self.cleaned_data.get('username')
|
||||||
password = self.cleaned_data.get('password')
|
password = self.cleaned_data.get('password')
|
||||||
region = self.cleaned_data.get('region')
|
region = self.cleaned_data.get('region')
|
||||||
tenant = self.cleaned_data.get('tenant')
|
domain = self.cleaned_data.get('domain', default_domain)
|
||||||
|
|
||||||
if not tenant:
|
|
||||||
tenant = None
|
|
||||||
|
|
||||||
if not (username and password):
|
if not (username and password):
|
||||||
# Don't authenticate, just let the other validators handle it.
|
# Don't authenticate, just let the other validators handle it.
|
||||||
@@ -58,7 +74,7 @@ class Login(AuthenticationForm):
|
|||||||
self.user_cache = authenticate(request=self.request,
|
self.user_cache = authenticate(request=self.request,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
tenant=tenant,
|
user_domain_name=domain,
|
||||||
auth_url=region)
|
auth_url=region)
|
||||||
msg = 'Login successful for user "%(username)s".' % \
|
msg = 'Login successful for user "%(username)s".' % \
|
||||||
{'username': username}
|
{'username': username}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.utils import datetime_safe
|
from django.utils import datetime_safe
|
||||||
|
|
||||||
|
from keystoneclient.access import AccessInfo
|
||||||
|
from keystoneclient.service_catalog import ServiceCatalog
|
||||||
from keystoneclient.v2_0.roles import Role, RoleManager
|
from keystoneclient.v2_0.roles import Role, RoleManager
|
||||||
from keystoneclient.v2_0.tenants import Tenant, TenantManager
|
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.v2_0.users import User, UserManager
|
||||||
from keystoneclient.service_catalog import ServiceCatalog
|
|
||||||
from keystoneclient import access
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataContainer(object):
|
class TestDataContainer(object):
|
||||||
@@ -18,7 +17,7 @@ class TestDataContainer(object):
|
|||||||
|
|
||||||
|
|
||||||
def generate_test_data():
|
def generate_test_data():
|
||||||
''' Builds a set of test_data data as returned by Keystone. '''
|
''' Builds a set of test_data data as returned by Keystone V2. '''
|
||||||
test_data = TestDataContainer()
|
test_data = TestDataContainer()
|
||||||
|
|
||||||
keystone_service = {
|
keystone_service = {
|
||||||
@@ -96,49 +95,49 @@ def generate_test_data():
|
|||||||
expiration = datetime_safe.datetime.isoformat(tomorrow)
|
expiration = datetime_safe.datetime.isoformat(tomorrow)
|
||||||
|
|
||||||
scoped_token_dict = {
|
scoped_token_dict = {
|
||||||
'token': {
|
'access': {
|
||||||
'id': uuid.uuid4().hex,
|
'token': {
|
||||||
'expires': expiration,
|
'id': uuid.uuid4().hex,
|
||||||
'tenant': tenant_dict_1,
|
'expires': expiration,
|
||||||
'tenants': [tenant_dict_1, tenant_dict_2]},
|
'tenant': tenant_dict_1,
|
||||||
'user': {
|
'tenants': [tenant_dict_1, tenant_dict_2]},
|
||||||
'id': user_dict['id'],
|
'user': {
|
||||||
'name': user_dict['name'],
|
'id': user_dict['id'],
|
||||||
'roles': [role_dict]},
|
'name': user_dict['name'],
|
||||||
'serviceCatalog': [keystone_service, nova_service]
|
'roles': [role_dict]},
|
||||||
|
'serviceCatalog': [keystone_service, nova_service]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
test_data.scoped_token = Token(TokenManager(None),
|
|
||||||
scoped_token_dict,
|
test_data.scoped_access_info = AccessInfo.factory(
|
||||||
loaded=True)
|
resp=None,
|
||||||
|
body=scoped_token_dict)
|
||||||
|
|
||||||
unscoped_token_dict = {
|
unscoped_token_dict = {
|
||||||
'token': {
|
'access': {
|
||||||
'id': uuid.uuid4().hex,
|
'token': {
|
||||||
'expires': expiration},
|
'id': uuid.uuid4().hex,
|
||||||
'user': {
|
'expires': expiration},
|
||||||
'id': user_dict['id'],
|
'user': {
|
||||||
'name': user_dict['name'],
|
'id': user_dict['id'],
|
||||||
'roles': [role_dict]},
|
'name': user_dict['name'],
|
||||||
'serviceCatalog': [keystone_service]
|
'roles': [role_dict]},
|
||||||
|
'serviceCatalog': [keystone_service]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
test_data.unscoped_token = Token(TokenManager(None),
|
test_data.unscoped_access_info = AccessInfo.factory(
|
||||||
unscoped_token_dict,
|
resp=None,
|
||||||
loaded=True)
|
body=unscoped_token_dict)
|
||||||
|
|
||||||
# Service Catalog
|
# Service Catalog
|
||||||
test_data.service_catalog = ServiceCatalog.factory({
|
test_data.service_catalog = ServiceCatalog.factory({
|
||||||
'serviceCatalog': [keystone_service, nova_service],
|
'serviceCatalog': [keystone_service, nova_service],
|
||||||
'token': {
|
'token': {
|
||||||
'id': scoped_token_dict['token']['id'],
|
'id': scoped_token_dict['access']['token']['id'],
|
||||||
'expires': scoped_token_dict['token']['expires'],
|
'expires': scoped_token_dict['access']['token']['expires'],
|
||||||
'user_id': user_dict['id'],
|
'user_id': user_dict['id'],
|
||||||
'tenant_id': tenant_dict_1['id']
|
'tenant_id': tenant_dict_1['id']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
versioned_scoped_toked_dict = scoped_token_dict
|
|
||||||
versioned_scoped_toked_dict['version'] = 'v2.0'
|
|
||||||
|
|
||||||
test_data.access_info = access.AccessInfo(versioned_scoped_toked_dict)
|
|
||||||
|
|
||||||
return test_data
|
return test_data
|
||||||
233
openstack_auth/tests/data_v3.py
Normal file
233
openstack_auth/tests/data_v3.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import requests
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import datetime_safe
|
||||||
|
|
||||||
|
from keystoneclient.access import AccessInfo
|
||||||
|
from keystoneclient.service_catalog import ServiceCatalog
|
||||||
|
from keystoneclient.v3.domains import Domain, DomainManager
|
||||||
|
from keystoneclient.v3.roles import Role, RoleManager
|
||||||
|
from keystoneclient.v3.projects import Project, ProjectManager
|
||||||
|
from keystoneclient.v3.users import User, UserManager
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataContainer(object):
|
||||||
|
""" Arbitrary holder for test data in an object-oriented fashion. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponse(requests.Response):
|
||||||
|
""" Class used to wrap requests.Response and provide some
|
||||||
|
convenience to initialize with a dict """
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self._text = None
|
||||||
|
super(TestResponse, self)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
self.status_code = data.get('status_code', None)
|
||||||
|
self.headers = data.get('headers', None)
|
||||||
|
# Fake the text attribute to streamline Response creation
|
||||||
|
self._text = data.get('text', None)
|
||||||
|
else:
|
||||||
|
self.status_code = data
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.__dict__ == other.__dict__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_data():
|
||||||
|
''' Builds a set of test_data data as returned by Keystone V2. '''
|
||||||
|
test_data = TestDataContainer()
|
||||||
|
|
||||||
|
keystone_service = {
|
||||||
|
'type': 'identity',
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'endpoints': [
|
||||||
|
{
|
||||||
|
'url': 'http://admin.localhost:35357/v3',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'interface': 'admin',
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://internal.localhost:5000/v3',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'interface': 'internal',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url':'http://public.localhost:5000/v3',
|
||||||
|
'region':'RegionOne',
|
||||||
|
'interface': 'public',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Domains
|
||||||
|
domain_dict = {'id': uuid.uuid4().hex,
|
||||||
|
'name': 'domain',
|
||||||
|
'description': '',
|
||||||
|
'enabled': True}
|
||||||
|
test_data.domain = Domain(DomainManager(None), domain_dict, loaded=True)
|
||||||
|
|
||||||
|
# Users
|
||||||
|
user_dict = {'id': uuid.uuid4().hex,
|
||||||
|
'name': 'gabriel',
|
||||||
|
'email': 'gabriel@example.com',
|
||||||
|
'password': 'swordfish',
|
||||||
|
'domain_id': domain_dict['id'],
|
||||||
|
'token': '',
|
||||||
|
'enabled': True}
|
||||||
|
test_data.user = User(UserManager(None), user_dict, loaded=True)
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
project_dict_1 = {'id': uuid.uuid4().hex,
|
||||||
|
'name': 'tenant_one',
|
||||||
|
'description': '',
|
||||||
|
'domain_id': domain_dict['id'],
|
||||||
|
'enabled': True}
|
||||||
|
project_dict_2 = {'id': uuid.uuid4().hex,
|
||||||
|
'name': '',
|
||||||
|
'description': '',
|
||||||
|
'domain_id': domain_dict['id'],
|
||||||
|
'enabled': False}
|
||||||
|
test_data.project_one = Project(ProjectManager(None),
|
||||||
|
project_dict_1,
|
||||||
|
loaded=True)
|
||||||
|
test_data.project_two = Project(ProjectManager(None),
|
||||||
|
project_dict_2,
|
||||||
|
loaded=True)
|
||||||
|
|
||||||
|
# Roles
|
||||||
|
role_dict = {'id': uuid.uuid4().hex,
|
||||||
|
'name': 'Member'}
|
||||||
|
test_data.role = Role(RoleManager, role_dict)
|
||||||
|
|
||||||
|
nova_service = {
|
||||||
|
'type': 'compute',
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'endpoints': [
|
||||||
|
{
|
||||||
|
'url': 'http://nova-admin.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'interface': 'admin',
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://nova-internal.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'interface': 'internal',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url':'http://nova-public.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region':'RegionOne',
|
||||||
|
'interface': 'public',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://nova2-admin.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region': 'RegionTwo',
|
||||||
|
'interface': 'admin',
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://nova2-internal.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region': 'RegionTwo',
|
||||||
|
'interface': 'internal',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url':'http://nova2-public.localhost:8774/v2.0/%s' \
|
||||||
|
% (project_dict_1['id']),
|
||||||
|
'region':'RegionTwo',
|
||||||
|
'interface': 'public',
|
||||||
|
'id': uuid.uuid4().hex
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tokens
|
||||||
|
tomorrow = datetime_safe.datetime.now() + timedelta(days=1)
|
||||||
|
expiration = datetime_safe.datetime.isoformat(tomorrow)
|
||||||
|
auth_token = uuid.uuid4().hex
|
||||||
|
auth_response_headers = {
|
||||||
|
'X-Subject-Token': auth_token
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_response = TestResponse({
|
||||||
|
"headers": auth_response_headers
|
||||||
|
})
|
||||||
|
|
||||||
|
scoped_token_dict = {
|
||||||
|
'token': {
|
||||||
|
'methods': ['password'],
|
||||||
|
'expires_at': expiration,
|
||||||
|
'project': {
|
||||||
|
'id': project_dict_1['id'],
|
||||||
|
'name': project_dict_1['name'],
|
||||||
|
'domain': {
|
||||||
|
'id': domain_dict['id'],
|
||||||
|
'name': domain_dict['name']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'id': user_dict['id'],
|
||||||
|
'name': user_dict['name'],
|
||||||
|
'domain': {
|
||||||
|
'id': domain_dict['id'],
|
||||||
|
'name': domain_dict['name']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'roles': [role_dict],
|
||||||
|
'catalog': [keystone_service, nova_service]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test_data.scoped_access_info = AccessInfo.factory(
|
||||||
|
resp=auth_response,
|
||||||
|
body=scoped_token_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
unscoped_token_dict = {
|
||||||
|
'token': {
|
||||||
|
'methods': ['password'],
|
||||||
|
'expires_at': expiration,
|
||||||
|
'user': {
|
||||||
|
'id': user_dict['id'],
|
||||||
|
'name': user_dict['name'],
|
||||||
|
'domain': {
|
||||||
|
'id': domain_dict['id'],
|
||||||
|
'name': domain_dict['name']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'roles': [role_dict],
|
||||||
|
'catalog': [keystone_service]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test_data.unscoped_access_info = AccessInfo.factory(
|
||||||
|
resp=auth_response,
|
||||||
|
body=unscoped_token_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
# Service Catalog
|
||||||
|
test_data.service_catalog = ServiceCatalog.factory({
|
||||||
|
'methods': ['password'],
|
||||||
|
'user': {},
|
||||||
|
'catalog': [keystone_service, nova_service],
|
||||||
|
}, token=auth_token)
|
||||||
|
|
||||||
|
return test_data
|
||||||
@@ -25,3 +25,13 @@ ROOT_URLCONF = 'openstack_auth.tests.urls'
|
|||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
|
||||||
SECRET_KEY = 'badcafe'
|
SECRET_KEY = 'badcafe'
|
||||||
|
|
||||||
|
OPENSTACK_API_VERSIONS = {
|
||||||
|
"identity": 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = False
|
||||||
|
|
||||||
|
OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'domain'
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
|
import mox
|
||||||
|
|
||||||
from django import test
|
from django import test
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from keystoneclient import exceptions as keystone_exceptions
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
from keystoneclient.v2_0 import client
|
from keystoneclient.v2_0 import client as client_v2
|
||||||
|
from keystoneclient.v3 import client as client_v3
|
||||||
|
|
||||||
import mox
|
from .data_v2 import generate_test_data as data_v2
|
||||||
|
from .data_v3 import generate_test_data as data_v3
|
||||||
from .data import generate_test_data
|
|
||||||
|
|
||||||
|
|
||||||
class OpenStackAuthTests(test.TestCase):
|
DEFAULT_DOMAIN = settings.OPENSTACK_KEYSTONE_DEFAULT_DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackAuthTestsV2(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(OpenStackAuthTests, self).setUp()
|
super(OpenStackAuthTestsV2, self).setUp()
|
||||||
self.mox = mox.Mox()
|
self.mox = mox.Mox()
|
||||||
self.data = generate_test_data()
|
self.data = data_v2()
|
||||||
|
self.ks_client_module = client_v2
|
||||||
endpoint = settings.OPENSTACK_KEYSTONE_URL
|
endpoint = settings.OPENSTACK_KEYSTONE_URL
|
||||||
self.keystone_client = client.Client(endpoint=endpoint,
|
self.keystone_client_unscoped = self.ks_client_module.Client(
|
||||||
auth_ref=self.data.access_info)
|
endpoint=endpoint,
|
||||||
if not hasattr(self.keystone_client, 'service_catalog'):
|
auth_ref=self.data.unscoped_access_info)
|
||||||
self.keystone_client.service_catalog = self.data.service_catalog
|
self.keystone_client_scoped = self.ks_client_module.Client(
|
||||||
|
endpoint=endpoint,
|
||||||
|
auth_ref=self.data.scoped_access_info)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.mox.UnsetStubs()
|
self.mox.UnsetStubs()
|
||||||
@@ -29,31 +37,30 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
def test_login(self):
|
def test_login(self):
|
||||||
tenants = [self.data.tenant_one, self.data.tenant_two]
|
tenants = [self.data.tenant_one, self.data.tenant_two]
|
||||||
user = self.data.user
|
user = self.data.user
|
||||||
sc = self.data.service_catalog
|
unscoped = self.data.unscoped_access_info
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'password': user.password,
|
'password': user.password,
|
||||||
'username': user.name}
|
'username': user.name}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.tenants, "list")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tokens, "authenticate")
|
|
||||||
|
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password=user.password,
|
password=user.password,
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndReturn(self.keystone_client)
|
insecure=False,
|
||||||
self.keystone_client.tenants.list().AndReturn(tenants)
|
debug=False)\
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
tenant_id=self.data.tenant_two.id,
|
self.keystone_client_unscoped.tenants.list().AndReturn(tenants)
|
||||||
insecure=False,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
token=sc.get_token()['id']) \
|
tenant_id=self.data.tenant_two.id,
|
||||||
.AndReturn(self.keystone_client)
|
insecure=False,
|
||||||
self.keystone_client.tokens.authenticate(tenant_id=tenants[1].id,
|
token=unscoped.auth_token,
|
||||||
token=sc.get_token()['id'],
|
debug=False) \
|
||||||
username=user.name) \
|
.AndReturn(self.keystone_client_scoped)
|
||||||
.AndReturn(self.data.scoped_token)
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@@ -71,18 +78,21 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
user = self.data.user
|
user = self.data.user
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'password': user.password,
|
'password': user.password,
|
||||||
'username': user.name}
|
'username': user.name}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.tenants, "list")
|
||||||
|
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password=user.password,
|
password=user.password,
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndReturn(self.keystone_client)
|
insecure=False,
|
||||||
self.keystone_client.tenants.list().AndReturn([])
|
debug=False)\
|
||||||
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
|
self.keystone_client_unscoped.tenants.list().AndReturn([])
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@@ -102,17 +112,19 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
user = self.data.user
|
user = self.data.user
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'password': "invalid",
|
'password': "invalid",
|
||||||
'username': user.name}
|
'username': user.name}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
|
||||||
exc = keystone_exceptions.Unauthorized(401)
|
exc = keystone_exceptions.Unauthorized(401)
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password="invalid",
|
password="invalid",
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndRaise(exc)
|
insecure=False,
|
||||||
|
debug=False).AndRaise(exc)
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@@ -131,17 +143,19 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
user = self.data.user
|
user = self.data.user
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'password': user.password,
|
'password': user.password,
|
||||||
'username': user.name}
|
'username': user.name}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
|
||||||
exc = keystone_exceptions.ClientException(500)
|
exc = keystone_exceptions.ClientException(500)
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password=user.password,
|
password=user.password,
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndRaise(exc)
|
insecure=False,
|
||||||
|
debug=False).AndRaise(exc)
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@@ -163,41 +177,39 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
tenant = self.data.tenant_two
|
tenant = self.data.tenant_two
|
||||||
tenants = [self.data.tenant_one, self.data.tenant_two]
|
tenants = [self.data.tenant_one, self.data.tenant_two]
|
||||||
user = self.data.user
|
user = self.data.user
|
||||||
scoped = self.data.scoped_token
|
unscoped = self.data.unscoped_access_info
|
||||||
|
scoped = self.data.scoped_access_info
|
||||||
sc = self.data.service_catalog
|
sc = self.data.service_catalog
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'username': user.name,
|
'username': user.name,
|
||||||
'password': user.password}
|
'password': user.password}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.tenants, "list")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tokens, "authenticate")
|
|
||||||
|
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password=user.password,
|
password=user.password,
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndReturn(self.keystone_client)
|
insecure=False,
|
||||||
self.keystone_client.tenants.list().AndReturn(tenants)
|
debug=False) \
|
||||||
self.keystone_client.tokens.authenticate(tenant_id=tenants[1].id,
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
token=sc.get_token()['id'],
|
self.keystone_client_unscoped.tenants.list().AndReturn(tenants)
|
||||||
username=user.name) \
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
.AndReturn(scoped)
|
tenant_id=self.data.tenant_two.id,
|
||||||
|
insecure=False,
|
||||||
|
token=unscoped.auth_token,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_scoped)
|
||||||
|
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=sc.url_for(),
|
||||||
tenant_id=self.data.tenant_two.id,
|
tenant_id=tenant.id,
|
||||||
insecure=False,
|
token=scoped.auth_token,
|
||||||
token=sc.get_token()['id']) \
|
insecure=False,
|
||||||
.AndReturn(self.keystone_client)
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_scoped)
|
||||||
client.Client(endpoint=sc.url_for(),
|
|
||||||
insecure=False) \
|
|
||||||
.AndReturn(self.keystone_client)
|
|
||||||
|
|
||||||
self.keystone_client.tokens.authenticate(tenant_id=tenant.id,
|
|
||||||
token=sc.get_token()['id']) \
|
|
||||||
.AndReturn(scoped)
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@@ -211,10 +223,7 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
|
|
||||||
url = reverse('switch_tenants', args=[tenant.id])
|
url = reverse('switch_tenants', args=[tenant.id])
|
||||||
|
|
||||||
scoped.tenant['id'] = self.data.tenant_two._info
|
scoped['token']['tenant']['id'] = self.data.tenant_two.id
|
||||||
sc.catalog['token']['id'] = self.data.tenant_two.id
|
|
||||||
|
|
||||||
form_data['tenant_id'] = tenant.id
|
|
||||||
|
|
||||||
if next:
|
if next:
|
||||||
form_data.update({REDIRECT_FIELD_NAME: next})
|
form_data.update({REDIRECT_FIELD_NAME: next})
|
||||||
@@ -226,8 +235,9 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
self.assertEqual(response['location'], expected_url)
|
self.assertEqual(response['location'], expected_url)
|
||||||
else:
|
else:
|
||||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||||
self.assertEqual(self.client.session['token']['token']['tenant']['id'],
|
|
||||||
scoped.tenant['id'])
|
self.assertEqual(self.client.session['token'].tenant['id'],
|
||||||
|
scoped.tenant_id)
|
||||||
|
|
||||||
def test_switch_with_next(self):
|
def test_switch_with_next(self):
|
||||||
self.test_switch(next='/next_url')
|
self.test_switch(next='/next_url')
|
||||||
@@ -236,33 +246,331 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
tenant = self.data.tenant_one
|
tenant = self.data.tenant_one
|
||||||
tenants = [self.data.tenant_one, self.data.tenant_two]
|
tenants = [self.data.tenant_one, self.data.tenant_two]
|
||||||
user = self.data.user
|
user = self.data.user
|
||||||
scoped = self.data.scoped_token
|
unscoped = self.data.unscoped_access_info
|
||||||
sc = self.data.service_catalog
|
sc = self.data.service_catalog
|
||||||
|
|
||||||
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
'username': user.name,
|
'username': user.name,
|
||||||
'password': user.password}
|
'password': user.password}
|
||||||
|
|
||||||
self.mox.StubOutWithMock(client, "Client")
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tenants, "list")
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.tenants, "list")
|
||||||
self.mox.StubOutWithMock(self.keystone_client.tokens, "authenticate")
|
|
||||||
|
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
password=user.password,
|
password=user.password,
|
||||||
username=user.name,
|
username=user.name,
|
||||||
insecure=False,
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
tenant_id=None).AndReturn(self.keystone_client)
|
insecure=False,
|
||||||
self.keystone_client.tenants.list().AndReturn(tenants)
|
debug=False) \
|
||||||
self.keystone_client.tokens.authenticate(tenant_id=tenants[1].id,
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
token=sc.get_token()['id'],
|
self.keystone_client_unscoped.tenants.list().AndReturn(tenants)
|
||||||
username=user.name) \
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
.AndReturn(scoped)
|
tenant_id=self.data.tenant_two.id,
|
||||||
|
insecure=False,
|
||||||
client.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
token=unscoped.auth_token,
|
||||||
tenant_id=self.data.tenant_two.id,
|
debug=False) \
|
||||||
insecure=False,
|
.AndReturn(self.keystone_client_scoped)
|
||||||
token=sc.get_token()['id']) \
|
|
||||||
.AndReturn(self.keystone_client)
|
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)
|
||||||
|
|
||||||
|
old_region = sc.get_endpoints()['compute'][0]['region']
|
||||||
|
self.assertEqual(self.client.session['services_region'], old_region)
|
||||||
|
|
||||||
|
region = sc.get_endpoints()['compute'][1]['region']
|
||||||
|
url = reverse('switch_services_region', args=[region])
|
||||||
|
|
||||||
|
form_data['region_name'] = region
|
||||||
|
|
||||||
|
if next:
|
||||||
|
form_data.update({REDIRECT_FIELD_NAME: next})
|
||||||
|
|
||||||
|
response = self.client.get(url, form_data)
|
||||||
|
|
||||||
|
if next:
|
||||||
|
expected_url = 'http://testserver%s' % next
|
||||||
|
self.assertEqual(response['location'], expected_url)
|
||||||
|
else:
|
||||||
|
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
|
self.assertEqual(self.client.session['services_region'], region)
|
||||||
|
|
||||||
|
def test_switch_region_with_next(self, next=None):
|
||||||
|
self.test_switch_region(next='/next_url')
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackAuthTestsV3(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(OpenStackAuthTestsV3, self).setUp()
|
||||||
|
self.mox = mox.Mox()
|
||||||
|
self.data = data_v3()
|
||||||
|
self.ks_client_module = client_v3
|
||||||
|
endpoint = settings.OPENSTACK_KEYSTONE_URL
|
||||||
|
self.keystone_client_unscoped = self.ks_client_module.Client(
|
||||||
|
endpoint=endpoint,
|
||||||
|
auth_ref=self.data.unscoped_access_info)
|
||||||
|
self.keystone_client_scoped = self.ks_client_module.Client(
|
||||||
|
endpoint=endpoint,
|
||||||
|
auth_ref=self.data.scoped_access_info)
|
||||||
|
settings.OPENSTACK_API_VERSIONS['identity'] = 3
|
||||||
|
settings.OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v3"
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.mox.UnsetStubs()
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_login(self):
|
||||||
|
projects = [self.data.project_one, self.data.project_two]
|
||||||
|
user = self.data.user
|
||||||
|
unscoped = self.data.unscoped_access_info
|
||||||
|
|
||||||
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'password': user.password,
|
||||||
|
'username': user.name}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.projects,
|
||||||
|
"list")
|
||||||
|
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password=user.password,
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False)\
|
||||||
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
|
self.keystone_client_unscoped.projects.list(user=user.id) \
|
||||||
|
.AndReturn(projects)
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
tenant_id=self.data.project_two.id,
|
||||||
|
insecure=False,
|
||||||
|
token=unscoped.auth_token,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_scoped)
|
||||||
|
|
||||||
|
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,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'password': user.password,
|
||||||
|
'username': user.name}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.projects,
|
||||||
|
"list")
|
||||||
|
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password=user.password,
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False)\
|
||||||
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
|
self.keystone_client_unscoped.projects.list(user=user.id) \
|
||||||
|
.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,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'password': "invalid",
|
||||||
|
'username': user.name}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
|
||||||
|
exc = keystone_exceptions.Unauthorized(401)
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password="invalid",
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False).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,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'password': user.password,
|
||||||
|
'username': user.name}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
|
||||||
|
exc = keystone_exceptions.ClientException(500)
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password=user.password,
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False).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, next=None):
|
||||||
|
project = self.data.project_two
|
||||||
|
projects = [self.data.project_one, self.data.project_two]
|
||||||
|
user = self.data.user
|
||||||
|
unscoped = self.data.unscoped_access_info
|
||||||
|
scoped = self.data.scoped_access_info
|
||||||
|
sc = self.data.service_catalog
|
||||||
|
|
||||||
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'username': user.name,
|
||||||
|
'password': user.password}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.projects,
|
||||||
|
"list")
|
||||||
|
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password=user.password,
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
|
self.keystone_client_unscoped.projects.list(user=user.id) \
|
||||||
|
.AndReturn(projects)
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
tenant_id=self.data.project_two.id,
|
||||||
|
insecure=False,
|
||||||
|
token=unscoped.auth_token,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_scoped)
|
||||||
|
self.ks_client_module.Client(auth_url=sc.url_for(),
|
||||||
|
tenant_id=project.id,
|
||||||
|
token=scoped.auth_token,
|
||||||
|
insecure=False,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_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=[project.id])
|
||||||
|
|
||||||
|
scoped['project']['id'] = self.data.project_two.id
|
||||||
|
|
||||||
|
if next:
|
||||||
|
form_data.update({REDIRECT_FIELD_NAME: next})
|
||||||
|
|
||||||
|
response = self.client.get(url, form_data)
|
||||||
|
|
||||||
|
if next:
|
||||||
|
expected_url = 'http://testserver%s' % next
|
||||||
|
self.assertEqual(response['location'], expected_url)
|
||||||
|
else:
|
||||||
|
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
|
self.assertEqual(self.client.session['token'].project['id'],
|
||||||
|
scoped.project_id)
|
||||||
|
|
||||||
|
def test_switch_with_next(self):
|
||||||
|
self.test_switch(next='/next_url')
|
||||||
|
|
||||||
|
def test_switch_region(self, next=None):
|
||||||
|
|
||||||
|
projects = [self.data.project_one, self.data.project_two]
|
||||||
|
user = self.data.user
|
||||||
|
unscoped = self.data.unscoped_access_info
|
||||||
|
sc = self.data.service_catalog
|
||||||
|
|
||||||
|
form_data = {'region': settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
'domain': DEFAULT_DOMAIN,
|
||||||
|
'username': user.name,
|
||||||
|
'password': user.password}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.ks_client_module, "Client")
|
||||||
|
self.mox.StubOutWithMock(self.keystone_client_unscoped.projects,
|
||||||
|
"list")
|
||||||
|
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
password=user.password,
|
||||||
|
username=user.name,
|
||||||
|
user_domain_name=DEFAULT_DOMAIN,
|
||||||
|
insecure=False,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_unscoped)
|
||||||
|
self.keystone_client_unscoped.projects.list(user=user.id) \
|
||||||
|
.AndReturn(projects)
|
||||||
|
self.ks_client_module.Client(auth_url=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
tenant_id=self.data.project_two.id,
|
||||||
|
insecure=False,
|
||||||
|
token=unscoped.auth_token,
|
||||||
|
debug=False) \
|
||||||
|
.AndReturn(self.keystone_client_scoped)
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
|
||||||
from .utils import patch_middleware_get_user
|
from .utils import patch_middleware_get_user
|
||||||
from .views import switch_region
|
|
||||||
|
|
||||||
patch_middleware_get_user()
|
patch_middleware_get_user()
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,23 @@ import logging
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as keystone_client
|
|
||||||
from keystoneclient import exceptions as keystone_exceptions
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
|
|
||||||
from .utils import check_token_expiration, is_ans1_token
|
from .utils import check_token_expiration
|
||||||
|
from .utils import get_keystone_version
|
||||||
|
from .utils import get_project_list
|
||||||
|
from .utils import is_ans1_token
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def set_session_from_user(request, user):
|
def set_session_from_user(request, user):
|
||||||
if is_ans1_token(user.token.id):
|
request.session['token'] = user.token
|
||||||
hashed_token = hashlib.md5(user.token.id).hexdigest()
|
|
||||||
user.token._info['token']['id'] = hashed_token
|
|
||||||
if 'token_list' not in request.session:
|
if 'token_list' not in request.session:
|
||||||
request.session['token_list'] = []
|
request.session['token_list'] = []
|
||||||
token_tuple = (user.endpoint, user.token.id)
|
token_tuple = (user.endpoint, user.token.id)
|
||||||
request.session['token_list'].append(token_tuple)
|
request.session['token_list'].append(token_tuple)
|
||||||
request.session['token'] = user.token._info
|
|
||||||
request.session['user_id'] = user.id
|
request.session['user_id'] = user.id
|
||||||
request.session['region_endpoint'] = user.endpoint
|
request.session['region_endpoint'] = user.endpoint
|
||||||
request.session['services_region'] = user.services_region
|
request.session['services_region'] = user.services_region
|
||||||
@@ -31,17 +30,65 @@ def create_user_from_token(request, token, endpoint, services_region=None):
|
|||||||
return User(id=token.user['id'],
|
return User(id=token.user['id'],
|
||||||
token=token,
|
token=token,
|
||||||
user=token.user['name'],
|
user=token.user['name'],
|
||||||
tenant_id=token.tenant['id'],
|
user_domain_id=token.user_domain_id,
|
||||||
tenant_name=token.tenant['name'],
|
project_id=token.project['id'],
|
||||||
|
project_name=token.project['name'],
|
||||||
|
domain_id=token.domain['id'],
|
||||||
|
domain_name=token.domain['name'],
|
||||||
enabled=True,
|
enabled=True,
|
||||||
service_catalog=token.serviceCatalog,
|
service_catalog=token.serviceCatalog,
|
||||||
roles=token.user['roles'],
|
roles=token.roles,
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
services_region=services_region)
|
services_region=services_region)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(object):
|
||||||
|
"""Token object that encapsulates the auth_ref (AccessInfo)from keystone
|
||||||
|
client.
|
||||||
|
|
||||||
|
Added for maintaining backward compatibility with horizon that expects
|
||||||
|
Token object in the user object.
|
||||||
|
"""
|
||||||
|
def __init__(self, auth_ref):
|
||||||
|
# User-related attributes
|
||||||
|
user = {}
|
||||||
|
user['id'] = auth_ref.user_id
|
||||||
|
user['name'] = auth_ref.username
|
||||||
|
self.user = user
|
||||||
|
self.user_domain_id = auth_ref.user_domain_id
|
||||||
|
|
||||||
|
#Token-related attributes
|
||||||
|
self.id = auth_ref.auth_token
|
||||||
|
if is_ans1_token(self.id):
|
||||||
|
self.id = hashlib.md5(self.id).hexdigest()
|
||||||
|
self.expires = auth_ref.expires
|
||||||
|
|
||||||
|
# Project-related attributes
|
||||||
|
project = {}
|
||||||
|
project['id'] = auth_ref.project_id
|
||||||
|
project['name'] = auth_ref.project_name
|
||||||
|
self.project = project
|
||||||
|
self.tenant = self.project
|
||||||
|
|
||||||
|
# Domain-related attributes
|
||||||
|
domain = {}
|
||||||
|
domain['id'] = auth_ref.domain_id
|
||||||
|
domain['name'] = auth_ref.domain_name
|
||||||
|
self.domain = domain
|
||||||
|
|
||||||
|
if auth_ref.version == 'v2.0':
|
||||||
|
self.roles = auth_ref['user']['roles']
|
||||||
|
else:
|
||||||
|
self.roles = auth_ref['roles']
|
||||||
|
|
||||||
|
if get_keystone_version() < 3:
|
||||||
|
self.serviceCatalog = auth_ref.get('serviceCatalog', [])
|
||||||
|
else:
|
||||||
|
self.serviceCatalog = auth_ref.get('catalog', [])
|
||||||
|
|
||||||
|
|
||||||
class User(AnonymousUser):
|
class User(AnonymousUser):
|
||||||
""" A User class with some extra special sauce for Keystone.
|
"""A User class with some extra special sauce for Keystone.
|
||||||
|
|
||||||
In addition to the standard Django user attributes, this class also has
|
In addition to the standard Django user attributes, this class also has
|
||||||
the following:
|
the following:
|
||||||
@@ -50,14 +97,29 @@ class User(AnonymousUser):
|
|||||||
|
|
||||||
The Keystone token object associated with the current user/tenant.
|
The Keystone token object associated with the current user/tenant.
|
||||||
|
|
||||||
|
The token object is deprecated, user auth_ref instead.
|
||||||
|
|
||||||
.. attribute:: tenant_id
|
.. attribute:: tenant_id
|
||||||
|
|
||||||
The id of the Keystone tenant for the current user/token.
|
The id of the Keystone tenant for the current user/token.
|
||||||
|
|
||||||
|
The tenant_id keyword argument is deprecated, use project_id instead.
|
||||||
|
|
||||||
.. attribute:: tenant_name
|
.. attribute:: tenant_name
|
||||||
|
|
||||||
The name of the Keystone tenant for the current user/token.
|
The name of the Keystone tenant for the current user/token.
|
||||||
|
|
||||||
|
The tenant_name keyword argument is deprecated, use project_name
|
||||||
|
instead.
|
||||||
|
|
||||||
|
.. attribute:: project_id
|
||||||
|
|
||||||
|
The id of the Keystone project for the current user/token.
|
||||||
|
|
||||||
|
.. attribute:: project_name
|
||||||
|
|
||||||
|
The name of the Keystone project for the current user/token.
|
||||||
|
|
||||||
.. attribute:: service_catalog
|
.. attribute:: service_catalog
|
||||||
|
|
||||||
The ``ServiceCatalog`` data returned by Keystone.
|
The ``ServiceCatalog`` data returned by Keystone.
|
||||||
@@ -66,17 +128,35 @@ class User(AnonymousUser):
|
|||||||
|
|
||||||
A list of dictionaries containing role names and ids as returned
|
A list of dictionaries containing role names and ids as returned
|
||||||
by Keystone.
|
by Keystone.
|
||||||
|
|
||||||
|
.. attribute:: services_region
|
||||||
|
|
||||||
|
A list of non-identity service endpoint regions extracted from the
|
||||||
|
service catalog.
|
||||||
|
|
||||||
|
.. attribute:: user_domain_id
|
||||||
|
|
||||||
|
The domain id of the current user.
|
||||||
|
|
||||||
|
.. attribute:: domain_id
|
||||||
|
|
||||||
|
The id of the Keystone domain scoped for the current user/token.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, id=None, token=None, user=None, tenant_id=None,
|
def __init__(self, id=None, token=None, user=None, tenant_id=None,
|
||||||
service_catalog=None, tenant_name=None, roles=None,
|
service_catalog=None, tenant_name=None, roles=None,
|
||||||
authorized_tenants=None, endpoint=None, enabled=False,
|
authorized_tenants=None, endpoint=None, enabled=False,
|
||||||
services_region=None):
|
services_region=None, user_domain_id=None, domain_id=None,
|
||||||
|
domain_name=None, project_id=None, project_name=None):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.pk = id
|
self.pk = id
|
||||||
self.token = token
|
self.token = token
|
||||||
self.username = user
|
self.username = user
|
||||||
self.tenant_id = tenant_id
|
self.user_domain_id = user_domain_id
|
||||||
self.tenant_name = tenant_name
|
self.domain_id = domain_id
|
||||||
|
self.domain_name = domain_name
|
||||||
|
self.project_id = project_id or tenant_id
|
||||||
|
self.project_name = project_name or tenant_name
|
||||||
self.service_catalog = service_catalog
|
self.service_catalog = service_catalog
|
||||||
self._services_region = services_region or \
|
self._services_region = services_region or \
|
||||||
self.default_services_region()
|
self.default_services_region()
|
||||||
@@ -85,6 +165,10 @@ class User(AnonymousUser):
|
|||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self._authorized_tenants = authorized_tenants
|
self._authorized_tenants = authorized_tenants
|
||||||
|
|
||||||
|
# List of variables to be deprecated.
|
||||||
|
self.tenant_id = self.project_id
|
||||||
|
self.tenant_name = self.project_name
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
@@ -131,14 +215,15 @@ class User(AnonymousUser):
|
|||||||
endpoint = self.endpoint
|
endpoint = self.endpoint
|
||||||
token = self.token
|
token = self.token
|
||||||
try:
|
try:
|
||||||
client = keystone_client.Client(username=self.username,
|
self._authorized_tenants = get_project_list(
|
||||||
auth_url=endpoint,
|
user_id=self.id,
|
||||||
token=token.id,
|
auth_url=endpoint,
|
||||||
insecure=insecure)
|
token=token.id,
|
||||||
self._authorized_tenants = client.tenants.list()
|
insecure=insecure,
|
||||||
|
debug=settings.DEBUG)
|
||||||
except (keystone_exceptions.ClientException,
|
except (keystone_exceptions.ClientException,
|
||||||
keystone_exceptions.AuthorizationFailure):
|
keystone_exceptions.AuthorizationFailure):
|
||||||
LOG.exception('Unable to retrieve tenant list.')
|
LOG.exception('Unable to retrieve project list.')
|
||||||
return self._authorized_tenants or []
|
return self._authorized_tenants or []
|
||||||
|
|
||||||
@authorized_tenants.setter
|
@authorized_tenants.setter
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from django.contrib import auth
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.auth import middleware
|
from django.contrib.auth import middleware
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.dateparse import parse_datetime
|
|
||||||
|
from keystoneclient.v2_0 import client as client_v2
|
||||||
|
from keystoneclient.v3 import client as client_v3
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -49,7 +51,7 @@ def check_token_expiration(token):
|
|||||||
|
|
||||||
Returns ``True`` if the token has not yet expired, otherwise ``False``.
|
Returns ``True`` if the token has not yet expired, otherwise ``False``.
|
||||||
"""
|
"""
|
||||||
expiration = parse_datetime(token.expires)
|
expiration = token.expires
|
||||||
if settings.USE_TZ and timezone.is_naive(expiration):
|
if settings.USE_TZ and timezone.is_naive(expiration):
|
||||||
# Presumes that the Keystone is using UTC.
|
# Presumes that the Keystone is using UTC.
|
||||||
expiration = timezone.make_aware(expiration, timezone.utc)
|
expiration = timezone.make_aware(expiration, timezone.utc)
|
||||||
@@ -121,3 +123,28 @@ def is_safe_url(url, host=None):
|
|||||||
return False
|
return False
|
||||||
netloc = urlparse.urlparse(url)[1]
|
netloc = urlparse.urlparse(url)[1]
|
||||||
return not netloc or netloc == host
|
return not netloc or netloc == host
|
||||||
|
|
||||||
|
|
||||||
|
# Helper for figuring out keystone version
|
||||||
|
# Implementation will change when API version discovery is available
|
||||||
|
def get_keystone_version():
|
||||||
|
return getattr(settings, 'OPENSTACK_API_VERSIONS', {}).get('identity', 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_keystone_client():
|
||||||
|
if get_keystone_version() < 3:
|
||||||
|
return client_v2
|
||||||
|
else:
|
||||||
|
return client_v3
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_list(*args, **kwargs):
|
||||||
|
if get_keystone_version() < 3:
|
||||||
|
client = get_keystone_client().Client(*args, **kwargs)
|
||||||
|
return client.tenants.list()
|
||||||
|
else:
|
||||||
|
auth_url = kwargs.get('auth_url', '').replace('v2.0', 'v3')
|
||||||
|
kwargs['auth_url'] = auth_url
|
||||||
|
client = get_keystone_client().Client(*args, **kwargs)
|
||||||
|
client.management_url = auth_url
|
||||||
|
return client.projects.list(user=kwargs.get('user_id'))
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from .utils import is_safe_url
|
from .utils import is_safe_url
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as keystone_client
|
from keystoneclient.v2_0 import client as keystone_client_v2
|
||||||
from keystoneclient import exceptions as keystone_exceptions
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
|
|
||||||
from .forms import Login
|
from .forms import Login
|
||||||
from .user import set_session_from_user, create_user_from_token
|
from .user import set_session_from_user, create_user_from_token, Token
|
||||||
|
from .utils import get_keystone_client
|
||||||
|
from .utils import get_keystone_version
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -88,34 +90,45 @@ def delete_all_tokens(token_list):
|
|||||||
try:
|
try:
|
||||||
endpoint = token_tuple[0]
|
endpoint = token_tuple[0]
|
||||||
token = token_tuple[1]
|
token = token_tuple[1]
|
||||||
client = keystone_client.Client(endpoint=endpoint,
|
if get_keystone_version() < 3:
|
||||||
token=token,
|
client = keystone_client_v2.Client(endpoint=endpoint,
|
||||||
insecure=insecure)
|
token=token,
|
||||||
client.tokens.delete(token=token)
|
insecure=insecure,
|
||||||
|
debug=settings.DEBUG)
|
||||||
|
client.tokens.delete(token=token)
|
||||||
|
else:
|
||||||
|
# FIXME: KS-client does not have delete token available
|
||||||
|
# Need to add this later when it is exposed.
|
||||||
|
pass
|
||||||
except keystone_exceptions.ClientException as e:
|
except keystone_exceptions.ClientException as e:
|
||||||
LOG.info('Could not delete token')
|
LOG.info('Could not delete token')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def switch(request, tenant_id, redirect_field_name=REDIRECT_FIELD_NAME):
|
def switch(request, tenant_id, redirect_field_name=REDIRECT_FIELD_NAME):
|
||||||
""" Switches an authenticated user from one tenant to another. """
|
""" Switches an authenticated user from one project to another. """
|
||||||
LOG.debug('Switching to tenant %s for user "%s".'
|
LOG.debug('Switching to tenant %s for user "%s".'
|
||||||
% (tenant_id, request.user.username))
|
% (tenant_id, request.user.username))
|
||||||
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
||||||
endpoint = request.user.endpoint
|
endpoint = request.user.endpoint
|
||||||
client = keystone_client.Client(endpoint=endpoint,
|
|
||||||
insecure=insecure)
|
|
||||||
try:
|
try:
|
||||||
token = client.tokens.authenticate(tenant_id=tenant_id,
|
if get_keystone_version() >= 3:
|
||||||
token=request.user.token.id)
|
endpoint = endpoint.replace('v2.0', 'v3')
|
||||||
msg = 'Tenant switch successful for user "%(username)s".' % \
|
|
||||||
|
client = get_keystone_client().Client(tenant_id=tenant_id,
|
||||||
|
token=request.user.token.id,
|
||||||
|
auth_url=endpoint,
|
||||||
|
insecure=insecure,
|
||||||
|
debug=settings.DEBUG)
|
||||||
|
auth_ref = client.auth_ref
|
||||||
|
msg = 'Project switch successful for user "%(username)s".' % \
|
||||||
{'username': request.user.username}
|
{'username': request.user.username}
|
||||||
LOG.info(msg)
|
LOG.info(msg)
|
||||||
except keystone_exceptions.ClientException:
|
except keystone_exceptions.ClientException:
|
||||||
msg = 'Tenant switch failed for user "%(username)s".' % \
|
msg = 'Project switch failed for user "%(username)s".' % \
|
||||||
{'username': request.user.username}
|
{'username': request.user.username}
|
||||||
LOG.warning(msg)
|
LOG.warning(msg)
|
||||||
token = None
|
auth_ref = None
|
||||||
LOG.exception('An error occurred while switching sessions.')
|
LOG.exception('An error occurred while switching sessions.')
|
||||||
|
|
||||||
# Ensure the user-originating redirection url is safe.
|
# Ensure the user-originating redirection url is safe.
|
||||||
@@ -124,8 +137,8 @@ def switch(request, tenant_id, redirect_field_name=REDIRECT_FIELD_NAME):
|
|||||||
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||||
|
|
||||||
if token:
|
if auth_ref:
|
||||||
user = create_user_from_token(request, token, endpoint)
|
user = create_user_from_token(request, Token(auth_ref), endpoint)
|
||||||
set_session_from_user(request, user)
|
set_session_from_user(request, user)
|
||||||
return shortcuts.redirect(redirect_to)
|
return shortcuts.redirect(redirect_to)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user