diff --git a/horizon/horizon/api/keystone.py b/horizon/horizon/api/keystone.py index cdc455169c..3c54123ce6 100644 --- a/horizon/horizon/api/keystone.py +++ b/horizon/horizon/api/keystone.py @@ -23,7 +23,9 @@ import logging from django.conf import settings from keystoneclient import exceptions as keystone_exceptions +from keystoneclient import service_catalog from keystoneclient.v2_0 import client as keystone_client +from keystoneclient.v2_0 import tokens from horizon.api.base import * from horizon.api.deprecated import admin_api @@ -59,7 +61,7 @@ class Services(APIResourceWrapper): def keystoneclient(request, username=None, password=None, tenant_id=None, - token_id=None, endpoint=None): + token_id=None, endpoint=None, endpoint_type=None): """Returns a client connected to the Keystone backend. Several forms of authentication are supported: @@ -82,8 +84,11 @@ def keystoneclient(request, username=None, password=None, tenant_id=None, user = request.user if hasattr(request, '_keystone') and \ request._keystone.auth_token == user.token: + LOG.debug("Using cached client for token: %s" % user.token) conn = request._keystone else: + LOG.debug("Creating a new client connection with endpoint: %s." + % endpoint) conn = keystone_client.Client(username=username or user.username, password=password, project_id=tenant_id or user.tenant_id, @@ -95,7 +100,10 @@ def keystoneclient(request, username=None, password=None, tenant_id=None, # Fetch the correct endpoint for the user type catalog = getattr(conn, 'service_catalog', None) if catalog and "serviceCatalog" in catalog.catalog.keys(): - if user.is_admin(): + if endpoint_type: + endpoint = catalog.url_for(service_type='identity', + endpoint_type=endpoint_type) + elif user.is_admin(): endpoint = catalog.url_for(service_type='identity', endpoint_type='adminURL') else: @@ -137,9 +145,11 @@ def tenant_delete(request, tenant_id): keystoneclient(request).tenants.delete(tenant_id) -def tenant_list_for_token(request, token): - c = keystoneclient(request, token_id=token, - endpoint=settings.OPENSTACK_KEYSTONE_URL) +def tenant_list_for_token(request, token, endpoint_type=None): + c = keystoneclient(request, + token_id=token, + endpoint=settings.OPENSTACK_KEYSTONE_URL, + endpoint_type=endpoint_type) return [Tenant(t) for t in c.tenants.list()] @@ -170,7 +180,17 @@ def token_create_scoped(request, tenant, token): del request._keystone c = keystoneclient(request, tenant_id=tenant, token_id=token, endpoint=settings.OPENSTACK_KEYSTONE_URL) - scoped_token = c.tokens.authenticate(tenant=tenant, token=token) + raw_token = c.tokens.authenticate(tenant=tenant, + token=token, + return_raw=True) + c.service_catalog = service_catalog.ServiceCatalog(raw_token) + if request.user.is_admin(): + c.management_url = c.service_catalog.url_for(service_type='identity', + endpoint_type='adminURL') + else: + c.management_url = c.service_catalog.url_for(service_type='identity', + endpoint_type='publicURL') + scoped_token = tokens.Token(tokens.TokenManager, raw_token) return Token(scoped_token) diff --git a/horizon/horizon/context_processors.py b/horizon/horizon/context_processors.py index 9f0a98780a..eccb0452a8 100644 --- a/horizon/horizon/context_processors.py +++ b/horizon/horizon/context_processors.py @@ -21,12 +21,17 @@ Context processors used by Horizon. """ +import logging + from django.conf import settings from django.contrib import messages from horizon import api +LOG = logging.getLogger(__name__) + + def horizon(request): """ The main Horizon context processor. Required for Horizon to function. @@ -45,15 +50,19 @@ def horizon(request): context = {} # Auth/Keystone context + context.setdefault('authorized_tenants', []) if request.user.is_authenticated(): try: - tenants = api.tenant_list_for_token(request, request.user.token) - context['tenants'] = tenants + tenants = api.tenant_list_for_token(request, + request.user.token, + endpoint_type='internalURL') + context['authorized_tenants'] = tenants except Exception, e: if hasattr(request.user, 'message_set'): - messages.error(request, _("Unable to retrieve tenant list from\ - keystone: %s") % e.message) - context['tenants'] = [] + messages.error(request, _("Unable to retrieve tenant list: %s") + % e.message) + else: + LOG.exception('Could not retrieve tenant list.') # Object Store/Swift context catalog = getattr(request.user, 'service_catalog', []) diff --git a/horizon/horizon/test.py b/horizon/horizon/test.py index 97f512aff0..969718b950 100644 --- a/horizon/horizon/test.py +++ b/horizon/horizon/test.py @@ -66,6 +66,12 @@ class TestCase(django_test.TestCase): TEST_TOKEN = 'aToken' TEST_USER = 'test' TEST_ROLES = [{'name': 'admin', 'id': '1'}] + TEST_CONTEXT = {'authorized_tenants': [{'enabled': True, + 'name': 'aTenant', + 'id': '1', + 'description': "None"}], + 'object_store_configured': False, + 'network_configured': False} TEST_SERVICE_CATALOG = [ {"endpoints": [{ @@ -107,12 +113,8 @@ class TestCase(django_test.TestCase): def setUp(self): self.mox = mox.Mox() - context_dict = {'tenants': [], - 'object_store_configured': False, - 'network_configured': False} - self._real_horizon_context_processor = context_processors.horizon - context_processors.horizon = lambda request: context_dict + context_processors.horizon = lambda request: self.TEST_CONTEXT self._real_get_user_from_request = users.get_user_from_request self.setActiveUser(token=self.TEST_TOKEN, diff --git a/horizon/horizon/tests/context_processor_tests.py b/horizon/horizon/tests/context_processor_tests.py index b4e507d12a..06b7e70fa0 100644 --- a/horizon/horizon/tests/context_processor_tests.py +++ b/horizon/horizon/tests/context_processor_tests.py @@ -18,8 +18,12 @@ # License for the specific language governing permissions and limitations # under the License. +from django import http +from mox import IsA + from horizon import context_processors from horizon import test +from horizon import api class ContextProcessorTests(test.TestCase): @@ -32,6 +36,20 @@ class ContextProcessorTests(test.TestCase): super(ContextProcessorTests, self).tearDown() self.request.user.service_catalog = self._prev_catalog + def test_authorized_tenants(self): + tenant_list = self.TEST_CONTEXT['authorized_tenants'] + self.mox.StubOutWithMock(api, 'tenant_list_for_token') + api.tenant_list_for_token(IsA(http.HttpRequest), + self.TEST_TOKEN, + endpoint_type='internalURL') \ + .AndReturn(tenant_list) + self.mox.ReplayAll() + + context = context_processors.horizon(self.request) + self.assertEqual(len(context['authorized_tenants']), 1) + tenant = context['authorized_tenants'].pop() + self.assertEqual(tenant['id'], self.TEST_TENANT) + def test_object_store(self): # Returns the object store service data when it's in the catalog context = context_processors.horizon(self.request) diff --git a/horizon/horizon/views/auth.py b/horizon/horizon/views/auth.py index 9322b04914..6269e22174 100644 --- a/horizon/horizon/views/auth.py +++ b/horizon/horizon/views/auth.py @@ -29,123 +29,36 @@ from openstackx.api import exceptions as api_exceptions from horizon import api from horizon import exceptions -from horizon import forms +from horizon import users +from horizon.base import Horizon +from horizon.views.auth_forms import Login, LoginWithTenant, _set_session_data LOG = logging.getLogger(__name__) -def _is_admin(token): - for role in token.user['roles']: - if role['name'].lower() == 'admin': - return True - return False - - -def _set_session_data(request, token): - request.session['admin'] = _is_admin(token) - request.session['serviceCatalog'] = token.serviceCatalog - request.session['tenant'] = token.tenant['name'] - request.session['tenant_id'] = token.tenant['id'] - request.session['token'] = token.id - request.session['user'] = token.user['name'] - request.session['roles'] = token.user['roles'] - - -class Login(forms.SelfHandlingForm): - username = forms.CharField(max_length="20", label=_("User Name")) - password = forms.CharField(max_length="20", label=_("Password"), - widget=forms.PasswordInput(render_value=False)) - - def handle(self, request, data): - try: - if data.get('tenant'): - token = api.token_create(request, - data.get('tenant'), - data['username'], - data['password']) - - tenants = api.tenant_list_for_token(request, token.id) - tenant = None - for t in tenants: - if t.id == data.get('tenant'): - tenant = t - else: - token = api.token_create(request, - '', - data['username'], - data['password']) - - # Unscoped token - request.session['unscoped_token'] = token.id - request.user.username = data['username'] - - # Get the tenant list, and log in using first tenant - # FIXME (anthony): add tenant chooser here? - tenants = api.tenant_list_for_token(request, token.id) - - # Abort if there are no valid tenants for this user - if not tenants: - messages.error(request, - _('No tenants present for user: %(user)s') % - {"user": data['username']}) - return - - # Create a token. - # NOTE(gabriel): Keystone can return tenants that you're - # authorized to administer but not to log into as a user, so in - # the case of an Unauthorized error we should iterate through - # the tenants until one succeeds or we've failed them all. - while tenants: - tenant = tenants.pop() - try: - token = api.token_create_scoped(request, - tenant.id, - token.id) - break - except exceptions.Unauthorized as e: - token = None - if token is None: - raise exceptions.Unauthorized( - _("You are not authorized for any available tenants.")) - - LOG.info('Login form for user "%s". Service Catalog data:\n%s' % - (data['username'], token.serviceCatalog)) - _set_session_data(request, token) - - return shortcuts.redirect('horizon:nova:overview:index') - - except api_exceptions.Unauthorized as e: - msg = _('Error authenticating: %s') % e.message - LOG.exception(msg) - messages.error(request, msg) - except api_exceptions.ApiException as e: - messages.error(request, - _('Error authenticating with keystone: %s') % - e.message) - - -class LoginWithTenant(Login): - username = forms.CharField(max_length="20", - widget=forms.TextInput(attrs={'readonly': 'readonly'})) - tenant = forms.CharField(widget=forms.HiddenInput()) - - def login(request): - if request.user and request.user.is_authenticated(): - if request.user.is_admin(): - return shortcuts.redirect('horizon:syspanel:overview:index') - else: - return shortcuts.redirect('horizon:nova:overview:index') + """ + Logs in a user and redirects them to the URL specified by + :func:`horizon.get_user_home`. + """ + if request.user.is_authenticated(): + user = users.User(users.get_user_from_request(request)) + return shortcuts.redirect(Horizon.get_user_home(user)) form, handled = Login.maybe_handle(request) if handled: return handled + # FIXME(gabriel): we don't ship a view named splash return shortcuts.render(request, 'splash.html', {'form': form}) def switch_tenants(request, tenant_id): + """ + Swaps a user from one tenant to another using the unscoped token from + Keystone to exchange scoped tokens for the new tenant. + """ form, handled = LoginWithTenant.maybe_handle( request, initial={'tenant': tenant_id, 'username': request.user.username}) @@ -159,10 +72,12 @@ def switch_tenants(request, tenant_id): tenant_id, unscoped_token) _set_session_data(request, token) - return shortcuts.redirect('horizon:nova:overview:index') + user = users.User(users.get_user_from_request(request)) + return shortcuts.redirect(Horizon.get_user_home(user)) except exceptions.Unauthorized as e: messages.error(_("You are not authorized for that tenant.")) + # FIXME(gabriel): we don't ship switch_tenants.html return shortcuts.render(request, 'switch_tenants.html', { 'to_tenant': tenant_id, @@ -170,5 +85,7 @@ def switch_tenants(request, tenant_id): def logout(request): + """ Clears the session and logs the current user out. """ request.session.clear() + # FIXME(gabriel): we don't ship a view named splash return shortcuts.redirect('splash') diff --git a/openstack-dashboard/dashboard/settings.py b/openstack-dashboard/dashboard/settings.py index 1b5100be2e..e61865f76a 100644 --- a/openstack-dashboard/dashboard/settings.py +++ b/openstack-dashboard/dashboard/settings.py @@ -138,4 +138,3 @@ if DEBUG: 'debug_toolbar.middleware.DebugToolbarMiddleware',) except ImportError: logging.info('Running in debug mode without debug_toolbar.') - diff --git a/openstack-dashboard/dashboard/templates/_topbar.html b/openstack-dashboard/dashboard/templates/_topbar.html index b75d38a51a..c246cb7e10 100644 --- a/openstack-dashboard/dashboard/templates/_topbar.html +++ b/openstack-dashboard/dashboard/templates/_topbar.html @@ -15,7 +15,7 @@