diff --git a/heat/common/auth_password.py b/heat/common/auth_password.py index afacd8ec77..8f29b7d47b 100644 --- a/heat/common/auth_password.py +++ b/heat/common/auth_password.py @@ -52,14 +52,14 @@ class KeystonePasswordAuthProtocol(object): ctx = context.RequestContext(username=username, password=password, tenant_id=tenant, auth_url=auth_url, is_admin=False) - hc = heat_keystoneclient.KeystoneClient(ctx) - client = hc.client + session = heat_keystoneclient.KeystoneClient(ctx).session + auth_ref = ctx.auth_plugin.get_access(session) except (keystone_exceptions.Unauthorized, keystone_exceptions.Forbidden, keystone_exceptions.NotFound, keystone_exceptions.AuthorizationFailure): return self._reject_request(env, start_response, auth_url) - env.update(self._build_user_headers(client.auth_ref)) + env.update(self._build_user_headers(auth_ref)) return self.app(env, start_response) diff --git a/heat/common/context.py b/heat/common/context.py index 42c9b23064..19536b6031 100644 --- a/heat/common/context.py +++ b/heat/common/context.py @@ -11,16 +11,60 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneclient import access +from keystoneclient.auth.identity import base +from keystoneclient.auth.identity import v3 +from keystoneclient.auth import token_endpoint +from oslo.config import cfg +from oslo.middleware import request_id as oslo_request_id from oslo.utils import importutils from oslo_context import context -from oslo_middleware import request_id as oslo_request_id from heat.common import exception +from heat.common.i18n import _LE from heat.common import policy from heat.common import wsgi from heat.db import api as db_api from heat.engine import clients from heat.openstack.common import local +from heat.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +# FIXME(jamielennox): I copied this out of a review that is proposed against +# keystoneclient which can be used when available. +# https://review.openstack.org/#/c/143338/ +class _AccessInfoPlugin(base.BaseIdentityPlugin): + """A plugin that turns an existing AccessInfo object into a usable plugin. + + In certain circumstances you already have an auth_ref/AccessInfo object + that you just want to reuse. This could have been from a cache, in + auth_token middleware or other. + + Turn that existing object into a simple identity plugin. This plugin cannot + be refreshed as the AccessInfo object does not contain any authorizing + information. + + :param auth_ref: the existing AccessInfo object. + :type auth_ref: keystoneclient.access.AccessInfo + :param auth_url: the url where this AccessInfo was retrieved from. Required + if using the AUTH_INTERFACE with get_endpoint. (optional) + """ + + def __init__(self, auth_url, auth_ref): + super(_AccessInfoPlugin, self).__init__(auth_url=auth_url, + reauthenticate=False) + self.auth_ref = auth_ref + + def get_auth_ref(self, session, **kwargs): + return self.auth_ref + + def invalidate(self): + # NOTE(jamielennox): Don't allow the default invalidation to occur + # because on next authentication request we will only get the same + # auth_ref object again. + return False class RequestContext(context.RequestContext): @@ -35,7 +79,7 @@ class RequestContext(context.RequestContext): read_only=False, show_deleted=False, overwrite=True, trust_id=None, trustor_user_id=None, request_id=None, auth_token_info=None, region_name=None, - **kwargs): + auth_plugin=None, **kwargs): """ :param overwrite: Set to False to ensure that the greenthread local copy of the index is not overwritten. @@ -66,6 +110,7 @@ class RequestContext(context.RequestContext): self.trust_id = trust_id self.trustor_user_id = trustor_user_id self.policy = policy.Enforcer() + self._auth_plugin = auth_plugin if is_admin is None: self.is_admin = self.policy.check_is_admin(self) @@ -110,6 +155,59 @@ class RequestContext(context.RequestContext): def from_dict(cls, values): return cls(**values) + @property + def _keystone_v3_endpoint(self): + if self.auth_url: + auth_uri = self.auth_url + else: + importutils.import_module('keystonemiddleware.auth_token') + auth_uri = cfg.CONF.keystone_authtoken.auth_uri + + return auth_uri.replace('v2.0', 'v3') + + def _create_auth_plugin(self): + if self.trust_id: + importutils.import_module('keystonemiddleware.auth_token') + username = cfg.CONF.keystone_authtoken.admin_user + password = cfg.CONF.keystone_authtoken.admin_password + + return v3.Password(username=username, + password=password, + user_domain_id='default', + auth_url=self._keystone_v3_endpoint, + trust_id=self.trust_id) + + if self.auth_token_info: + auth_ref = access.AccessInfo.factory(body=self.auth_token_info, + auth_token=self.auth_token) + return _AccessInfoPlugin(self._keystone_v3_endpoint, auth_ref) + + if self.auth_token: + # FIXME(jamielennox): This is broken but consistent. If you + # only have a token but don't load a service catalog then + # url_for wont work. Stub with the keystone endpoint so at + # least it might be right. + return token_endpoint.Token(endpoint=self._keystone_v3_endpoint, + token=self.auth_token) + + if self.password: + return v3.Password(username=self.username, + password=self.password, + project_id=self.tenant_id, + user_domain_id='default', + auth_url=self._keystone_v3_endpoint) + + LOG.error(_LE("Keystone v3 API connection failed, no password " + "trust or auth_token!")) + raise exception.AuthorizationFailure() + + @property + def auth_plugin(self): + if not self._auth_plugin: + self._auth_plugin = self._create_auth_plugin() + + return self._auth_plugin + def get_admin_context(show_deleted=False): return RequestContext(is_admin=True, show_deleted=show_deleted) @@ -160,6 +258,7 @@ class ContextMiddleware(wsgi.Middleware): if roles is not None: roles = roles.split(',') token_info = environ.get('keystone.token_info') + auth_plugin = environ.get('keystone.token_auth') req_id = environ.get(oslo_request_id.ENV_REQUEST_ID) except Exception: @@ -175,7 +274,8 @@ class ContextMiddleware(wsgi.Middleware): roles=roles, request_id=req_id, auth_token_info=token_info, - region_name=region_name) + region_name=region_name, + auth_plugin=auth_plugin) def ContextMiddleware_filter_factory(global_conf, **local_conf): diff --git a/heat/common/heat_keystoneclient.py b/heat/common/heat_keystoneclient.py index 4de855152b..3db96523fe 100644 --- a/heat/common/heat_keystoneclient.py +++ b/heat/common/heat_keystoneclient.py @@ -14,11 +14,9 @@ """Keystone Client functionality for use by resources.""" import collections -import copy import json import uuid -from keystoneclient.auth.identity import v3 as kc_auth_v3 import keystoneclient.exceptions as kc_exception from keystoneclient import session from keystoneclient.v3 import client as kc_v3 @@ -75,6 +73,8 @@ class KeystoneClientV3(object): self._admin_client = None self._domain_admin_client = None + self.session = session.Session.construct(self._ssl_options()) + if self.context.auth_url: self.v3_endpoint = self.context.auth_url.replace('v2.0', 'v3') else: @@ -158,68 +158,27 @@ class KeystoneClientV3(object): return self._domain_admin_client def _v3_client_init(self): - kwargs = { - 'auth_url': self.v3_endpoint, - 'endpoint': self.v3_endpoint - } - # Note try trust_id first, as we can't reuse auth_token in that case - if self.context.trust_id is not None: - # We got a trust_id, so we use the admin credentials - # to authenticate with the trust_id so we can use the - # trust impersonating the trustor user. - kwargs.update(self._service_admin_creds()) - kwargs['trust_id'] = self.context.trust_id - kwargs.pop('project_name') - elif self.context.auth_token_info is not None: - # The auth_ref version must be set according to the token version - if 'access' in self.context.auth_token_info: - kwargs['auth_ref'] = copy.deepcopy( - self.context.auth_token_info['access']) - kwargs['auth_ref']['version'] = 'v2.0' - kwargs['auth_ref']['token']['id'] = self.context.auth_token - elif 'token' in self.context.auth_token_info: - kwargs['auth_ref'] = copy.deepcopy( - self.context.auth_token_info['token']) - kwargs['auth_ref']['version'] = 'v3' - kwargs['auth_ref']['auth_token'] = self.context.auth_token - else: - LOG.error(_LE('Unknown version in auth_token_info')) - raise exception.AuthorizationFailure( - _('Unknown token version')) - elif self.context.auth_token is not None: - kwargs['token'] = self.context.auth_token - kwargs['project_id'] = self.context.tenant_id - elif self.context.password is not None: - kwargs['username'] = self.context.username - kwargs['password'] = self.context.password - kwargs['project_id'] = self.context.tenant_id - else: - LOG.error(_LE("Keystone v3 API connection failed, no password " - "trust or auth_token!")) - raise exception.AuthorizationFailure() - kwargs.update(self._ssl_options()) - client = kc_v3.Client(**kwargs) + client = kc_v3.Client(session=self.session, + auth=self.context.auth_plugin) - # If auth_ref has already be specified via auth_token_info, don't - # authenticate as we want to reuse, rather than request a new token - if 'auth_ref' not in kwargs: + if hasattr(self.context.auth_plugin, 'get_access'): + # NOTE(jamielennox): get_access returns the current token without + # reauthenticating if it's present and valid. try: - client.authenticate() + auth_ref = self.context.auth_plugin.get_access(self.session) except kc_exception.Unauthorized: LOG.error(_LE("Keystone client authentication failed")) raise exception.AuthorizationFailure() - # If we are authenticating with a trust set the context auth_token - # with the trust scoped token - if 'trust_id' in kwargs: - # Sanity check - if not client.auth_ref.trust_scoped: - LOG.error(_LE("trust token re-scoping failed!")) - raise exception.AuthorizationFailure() - # Sanity check that impersonation is effective - if self.context.trustor_user_id != client.auth_ref.user_id: - LOG.error(_LE("Trust impersonation failed")) - raise exception.AuthorizationFailure() + if self.context.trust_id: + # Sanity check + if not auth_ref.trust_scoped: + LOG.error(_LE("trust token re-scoping failed!")) + raise exception.AuthorizationFailure() + # Sanity check that impersonation is effective + if self.context.trustor_user_id != auth_ref.user_id: + LOG.error(_LE("Trust impersonation failed")) + raise exception.AuthorizationFailure() return client @@ -282,29 +241,29 @@ class KeystoneClientV3(object): # can't lookup the ID in keystoneclient unless they're admin # workaround this by getting the user_id from admin_client trustee_user_id = self.admin_client.auth_ref.user_id - trustor_user_id = self.client.auth_ref.user_id - trustor_project_id = self.client.auth_ref.project_id + trustor = self.context.auth_plugin.get_access(self.session) + # inherit the roles of the trustor, unless set trusts_delegated_roles if cfg.CONF.trusts_delegated_roles: roles = cfg.CONF.trusts_delegated_roles else: roles = self.context.roles try: - trust = self.client.trusts.create(trustor_user=trustor_user_id, + trust = self.client.trusts.create(trustor_user=trustor.user_id, trustee_user=trustee_user_id, - project=trustor_project_id, + project=trustor.project_id, impersonation=True, role_names=roles) except kc_exception.NotFound: LOG.debug("Failed to find roles %s for user %s" - % (roles, trustor_user_id)) + % (roles, trustor.user_id)) raise exception.MissingCredentialError( required=_("roles %s") % roles) trust_context = context.RequestContext.from_dict( self.context.to_dict()) trust_context.trust_id = trust.id - trust_context.trustor_user_id = trustor_user_id + trust_context.trustor_user_id = trustor.user_id return trust_context def delete_trust(self, trust_id): @@ -383,19 +342,6 @@ class KeystoneClientV3(object): # catalog (the token is expected to be used inside an instance # where a specific endpoint will be specified, and user-data # space is limited..) - if self._stack_domain_is_id: - auth = kc_auth_v3.Password(auth_url=self.v3_endpoint, - user_id=user_id, - password=password, - project_id=project_id, - user_domain_id=self.stack_domain) - else: - auth = kc_auth_v3.Password(auth_url=self.v3_endpoint, - user_id=user_id, - password=password, - project_id=project_id, - user_domain_name=self.stack_domain) - sess = session.Session(auth=auth) # Note we do this directly via a post as there's currently # no way to get a nocatalog token via keystoneclient token_url = "%s/auth/tokens?nocatalog" % self.v3_endpoint @@ -410,8 +356,8 @@ class KeystoneClientV3(object): 'domain': domain, 'password': password, 'id': user_id}}, 'methods': ['password']}}} - t = sess.post(token_url, headers=headers, json=body, - authenticated=False) + t = self.session.post(token_url, headers=headers, + json=body, authenticated=False) return t.headers['X-Subject-Token'] def create_stack_domain_user(self, username, project_id, password=None): @@ -589,7 +535,7 @@ class KeystoneClientV3(object): raise ValueError("Must specify either credential_id or access") def create_ec2_keypair(self, user_id=None): - user_id = user_id or self.client.auth_ref.user_id + user_id = user_id or self.context.get_access(self.session).user_id project_id = self.context.tenant_id data_blob = {'access': uuid.uuid4().hex, 'secret': uuid.uuid4().hex} @@ -665,11 +611,11 @@ class KeystoneClientV3(object): default_region_name = (self.context.region_name or cfg.CONF.region_name_for_services) kwargs.setdefault('region_name', default_region_name) - return self.client.service_catalog.url_for(**kwargs) + return self.context.auth_plugin.get_endpoint(self.session, **kwargs) @property def auth_token(self): - return self.client.auth_token + return self.context.auth_plugin.get_token(self.session) class KeystoneClient(object): diff --git a/heat/engine/clients/client_plugin.py b/heat/engine/clients/client_plugin.py index 75b363643d..d730695945 100644 --- a/heat/engine/clients/client_plugin.py +++ b/heat/engine/clients/client_plugin.py @@ -13,6 +13,7 @@ import abc +from keystoneclient import exceptions from oslo.config import cfg import six @@ -41,14 +42,34 @@ class ClientPlugin(object): @property def auth_token(self): - # Always use the auth_token from the keystone client, as - # this may be refreshed if the context contains credentials - # which allow reissuing of a new token before the context - # auth_token expiry (e.g trust_id or username/password) - return self.clients.client('keystone').auth_token + # NOTE(jamielennox): use the session defined by the keystoneclient + # options as traditionally the token was always retrieved from + # keystoneclient. + session = self.clients.client('keystone').session + return self.context.auth_plugin.get_token(session) def url_for(self, **kwargs): - return self.clients.client('keystone').url_for(**kwargs) + # NOTE(jamielennox): use the session defined by the keystoneclient + # options as traditionally the token was always retrieved from + # keystoneclient. + session = self.clients.client('keystone').session + + try: + kwargs.setdefault('interface', kwargs.pop('endpoint_type')) + except KeyError: + pass + + reg = self.context.region_name or cfg.CONF.region_name_for_services + kwargs.setdefault('region_name', reg) + + url = self.context.auth_plugin.get_endpoint(session, **kwargs) + + # NOTE(jamielennox): raising exception maintains compatibility with + # older keystoneclient service catalog searching. + if url is None: + raise exceptions.EndpointNotFound() + + return url def _get_client_option(self, client, option): # look for the option in the [clients_${client}] section diff --git a/heat/tests/common.py b/heat/tests/common.py index a44e1816e2..0e513a8413 100644 --- a/heat/tests/common.py +++ b/heat/tests/common.py @@ -24,6 +24,7 @@ from oslotest import mockpatch import testscenarios import testtools +from heat.common import context from heat.common import messaging from heat.engine.clients.os import cinder from heat.engine.clients.os import glance @@ -137,6 +138,13 @@ class HeatTestCase(testscenarios.WithScenarios, mockfixture = self.useFixture(mockpatch.Patch(target, **kwargs)) return mockfixture.mock + def stub_auth(self, ctx=None, **kwargs): + auth = self.patchobject(ctx or context.RequestContext, + "_create_auth_plugin") + fake_auth = fakes.FakeAuth(**kwargs) + auth.return_value = fake_auth + return auth + def stub_keystoneclient(self, fake_client=None, **kwargs): client = self.patchobject(keystone.KeystoneClientPlugin, "_create") fkc = fake_client or fakes.FakeKeystoneClient(**kwargs) diff --git a/heat/tests/fakes.py b/heat/tests/fakes.py index aae0cb64c3..e7707db16c 100644 --- a/heat/tests/fakes.py +++ b/heat/tests/fakes.py @@ -19,7 +19,8 @@ wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ -from keystoneclient import exceptions +from keystoneclient import auth +from keystoneclient import session from heat.common import context @@ -77,18 +78,34 @@ class FakeClient(object): pass +class FakeAuth(auth.BaseAuthPlugin): + + def __init__(self, auth_token='abcd1234', only_services=None): + self.auth_token = auth_token + self.only_services = only_services + + def get_token(self, session, **kwargs): + return self.auth_token + + def get_endpoint(self, session, service_type=None, **kwargs): + if (self.only_services is not None and + service_type not in self.only_services): + return None + + return 'http://example.com:1234/v1' + + class FakeKeystoneClient(object): def __init__(self, username='test_user', password='apassword', user_id='1234', access='4567', secret='8901', - credential_id='abcdxyz', auth_token='abcd1234', - only_services=None): + credential_id='abcdxyz'): self.username = username self.password = password self.user_id = user_id self.access = access self.secret = secret + self.session = session.Session() self.credential_id = credential_id - self.only_services = only_services class FakeCred(object): id = self.credential_id @@ -96,8 +113,6 @@ class FakeKeystoneClient(object): secret = self.secret self.creds = FakeCred() - self.auth_token = auth_token - def create_stack_user(self, username, password=''): self.username = username return self.user_id @@ -131,15 +146,6 @@ class FakeKeystoneClient(object): def disable_stack_user(self, user_id): pass - def url_for(self, **kwargs): - if self.only_services is not None: - if ('service_type' in kwargs and - kwargs['service_type'] not in self.only_services): - # keystone client throws keystone exceptions, not cinder - # exceptions. - raise exceptions.EndpointNotFound() - return 'http://example.com:1234/v1' - def create_trust_context(self): return context.RequestContext(username=self.username, password=self.password, diff --git a/heat/tests/test_auth_password.py b/heat/tests/test_auth_password.py index 1ba3b6a560..91d66ab368 100644 --- a/heat/tests/test_auth_password.py +++ b/heat/tests/test_auth_password.py @@ -14,8 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystoneclient.auth.identity import v3 as ks_v3_auth from keystoneclient import exceptions as keystone_exc -from keystoneclient.v3 import client as keystone_client +from keystoneclient import session as ks_session +import mox from oslo.config import cfg import webob @@ -76,6 +78,7 @@ TOKEN_V3_RESPONSE = { 'id': 'tenant_id1', 'name': 'tenant_name1', }, + 'methods': ['password'], }, 'user': { 'id': 'user_id1', @@ -120,21 +123,20 @@ class KeystonePasswordAuthProtocolTest(common.HeatTestCase): self.response_headers = dict(headers) def test_valid_v2_request(self): - mock_client = self.m.CreateMock(keystone_client.Client) - self.m.StubOutWithMock(keystone_client, 'Client') - keystone_client.Client( + mock_auth = self.m.CreateMock(ks_v3_auth.Password) + self.m.StubOutWithMock(ks_v3_auth, 'Password') + + ks_v3_auth.Password( auth_url=self.config['auth_uri'], - cacert=None, - cert=None, - endpoint=self.config['auth_uri'], - insecure=False, - key=None, password='goodpassword', project_id='tenant_id1', - username='user_name1').AndReturn(mock_client) - mock_client.auth_ref = TOKEN_V2_RESPONSE + user_domain_id='default', + username='user_name1').AndReturn(mock_auth) + + m = mock_auth.get_access(mox.IsA(ks_session.Session)) + m.AndReturn(TOKEN_V2_RESPONSE) + self.app.expected_env['keystone.token_info'] = TOKEN_V2_RESPONSE - mock_client.authenticate().AndReturn(None) self.m.ReplayAll() req = webob.Request.blank('/tenant_id1/') req.headers['X_AUTH_USER'] = 'user_name1' @@ -144,23 +146,21 @@ class KeystonePasswordAuthProtocolTest(common.HeatTestCase): self.m.VerifyAll() def test_valid_v3_request(self): - mock_client = self.m.CreateMock(keystone_client.Client) - self.m.StubOutWithMock(keystone_client, 'Client') - keystone_client.Client( - auth_url=self.config['auth_uri'], - cacert=None, - cert=None, - endpoint=self.config['auth_uri'], - insecure=False, - key=None, - password='goodpassword', - project_id='tenant_id1', - username='user_name1').AndReturn(mock_client) - mock_client.auth_ref = TOKEN_V3_RESPONSE + mock_auth = self.m.CreateMock(ks_v3_auth.Password) + self.m.StubOutWithMock(ks_v3_auth, 'Password') + + ks_v3_auth.Password(auth_url=self.config['auth_uri'], + password='goodpassword', + project_id='tenant_id1', + user_domain_id='default', + username='user_name1').AndReturn(mock_auth) + + m = mock_auth.get_access(mox.IsA(ks_session.Session)) + m.AndReturn(TOKEN_V3_RESPONSE) + self.app.expected_env['keystone.token_info'] = { 'token': TOKEN_V3_RESPONSE } - mock_client.authenticate().AndReturn(None) self.m.ReplayAll() req = webob.Request.blank('/tenant_id1/') req.headers['X_AUTH_USER'] = 'user_name1' @@ -170,18 +170,15 @@ class KeystonePasswordAuthProtocolTest(common.HeatTestCase): self.m.VerifyAll() def test_request_with_bad_credentials(self): - self.m.StubOutWithMock( - keystone_client, 'Client', use_mock_anything=True) - keystone_client.Client( - auth_url=self.config['auth_uri'], - cacert=None, - cert=None, - endpoint=self.config['auth_uri'], - insecure=False, - key=None, - password='badpassword', - project_id='tenant_id1', - username='user_name1').AndRaise(keystone_exc.Unauthorized(401)) + self.m.StubOutWithMock(ks_v3_auth, 'Password') + + m = ks_v3_auth.Password(auth_url=self.config['auth_uri'], + password='badpassword', + project_id='tenant_id1', + user_domain_id='default', + username='user_name1') + m.AndRaise(keystone_exc.Unauthorized(401)) + self.m.ReplayAll() req = webob.Request.blank('/tenant_id1/') req.headers['X_AUTH_USER'] = 'user_name1' diff --git a/heat/tests/test_clients.py b/heat/tests/test_clients.py index 47014575ed..ee042d59f7 100644 --- a/heat/tests/test_clients.py +++ b/heat/tests/test_clients.py @@ -31,6 +31,7 @@ from heat.common import exception from heat.engine import clients from heat.engine.clients import client_plugin from heat.tests import common +from heat.tests import fakes from heat.tests import utils from heat.tests.v1_1 import fakes as fakes_v1_1 @@ -87,11 +88,11 @@ class ClientsTest(common.HeatTestCase): @mock.patch.object(heatclient, 'Client') def test_clients_heat_no_auth_token(self, mock_call): - self.stub_keystoneclient(auth_token='anewtoken') con = mock.Mock() con.auth_url = "http://auth.example.com:5000/v2.0" con.tenant_id = "b363706f891f48019483f8bd6503c54b" con.auth_token = None + con.auth_plugin = fakes.FakeAuth(auth_token='anewtoken') c = clients.Clients(con) con.clients = c @@ -102,11 +103,12 @@ class ClientsTest(common.HeatTestCase): @mock.patch.object(heatclient, 'Client') def test_clients_heat_cached(self, mock_call): - self.stub_keystoneclient() + self.stub_auth() con = mock.Mock() con.auth_url = "http://auth.example.com:5000/v2.0" con.tenant_id = "b363706f891f48019483f8bd6503c54b" con.auth_token = "3bcc3d3a03f44e3d8377f9247b0ad155" + con.trust_id = None c = clients.Clients(con) con.clients = c @@ -120,22 +122,6 @@ class ClientsTest(common.HeatTestCase): heat_cached = obj.client() self.assertEqual(heat, heat_cached) - def test_clients_auth_token_update(self): - fkc = self.stub_keystoneclient(auth_token='token1') - con = mock.Mock() - con.auth_url = "http://auth.example.com:5000/v2.0" - con.trust_id = "b363706f891f48019483f8bd6503c54b" - con.username = 'heat' - con.password = 'verysecret' - con.auth_token = None - obj = clients.Clients(con) - con.clients = obj - - self.assertIsNotNone(obj.client('heat')) - self.assertEqual('token1', obj.auth_token) - fkc.auth_token = 'token2' - self.assertEqual('token2', obj.auth_token) - class FooClientsPlugin(client_plugin.ClientPlugin): @@ -177,37 +163,37 @@ class ClientPluginTest(common.HeatTestCase): def test_auth_token(self): con = mock.Mock() con.auth_token = "1234" + con.trust_id = None c = clients.Clients(con) con.clients = c - c.client = mock.Mock(name="client") - mock_keystone = mock.Mock() - c.client.return_value = mock_keystone - mock_keystone.auth_token = '5678' + con.auth_plugin = mock.Mock(name="auth_plugin") + con.auth_plugin.get_token = mock.Mock(name="get_token") + con.auth_plugin.get_token.return_value = '5678' plugin = FooClientsPlugin(con) - # assert token is from keystone rather than context + # assert token is from plugin rather than context # even though both are set self.assertEqual('5678', plugin.auth_token) - c.client.assert_called_with('keystone') + con.auth_plugin.get_token.assert_called() def test_url_for(self): con = mock.Mock() con.auth_token = "1234" + con.trust_id = None c = clients.Clients(con) con.clients = c - c.client = mock.Mock(name="client") - mock_keystone = mock.Mock() - c.client.return_value = mock_keystone - mock_keystone.url_for.return_value = 'http://192.0.2.1/foo' + con.auth_plugin = mock.Mock(name="auth_plugin") + con.auth_plugin.get_endpoint = mock.Mock(name="get_endpoint") + con.auth_plugin.get_endpoint.return_value = 'http://192.0.2.1/foo' plugin = FooClientsPlugin(con) self.assertEqual('http://192.0.2.1/foo', plugin.url_for(service_type='foo')) - c.client.assert_called_with('keystone') + con.auth_plugin.get_endpoint.assert_called() def test_abstract_create(self): con = mock.Mock() @@ -706,19 +692,19 @@ class TestIsNotFound(common.HeatTestCase): class ClientAPIVersionTest(common.HeatTestCase): def test_cinder_api_v1_and_v2(self): - self.stub_keystoneclient() + self.stub_auth() ctx = utils.dummy_context() client = clients.Clients(ctx).client('cinder') self.assertEqual(2, client.volume_api_version) def test_cinder_api_v1_only(self): - self.stub_keystoneclient(only_services=['volume']) + self.stub_auth(only_services=['volume']) ctx = utils.dummy_context() client = clients.Clients(ctx).client('cinder') self.assertEqual(1, client.volume_api_version) def test_cinder_api_v2_only(self): - self.stub_keystoneclient(only_services=['volumev2']) + self.stub_auth(only_services=['volumev2']) ctx = utils.dummy_context() client = clients.Clients(ctx).client('cinder') self.assertEqual(2, client.volume_api_version) diff --git a/heat/tests/test_heatclient.py b/heat/tests/test_heatclient.py index 44d90ac308..64cfbe49d4 100644 --- a/heat/tests/test_heatclient.py +++ b/heat/tests/test_heatclient.py @@ -14,7 +14,9 @@ import json import uuid +from keystoneclient import access as ks_access from keystoneclient.auth.identity import v3 as ks_auth_v3 +from keystoneclient.auth import token_endpoint as ks_token_endpoint import keystoneclient.exceptions as kc_exception from keystoneclient import session as ks_session from keystoneclient.v3 import client as kc_v3 @@ -23,6 +25,7 @@ import mox from oslo.config import cfg import six +from heat.common import context from heat.common import exception from heat.common import heat_keystoneclient from heat.tests import common @@ -44,6 +47,9 @@ class KeystoneClientTest(common.HeatTestCase): self.mock_ks_v3_client_domain_mngr = self.m.CreateMock( kc_v3_domains.DomainManager) self.m.StubOutWithMock(kc_v3, "Client") + self.m.StubOutWithMock(ks_auth_v3, 'Password') + self.m.StubOutWithMock(ks_token_endpoint, 'Token') + self.m.StubOutWithMock(context, '_AccessInfoPlugin') dummy_url = 'http://server.test:5000/v2.0' cfg.CONF.set_override('auth_uri', dummy_url, @@ -100,56 +106,49 @@ class KeystoneClientTest(common.HeatTestCase): self.mock_admin_client.auth_ref = self.m.CreateMockAnything() self.mock_admin_client.auth_ref.user_id = '1234' - def _stubs_v3(self, method='token', auth_ok=True, trust_scoped=True, - user_id='trustor_user_id', auth_ref=None): + def _stubs_v3(self, method='token', trust_scoped=True, + user_id='trustor_user_id', auth_ref=None, client=True, + times=1): + mock_auth_ref = self.m.CreateMockAnything() + mock_ks_auth = self.m.CreateMockAnything() if method == 'token': - kc_v3.Client( - token='abcd1234', project_id='test_tenant_id', - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - cacert=None, - cert=None, - insecure=False, - key=None).AndReturn(self.mock_ks_v3_client) + p = ks_token_endpoint.Token(token='abcd1234', + endpoint='http://server.test:5000/v3') elif method == 'auth_ref': - kc_v3.Client( - auth_ref=auth_ref, - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - cacert=None, - cert=None, - insecure=False, - key=None).AndReturn(self.mock_ks_v3_client) - elif method == 'password': - kc_v3.Client( - username='test_username', - password='password', - project_id='test_tenant_id', - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - cacert=None, - cert=None, - insecure=False, - key=None).AndReturn(self.mock_ks_v3_client) - elif method == 'trust': - kc_v3.Client( - username='heat', - password='verybadpass', - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - trust_id='atrust123', - cacert=None, - cert=None, - insecure=False, - key=None).AndReturn(self.mock_ks_v3_client) - self.mock_ks_v3_client.auth_ref = self.m.CreateMockAnything() - self.mock_ks_v3_client.auth_ref.user_id = user_id - self.mock_ks_v3_client.auth_ref.trust_scoped = trust_scoped - self.mock_ks_v3_client.auth_ref.auth_token = 'atrusttoken' + p = context._AccessInfoPlugin('http://server.test:5000/v3', + mox.IsA(ks_access.AccessInfo)) - if method != 'auth_ref': - self.mock_ks_v3_client.authenticate().AndReturn(auth_ok) + elif method == 'password': + p = ks_auth_v3.Password(auth_url='http://server.test:5000/v3', + username='test_username', + password='password', + project_id='test_tenant_id', + user_domain_id='default') + + elif method == 'trust': + p = ks_auth_v3.Password(auth_url='http://server.test:5000/v3', + username='heat', + password='verybadpass', + user_domain_id='default', + trust_id='atrust123') + + mock_auth_ref.user_id = user_id + mock_auth_ref.trust_scoped = trust_scoped + mock_auth_ref.auth_token = 'atrusttoken' + + p.AndReturn(mock_ks_auth) + + if client: + c = kc_v3.Client(session=mox.IsA(ks_session.Session), + auth=mock_ks_auth) + c.AndReturn(self.mock_ks_v3_client) + + for x in xrange(0, times): + m = mock_ks_auth.get_access(mox.IsA(ks_session.Session)) + m.AndReturn(mock_auth_ref) + + return mock_ks_auth, mock_auth_ref def test_username_length(self): """Test that user names >64 characters are properly truncated.""" @@ -427,7 +426,8 @@ class KeystoneClientTest(common.HeatTestCase): ctx.password = None ctx.trust_id = None ctx.auth_token = 'ctx_token' - ctx.auth_token_info = {'access': {'token': {'expires': '123'}}} + ctx.auth_token_info = {'access': { + 'token': {'id': 'abcd1234', 'expires': '123'}}} heat_ks_client = heat_keystoneclient.KeystoneClient(ctx) heat_ks_client.client self.assertIsNotNone(heat_ks_client._client) @@ -437,7 +437,9 @@ class KeystoneClientTest(common.HeatTestCase): """Test creating the client, token v3 auth_ref.""" expected_auth_ref = {'auth_token': 'ctx_token', - 'expires': '456', 'version': 'v3'} + 'expires': '456', + 'version': 'v3', + 'methods': []} self._stubs_v3(method='auth_ref', auth_ref=expected_auth_ref) self.m.ReplayAll() @@ -446,7 +448,7 @@ class KeystoneClientTest(common.HeatTestCase): ctx.password = None ctx.trust_id = None ctx.auth_token = 'ctx_token' - ctx.auth_token_info = {'token': {'expires': '456'}} + ctx.auth_token_info = {'token': {'expires': '456', 'methods': []}} heat_ks_client = heat_keystoneclient.KeystoneClient(ctx) heat_ks_client.client self.assertIsNotNone(heat_ks_client._client) @@ -510,19 +512,19 @@ class KeystoneClientTest(common.HeatTestCase): id = 'atrust123' self._stub_admin_client() + mock_client, mock_auth_ref = self._stubs_v3(times=2) - self._stubs_v3() cfg.CONF.set_override('deferred_auth_method', 'trusts') if delegate_roles: cfg.CONF.set_override('trusts_delegated_roles', delegate_roles) trustor_roles = ['heat_stack_owner', 'admin', '__member__'] trustee_roles = delegate_roles or trustor_roles - self.mock_ks_v3_client.auth_ref = self.m.CreateMockAnything() - self.mock_ks_v3_client.auth_ref.user_id = '5678' - self.mock_ks_v3_client.auth_ref.project_id = '42' - self.mock_ks_v3_client.trusts = self.m.CreateMockAnything() + mock_auth_ref.user_id = '5678' + mock_auth_ref.project_id = '42' + + self.mock_ks_v3_client.trusts = self.m.CreateMockAnything() self.mock_ks_v3_client.trusts.create( trustor_user='5678', trustee_user='1234', @@ -543,18 +545,15 @@ class KeystoneClientTest(common.HeatTestCase): """Test create_trust_context when creating a trust.""" - class MockTrust(object): - id = 'atrust123' - self._stub_admin_client() - self._stubs_v3() + mock_auth, mock_auth_ref = self._stubs_v3(times=2) cfg.CONF.set_override('deferred_auth_method', 'trusts') cfg.CONF.set_override('trusts_delegated_roles', ['heat_stack_owner']) - self.mock_ks_v3_client.auth_ref = self.m.CreateMockAnything() - self.mock_ks_v3_client.auth_ref.user_id = '5678' - self.mock_ks_v3_client.auth_ref.project_id = '42' + mock_auth_ref.user_id = '5678' + mock_auth_ref.project_id = '42' + self.mock_ks_v3_client.trusts = self.m.CreateMockAnything() self.mock_ks_v3_client.trusts.create( trustor_user='5678', @@ -1366,7 +1365,6 @@ class KeystoneClientTest(common.HeatTestCase): heat_ks_client.delete_stack_domain_project(project_id='aprojectid') def _stub_domain_user_pw_auth(self): - self.m.StubOutWithMock(ks_auth_v3, 'Password') ks_auth_v3.Password(auth_url='http://server.test:5000/v3', user_id='duser', password='apassw', @@ -1375,7 +1373,6 @@ class KeystoneClientTest(common.HeatTestCase): def test_stack_domain_user_token(self): """Test stack_domain_user_token function.""" - self._stub_domain_user_pw_auth() dummysession = self.m.CreateMockAnything() dummyresp = self.m.CreateMockAnything() dummyresp.headers = {'X-Subject-Token': 'dummytoken'} @@ -1384,7 +1381,7 @@ class KeystoneClientTest(common.HeatTestCase): headers={'Accept': 'application/json'}, json=mox.IgnoreArg()).AndReturn(dummyresp) self.m.StubOutWithMock(ks_session, 'Session') - ks_session.Session(auth='dummyauth').AndReturn(dummysession) + ks_session.Session.construct(mox.IsA(dict)).AndReturn(dummysession) self.m.ReplayAll() ctx = utils.dummy_context() @@ -1421,10 +1418,9 @@ class KeystoneClientTest(common.HeatTestCase): Helper function for testing url_for depending on different ways to pass region name. """ - self._stubs_v3() - self.mock_ks_v3_client.service_catalog = self.m.CreateMockAnything() - self.mock_ks_v3_client.service_catalog.url_for( - **expected_kwargs).AndReturn(service_url) + mock_ks_auth, mock_auth_ref = self._stubs_v3(client=False) + mock_ks_auth.get_endpoint(mox.IsA(ks_session.Session), + **expected_kwargs).AndReturn(service_url) self.m.ReplayAll() ctx = ctx or utils.dummy_context() @@ -1525,7 +1521,6 @@ class KeystoneClientTestDomainName(KeystoneClientTest): self.mock_admin_client.auth_ref.user_id = '1234' def _stub_domain_user_pw_auth(self): - self.m.StubOutWithMock(ks_auth_v3, 'Password') ks_auth_v3.Password(auth_url='http://server.test:5000/v3', user_id='duser', password='apassw', diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index c19d13eef8..c3c8a319c4 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -972,7 +972,7 @@ class StackTest(common.HeatTestCase): def test_no_auth_token(self): ctx = utils.dummy_context() ctx.auth_token = None - self.stub_keystoneclient() + self.stub_auth() self.m.ReplayAll() stack = parser.Stack(ctx, 'test_stack', self.tmpl) diff --git a/heat/tests/test_swift.py b/heat/tests/test_swift.py index 55cd84cce1..23423cba65 100644 --- a/heat/tests/test_swift.py +++ b/heat/tests/test_swift.py @@ -77,7 +77,7 @@ class swiftTest(common.HeatTestCase): self.m.StubOutWithMock(sc.Connection, 'delete_object') self.m.StubOutWithMock(sc.Connection, 'head_container') self.m.StubOutWithMock(sc.Connection, 'get_auth') - self.stub_keystoneclient() + self.stub_auth() def create_resource(self, t, stack, resource_name): resource_defns = stack.t.resource_definitions(stack)