]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Migrate stored credentials to keystone trusts
authorSteven Hardy <shardy@redhat.com>
Mon, 2 Sep 2013 15:32:40 +0000 (16:32 +0100)
committerSteven Hardy <shardy@redhat.com>
Tue, 3 Sep 2013 23:12:07 +0000 (00:12 +0100)
Migrate the stored user_creds, which currently only supports
storing username/password credentials to use the keystone v3
API OS-TRUST extension, which allows explicit impersonation of
users calling heat (trustors) by the heat service user (the
trustee)

Note this feature is made optional via a new config option,
defaulted to off, and it requires the following patches to
keystoneclient (in 0.3.2 release) and keystone to work:

https://review.openstack.org/#/c/39899/
https://review.openstack.org/#/c/42456/

Also note that if the feature is enabled, by setting
deferred_auth_method=trusts in heat.conf, you must add
a keystone_authtoken section, which is also used by the
keystoneclient auth_token middleware.

blueprint heat-trusts

Change-Id: I288114d827481bc0a24eba4556400d98b1a44c09

12 files changed:
etc/heat/heat.conf.sample
heat/common/config.py
heat/common/heat_keystoneclient.py
heat/engine/service.py
heat/tests/fakes.py
heat/tests/test_ceilometer_alarm.py
heat/tests/test_engine_service.py
heat/tests/test_heatclient.py
heat/tests/test_metadata_refresh.py
heat/tests/test_signal.py
heat/tests/utils.py
requirements.txt

index 90e608f5aff20a34d065c817b115e15edf404e65..d8f238fd9b92a4b88900bf16069bc4d6a19abbe0 100644 (file)
 # The directory to search for environment files (string value)
 #environment_dir=/etc/heat/environment.d
 
+# Select deferred auth method, stored password or trusts
+# (string value)
+#deferred_auth_method=password
+
+# Subset of trustor roles to be delegated to heat (list value)
+#trusts_delegated_roles=heat_stack_owner
+
 # Name of the engine node. This can be an opaque identifier.It
 # is not necessarily a hostname, FQDN, or IP address. (string
 # value)
 #cloud_backend=<None>
 
 
+#
+# Options defined in heat.openstack.common.db.sqlalchemy.session
+#
+
+# the filename to use with sqlite (string value)
+#sqlite_db=heat.sqlite
+
+# If true, use synchronous mode for sqlite (boolean value)
+#sqlite_synchronous=true
+
+
 #
 # Options defined in heat.openstack.common.eventlet_backdoor
 #
 #use_tpool=false
 
 
+#
+# Options defined in heat.openstack.common.db.sqlalchemy.session
+#
+
+# The SQLAlchemy connection string used to connect to the
+# database (string value)
+#connection=sqlite:////heat/openstack/common/db/$sqlite_db
+
+# The SQLAlchemy connection string used to connect to the
+# slave database (string value)
+#slave_connection=
+
+# timeout before idle sql connections are reaped (integer
+# value)
+#idle_timeout=3600
+
+# Minimum number of SQL connections to keep open in a pool
+# (integer value)
+#min_pool_size=1
+
+# Maximum number of SQL connections to keep open in a pool
+# (integer value)
+#max_pool_size=<None>
+
+# maximum db connection retries during startup. (setting -1
+# implies an infinite retry count) (integer value)
+#max_retries=10
+
+# interval between retries of opening a sql connection
+# (integer value)
+#retry_interval=10
+
+# If set, use this value for max_overflow with sqlalchemy
+# (integer value)
+#max_overflow=<None>
+
+# Verbosity of SQL debugging information. 0=None,
+# 100=Everything (integer value)
+#connection_debug=0
+
+# Add python stack traces to SQL as comment strings (boolean
+# value)
+#connection_trace=false
+
+# If set, use this value for pool_timeout with sqlalchemy
+# (integer value)
+#pool_timeout=<None>
+
+
 [paste_deploy]
 
 #
index c428bb63757f8e369e33d78c07ddf8a50e5e36ea..53d26b51d50a1e85887def3bea524001f866d1be 100644 (file)
@@ -89,7 +89,16 @@ engine_opts = [
                 help='List of directories to search for Plugins'),
     cfg.StrOpt('environment_dir',
                default='/etc/heat/environment.d',
-               help='The directory to search for environment files')]
+               help='The directory to search for environment files'),
+    cfg.StrOpt('deferred_auth_method',
+               choices=['password', 'trusts'],
+               default='password',
+               help=_('Select deferred auth method, '
+                      'stored password or trusts')),
+    cfg.ListOpt('trusts_delegated_roles',
+                default=['heat_stack_owner'],
+                help=_('Subset of trustor roles to be delegated to heat'))]
+
 
 rpc_opts = [
     cfg.StrOpt('host',
index 978b3ce900244bee1795900d5b56df117e8485a9..ec463d6766402c72bea696dd5544048f807ae525 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from heat.openstack.common import exception
+from heat.common import exception
 
 import eventlet
+import hashlib
+
 from keystoneclient.v2_0 import client as kc
+from keystoneclient.v3 import client as kc_v3
 from oslo.config import cfg
 
+from heat.openstack.common import importutils
 from heat.openstack.common import log as logging
 
 logger = logging.getLogger('heat.common.keystoneclient')
@@ -35,24 +39,162 @@ class KeystoneClient(object):
     """
     def __init__(self, context):
         self.context = context
+        # We have to maintain two clients authenticated with keystone:
+        # - ec2 interface is v2.0 only
+        # - trusts is v3 only
+        # - passing a v2 auth_token to the v3 client won't work until lp bug
+        #   #1212778 is fixed
+        # - passing a v3 token to the v2 client works but we have to either
+        #   md5sum it or use the nocatalog option to auth/tokens (not yet
+        #   supported by keystoneclient), or we hit the v2 8192byte size limit
+        # - context.auth_url is expected to contain the v2.0 keystone endpoint
+        if cfg.CONF.deferred_auth_method == 'trusts':
+            # Create connection to v3 API
+            self.client_v3 = self._v3_client_init()
+
+            # Set context auth_token to md5sum of v3 token
+            auth_token = self.client_v3.auth_ref.get('auth_token')
+            self.context.auth_token = self._md5_token(auth_token)
+
+            # Create the connection to the v2 API, reusing the md5-ified token
+            self.client_v2 = self._v2_client_init()
+        else:
+            # Create the connection to the v2 API, using the context creds
+            self.client_v2 = self._v2_client_init()
+            self.client_v3 = None
+
+    def _md5_token(self, auth_token):
+        # Get the md5sum of the v3 token, which we can pass instead of the
+        # actual token to avoid v2 8192byte size limit on the v2 token API
+        m_enc = hashlib.md5()
+        m_enc.update(auth_token)
+        return m_enc.hexdigest()
+
+    def _v2_client_init(self):
         kwargs = {
-            'auth_url': context.auth_url,
+            'auth_url': self.context.auth_url
         }
+        # Note check for auth_token first so we use existing token if
+        # available from v3 auth
+        if self.context.auth_token is not None:
+            kwargs['tenant_name'] = self.context.tenant
+            kwargs['token'] = self.context.auth_token
+        elif self.context.password is not None:
+            kwargs['username'] = self.context.username
+            kwargs['password'] = self.context.password
+            kwargs['tenant_name'] = self.context.tenant
+            kwargs['tenant_id'] = self.context.tenant_id
+        else:
+            logger.error("Keystone v2 API connection failed, no password or "
+                         "auth_token!")
+            raise exception.AuthorizationFailure()
+        client_v2 = kc.Client(**kwargs)
+        if not client_v2.authenticate():
+            logger.error("Keystone v2 API authentication failed")
+            raise exception.AuthorizationFailure()
+        return client_v2
 
-        if context.password is not None:
-            kwargs['username'] = context.username
-            kwargs['password'] = context.password
-            kwargs['tenant_name'] = context.tenant
-            kwargs['tenant_id'] = context.tenant_id
-        elif context.auth_token is not None:
-            kwargs['tenant_name'] = context.tenant
-            kwargs['token'] = context.auth_token
+    @staticmethod
+    def _service_admin_creds(api_version=2):
+        # Import auth_token to have keystone_authtoken settings setup.
+        importutils.import_module('keystoneclient.middleware.auth_token')
+
+        creds = {
+            'username': cfg.CONF.keystone_authtoken.admin_user,
+            'password': cfg.CONF.keystone_authtoken.admin_password,
+        }
+        if api_version >= 3:
+            creds['auth_url'] =\
+                cfg.CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3')
+            creds['project_name'] =\
+                cfg.CONF.keystone_authtoken.admin_tenant_name
         else:
-            logger.error("Keystone connection failed, no password or " +
+            creds['auth_url'] = cfg.CONF.keystone_authtoken.auth_uri
+            creds['tenant_name'] =\
+                cfg.CONF.keystone_authtoken.admin_tenant_name
+
+        return creds
+
+    def _v3_client_init(self):
+        kwargs = {}
+        if self.context.auth_token is not None:
+            kwargs['project_name'] = self.context.tenant
+            kwargs['token'] = self.context.auth_token
+            kwargs['auth_url'] = self.context.auth_url.replace('v2.0', 'v3')
+            kwargs['endpoint'] = kwargs['auth_url']
+        elif self.context.trust_id is not None:
+            # We got a trust_id, so we use the admin credentials and get a
+            # Token back impersonating the trustor user
+            kwargs.update(self._service_admin_creds(api_version=3))
+            kwargs['trust_id'] = self.context.trust_id
+        elif self.context.password is not None:
+            kwargs['username'] = self.context.username
+            kwargs['password'] = self.context.password
+            kwargs['project_name'] = self.context.tenant
+            kwargs['project_id'] = self.context.tenant_id
+            kwargs['auth_url'] = self.context.auth_url.replace('v2.0', 'v3')
+            kwargs['endpoint'] = kwargs['auth_url']
+        else:
+            logger.error("Keystone v3 API connection failed, no password or "
                          "auth_token!")
+            raise exception.AuthorizationFailure()
+
+        client_v3 = kc_v3.Client(**kwargs)
+        if not client_v3.authenticate():
+            logger.error("Keystone v3 API authentication failed")
+            raise exception.AuthorizationFailure()
+        return client_v3
+
+    def create_trust_context(self):
+        """
+        If cfg.CONF.deferred_auth_method is trusts, we create a
+        trust using the trustor identity in the current context, with the
+        trustee as the heat service user
+
+        If deferred_auth_method != trusts, we do nothing
+
+        If the current context already contains a trust_id, we do nothing
+        """
+        if cfg.CONF.deferred_auth_method != 'trusts':
             return
-        self.client = kc.Client(**kwargs)
-        self.client.authenticate()
+
+        if self.context.trust_id:
+            return
+
+        # We need the service admin user ID (not name), as the trustor user
+        # can't lookup the ID in keystoneclient unless they're admin
+        # workaround this by creating a temporary admin client connection
+        # then getting the user ID from the auth_ref
+        admin_creds = self._service_admin_creds()
+        admin_client = kc.Client(**admin_creds)
+        if not admin_client.authenticate():
+            logger.error("Keystone v2 API admin authentication failed")
+            raise exception.AuthorizationFailure()
+
+        trustee_user_id = admin_client.auth_ref['user']['id']
+        trustor_user_id = self.client_v3.auth_ref['user']['id']
+        trustor_project_id = self.client_v3.auth_ref['project']['id']
+        roles = cfg.CONF.trusts_delegated_roles
+        trust = self.client_v3.trusts.create(trustor_user=trustor_user_id,
+                                             trustee_user=trustee_user_id,
+                                             project=trustor_project_id,
+                                             impersonation=True,
+                                             role_names=roles)
+        self.context.trust_id = trust.id
+        self.context.trustor_user_id = trustor_user_id
+
+    def delete_trust_context(self):
+        """
+        If a trust_id exists in the context, we delete it
+
+        """
+        if not self.context.trust_id:
+            return
+
+        self.client_v3.trusts.delete(self.context.trust_id)
+
+        self.context.trust_id = None
+        self.context.trustor_user_id = None
 
     def create_stack_user(self, username, password=''):
         """
@@ -66,35 +208,34 @@ class KeystoneClient(object):
                            "characters." % username)
             #get the last 64 characters of the username
             username = username[-64:]
-        user = self.client.users.create(username,
-                                        password,
-                                        '%s@heat-api.org' %
-                                        username,
-                                        tenant_id=self.context.tenant_id,
-                                        enabled=True)
+        user = self.client_v2.users.create(username,
+                                           password,
+                                           '%s@heat-api.org' %
+                                           username,
+                                           tenant_id=self.context.tenant_id,
+                                           enabled=True)
 
         # We add the new user to a special keystone role
         # This role is designed to allow easier differentiation of the
         # heat-generated "stack users" which will generally have credentials
         # deployed on an instance (hence are implicitly untrusted)
-        roles = self.client.roles.list()
+        roles = self.client_v2.roles.list()
         stack_user_role = [r.id for r in roles
                            if r.name == cfg.CONF.heat_stack_user_role]
         if len(stack_user_role) == 1:
             role_id = stack_user_role[0]
             logger.debug("Adding user %s to role %s" % (user.id, role_id))
-            self.client.roles.add_user_role(user.id, role_id,
-                                            self.context.tenant_id)
+            self.client_v2.roles.add_user_role(user.id, role_id,
+                                               self.context.tenant_id)
         else:
             logger.error("Failed to add user %s to role %s, check role exists!"
-                         % (username,
-                            cfg.CONF.heat_stack_user_role))
+                         % (username, cfg.CONF.heat_stack_user_role))
 
         return user.id
 
     def delete_stack_user(self, user_id):
 
-        user = self.client.users.get(user_id)
+        user = self.client_v2.users.get(user_id)
 
         # FIXME (shardy) : need to test, do we still need this retry logic?
         # Copied from user.py, but seems like something we really shouldn't
@@ -128,16 +269,16 @@ class KeystoneClient(object):
             raise exception.Error(reason)
 
     def delete_ec2_keypair(self, user_id, accesskey):
-        self.client.ec2.delete(user_id, accesskey)
+        self.client_v2.ec2.delete(user_id, accesskey)
 
     def get_ec2_keypair(self, user_id):
         # We make the assumption that each user will only have one
         # ec2 keypair, it's not clear if AWS allow multiple AccessKey resources
         # to be associated with a single User resource, but for simplicity
         # we assume that here for now
-        cred = self.client.ec2.list(user_id)
+        cred = self.client_v2.ec2.list(user_id)
         if len(cred) == 0:
-            return self.client.ec2.create(user_id, self.context.tenant_id)
+            return self.client_v2.ec2.create(user_id, self.context.tenant_id)
         if len(cred) == 1:
             return cred[0]
         else:
@@ -146,15 +287,15 @@ class KeystoneClient(object):
 
     def disable_stack_user(self, user_id):
         # FIXME : This won't work with the v3 keystone API
-        self.client.users.update_enabled(user_id, False)
+        self.client_v2.users.update_enabled(user_id, False)
 
     def enable_stack_user(self, user_id):
         # FIXME : This won't work with the v3 keystone API
-        self.client.users.update_enabled(user_id, True)
+        self.client_v2.users.update_enabled(user_id, True)
 
     def url_for(self, **kwargs):
-        return self.client.service_catalog.url_for(**kwargs)
+        return self.client_v2.service_catalog.url_for(**kwargs)
 
     @property
     def auth_token(self):
-        return self.client.auth_token
+        return self.client_v2.auth_token
index 8e258c9fd44f1c265023b4726672a6381f272f2b..5b35bdcb004f70d242a976f381c106d82c169b2f 100644 (file)
@@ -30,6 +30,7 @@ from heat.engine.event import Event
 from heat.engine import environment
 from heat.common import exception
 from heat.common import identifier
+from heat.common import heat_keystoneclient as hkc
 from heat.engine import parameters
 from heat.engine import parser
 from heat.engine import properties
@@ -252,6 +253,11 @@ class EngineService(service.Service):
 
         stack.validate()
 
+        # Creates a trust and sets the trust_id and trustor_user_id in
+        # the current context, before we store it in stack.store()
+        # Does nothing if deferred_auth_method is 'password'
+        stack.clients.keystone().create_trust_context()
+
         stack_id = stack.store()
 
         self._start_in_thread(stack_id, _stack_create, stack)
@@ -384,6 +390,13 @@ class EngineService(service.Service):
 
         stack = parser.Stack.load(cnxt, stack=st)
 
+        # If we created a trust, delete it
+        # Note this is using the current request context, not the stored
+        # context, as it seems it's not possible to delete a trust with
+        # a token obtained via that trust.  This means that only the user
+        # who created the stack can delete it when using trusts atm.
+        stack.clients.keystone().delete_trust_context()
+
         # Kill any pending threads by calling ThreadGroup.stop()
         if st.id in self.stg:
             self.stg[st.id].stop()
@@ -529,8 +542,7 @@ class EngineService(service.Service):
         # but this happens because the keystone user associated with the
         # signal doesn't have permission to read the secret key of
         # the user associated with the cfn-credentials file
-        user_creds = db_api.user_creds_get(s.user_creds_id)
-        stack_context = context.RequestContext.from_dict(user_creds)
+        stack_context = self._load_user_creds(s.user_creds_id)
         stack = parser.Stack.load(stack_context, stack=s)
 
         if resource_name not in stack:
@@ -614,6 +626,15 @@ class EngineService(service.Service):
         stack = parser.Stack.load(cnxt, stack=s)
         self._start_in_thread(stack.id, _stack_resume, stack)
 
+    def _load_user_creds(self, creds_id):
+        user_creds = db_api.user_creds_get(creds_id)
+        stored_context = context.RequestContext.from_dict(user_creds)
+        # heat_keystoneclient populates the context with an auth_token
+        # either via the stored user/password or trust_id, depending
+        # on how deferred_auth_method is configured in the conf file
+        kc = hkc.KeystoneClient(stored_context)
+        return stored_context
+
     @request_context
     def metadata_update(self, cnxt, stack_identity,
                         resource_name, metadata):
@@ -634,8 +655,7 @@ class EngineService(service.Service):
         # but this happens because the keystone user associated with the
         # WaitCondition doesn't have permission to read the secret key of
         # the user associated with the cfn-credentials file
-        user_creds = db_api.user_creds_get(s.user_creds_id)
-        stack_context = context.RequestContext.from_dict(user_creds)
+        stack_context = self._load_user_creds(s.user_creds_id)
         refresh_stack = parser.Stack.load(stack_context, stack=s)
 
         # Refresh the metadata for all other resources, since we expect
@@ -664,8 +684,7 @@ class EngineService(service.Service):
             logger.error("Unable to retrieve stack %s for periodic task" %
                          sid)
             return
-        user_creds = db_api.user_creds_get(stack.user_creds_id)
-        stack_context = context.RequestContext.from_dict(user_creds)
+        stack_context = self._load_user_creds(stack.user_creds_id)
 
         # Get all watchrules for this stack and evaluate them
         try:
index 6e77c703129c76033d87ebe6552e72bee245b2b2..dd04279555e675a77f67417da6863267b381e08d 100644 (file)
@@ -137,3 +137,9 @@ class FakeKeystoneClient(object):
 
     def url_for(self, **kwargs):
         return 'http://example.com:1234/v1'
+
+    def create_trust_context(self):
+        pass
+
+    def delete_trust_context(self):
+        pass
index a07f0ecdc945c397a56d21c45cdbd81979c0d6b0..a7fb4c7b05e1410de8dfdd056eb2a8f49f0e974f 100644 (file)
@@ -25,7 +25,6 @@ from heat.tests import generic_resource
 from heat.tests.common import HeatTestCase
 from heat.tests import utils
 
-from heat.common import context
 from heat.common import template_format
 
 from heat.openstack.common.importutils import try_import
@@ -105,7 +104,7 @@ class CeilometerAlarmTest(HeatTestCase):
             template = alarm_template
         temp = template_format.parse(template)
         template = parser.Template(temp)
-        ctx = context.get_admin_context()
+        ctx = utils.dummy_context()
         ctx.tenant_id = 'test_tenant'
         stack = parser.Stack(ctx, utils.random_name(), template,
                              disable_rollback=True)
index 0959c49e0531e1e542772f96c2c2ea3a3693eb44..bd60bbd3c1ff87eadf08b097f87b789e31b382c5 100644 (file)
@@ -23,6 +23,7 @@ from testtools import matchers
 from oslo.config import cfg
 
 from heat.engine import environment
+from heat.common import heat_keystoneclient as hkc
 from heat.common import exception
 from heat.tests.v1_1 import fakes
 import heat.rpc.api as engine_api
@@ -298,6 +299,16 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
         self.m.StubOutWithMock(stack, 'validate')
         stack.validate().AndReturn(None)
 
+        self.m.StubOutClassWithMocks(hkc.kc, "Client")
+        mock_ks_client = hkc.kc.Client(
+            auth_url=mox.IgnoreArg(),
+            tenant_name='test_tenant',
+            token='abcd1234')
+        mock_ks_client.authenticate().AndReturn(True)
+
+        self.m.StubOutWithMock(hkc.KeystoneClient, 'create_trust_context')
+        hkc.KeystoneClient.create_trust_context().AndReturn(None)
+
         self.m.StubOutWithMock(threadgroup, 'ThreadGroup')
         threadgroup.ThreadGroup().AndReturn(DummyThreadGroup())
 
@@ -413,6 +424,16 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
 
         parser.Stack.load(self.ctx, stack=s).AndReturn(stack)
 
+        self.m.StubOutClassWithMocks(hkc.kc, "Client")
+        mock_ks_client = hkc.kc.Client(
+            auth_url=mox.IgnoreArg(),
+            tenant_name='test_tenant',
+            token='abcd1234')
+        mock_ks_client.authenticate().AndReturn(True)
+
+        self.m.StubOutWithMock(hkc.KeystoneClient, 'delete_trust_context')
+        hkc.KeystoneClient.delete_trust_context().AndReturn(None)
+
         self.man.tg = DummyThreadGroup()
 
         self.m.ReplayAll()
@@ -1185,9 +1206,9 @@ class StackServiceTest(HeatTestCase):
         service.EngineService._get_stack(self.ctx,
                                          self.stack.identifier()).AndReturn(s)
 
-        self.m.StubOutWithMock(db_api, 'user_creds_get')
-        db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn(
-            self.ctx.to_dict())
+        self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
+        service.EngineService._load_user_creds(
+            mox.IgnoreArg()).AndReturn(self.ctx)
 
         self.m.StubOutWithMock(rsrs.Resource, 'signal')
         rsrs.Resource.signal(mox.IgnoreArg()).AndReturn(None)
@@ -1215,9 +1236,9 @@ class StackServiceTest(HeatTestCase):
         service.EngineService._get_stack(self.ctx,
                                          self.stack.identifier()).AndReturn(s)
 
-        self.m.StubOutWithMock(db_api, 'user_creds_get')
-        db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn(
-            self.ctx.to_dict())
+        self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
+        service.EngineService._load_user_creds(
+            mox.IgnoreArg()).AndReturn(self.ctx)
         self.m.ReplayAll()
 
         self.assertRaises(exception.ResourceNotFound,
@@ -1238,10 +1259,10 @@ class StackServiceTest(HeatTestCase):
         service.EngineService._get_stack(self.ctx,
                                          self.stack.identifier()).AndReturn(s)
         self.m.StubOutWithMock(instances.Instance, 'metadata_update')
-        self.m.StubOutWithMock(db_api, 'user_creds_get')
         instances.Instance.metadata_update(new_metadata=test_metadata)
-        db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn(
-            self.ctx.to_dict())
+        self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
+        service.EngineService._load_user_creds(
+            mox.IgnoreArg()).AndReturn(self.ctx)
         self.m.ReplayAll()
 
         result = self.eng.metadata_update(self.ctx,
index fb655a6bb881ef9627f342d844cde0fe929c530e..77755a4e693202e728f03647e7d89699a591a4f6 100644 (file)
 
 import mox
 
+from oslo.config import cfg
+
+from heat.common import exception
 from heat.common import heat_keystoneclient
 from heat.tests.common import HeatTestCase
 from heat.tests import utils
 
+from heat.openstack.common import importutils
+
 
 class KeystoneClientTest(HeatTestCase):
     """Test cases for heat.common.heat_keystoneclient."""
 
     def setUp(self):
         super(KeystoneClientTest, self).setUp()
-        # load config so role checking doesn't barf
-        # mock the internal keystone client and its authentication
-        self.m.StubOutClassWithMocks(heat_keystoneclient.kc, "Client")
-        self.mock_ks_client = heat_keystoneclient.kc.Client(
-            auth_url=mox.IgnoreArg(),
-            password=mox.IgnoreArg(),
-            tenant_id=mox.IgnoreArg(),
-            tenant_name=mox.IgnoreArg(),
-            username=mox.IgnoreArg())
-        self.mock_ks_client.authenticate().AndReturn(True)
-        # verify all the things
+
+        # Import auth_token to have keystone_authtoken settings setup.
+        importutils.import_module('keystoneclient.middleware.auth_token')
+
+        dummy_url = 'http://_testnoexisthost_:5000/v2.0'
+        cfg.CONF.set_override('auth_uri', dummy_url,
+                              group='keystone_authtoken')
+        cfg.CONF.set_override('admin_user', 'heat',
+                              group='keystone_authtoken')
+        cfg.CONF.set_override('admin_password', 'verybadpass',
+                              group='keystone_authtoken')
+        cfg.CONF.set_override('admin_tenant_name', 'service',
+                              group='keystone_authtoken')
         self.addCleanup(self.m.VerifyAll)
 
+    def tearDown(self):
+        super(KeystoneClientTest, self).tearDown()
+        cfg.CONF.clear_override('deferred_auth_method')
+        cfg.CONF.clear_override('auth_uri', group='keystone_authtoken')
+        cfg.CONF.clear_override('admin_user', group='keystone_authtoken')
+        cfg.CONF.clear_override('admin_password', group='keystone_authtoken')
+        cfg.CONF.clear_override('admin_tenant_name',
+                                group='keystone_authtoken')
+
+    def _stubs_v2(self, method='token', auth_ok=True):
+        self.m.StubOutClassWithMocks(heat_keystoneclient.kc, "Client")
+        if method == 'token':
+            self.mock_ks_client = heat_keystoneclient.kc.Client(
+                auth_url=mox.IgnoreArg(),
+                tenant_name='test_tenant',
+                token='abcd1234')
+            self.mock_ks_client.authenticate().AndReturn(auth_ok)
+        elif method == 'password':
+            self.mock_ks_client = heat_keystoneclient.kc.Client(
+                auth_url=mox.IgnoreArg(),
+                tenant_name='test_tenant',
+                tenant_id='test_tenant_id',
+                username='test_username',
+                password='password')
+            self.mock_ks_client.authenticate().AndReturn(auth_ok)
+
+    def _stubs_v3(self, method='token', auth_ok=True):
+        self.m.StubOutClassWithMocks(heat_keystoneclient.kc, "Client")
+        self.m.StubOutClassWithMocks(heat_keystoneclient.kc_v3, "Client")
+
+        if method == 'token':
+            self.mock_ks_v3_client = heat_keystoneclient.kc_v3.Client(
+                token='abcd1234', project_name='test_tenant',
+                auth_url='http://_testnoexisthost_:5000/v3',
+                endpoint='http://_testnoexisthost_:5000/v3')
+        elif method == 'password':
+            self.mock_ks_v3_client = heat_keystoneclient.kc_v3.Client(
+                username='test_username',
+                password='password',
+                project_name='test_tenant',
+                project_id='test_tenant_id',
+                auth_url='http://_testnoexisthost_:5000/v3',
+                endpoint='http://_testnoexisthost_:5000/v3')
+        elif method == 'trust':
+            self.mock_ks_v3_client = heat_keystoneclient.kc_v3.Client(
+                username='heat',
+                password='verybadpass',
+                project_name='service',
+                auth_url='http://_testnoexisthost_:5000/v3',
+                trust_id='atrust123')
+
+        self.mock_ks_v3_client.authenticate().AndReturn(auth_ok)
+        if auth_ok:
+            self.mock_ks_v3_client.auth_ref = self.m.CreateMockAnything()
+            self.mock_ks_v3_client.auth_ref.get('auth_token').AndReturn(
+                'av3token')
+            self.mock_ks_client = heat_keystoneclient.kc.Client(
+                auth_url=mox.IgnoreArg(),
+                tenant_name='test_tenant',
+                token='4b97cc1b2454e137ee2e8261e115bbe8')
+            self.mock_ks_client.authenticate().AndReturn(auth_ok)
+
     def test_username_length(self):
         """Test that user names >64 characters are properly truncated."""
 
+        self._stubs_v2()
+
         # a >64 character user name and the expected version
         long_user_name = 'U' * 64 + 'S'
         good_user_name = long_user_name[-64:]
@@ -64,3 +135,230 @@ class KeystoneClientTest(HeatTestCase):
         heat_ks_client = heat_keystoneclient.KeystoneClient(
             utils.dummy_context())
         heat_ks_client.create_stack_user(long_user_name, password='password')
+
+    def test_init_v2_password(self):
+
+        """Test creating the client without trusts, user/password context."""
+
+        self._stubs_v2(method='password')
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNotNone(heat_ks_client.client_v2)
+        self.assertIsNone(heat_ks_client.client_v3)
+
+    def test_init_v2_bad_nocreds(self):
+
+        """Test creating the client without trusts, no credentials."""
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        ctx.username = None
+        ctx.password = None
+        self.assertRaises(exception.AuthorizationFailure,
+                          heat_keystoneclient.KeystoneClient, ctx)
+
+    def test_init_v2_bad_denied(self):
+
+        """Test creating the client without trusts, auth failure."""
+
+        self._stubs_v2(method='password', auth_ok=False)
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        self.assertRaises(exception.AuthorizationFailure,
+                          heat_keystoneclient.KeystoneClient, ctx)
+
+    def test_init_v3_token(self):
+
+        """Test creating the client with trusts, token auth."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3()
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.username = None
+        ctx.password = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNotNone(heat_ks_client.client_v2)
+        self.assertIsNotNone(heat_ks_client.client_v3)
+
+    def test_init_v3_password(self):
+
+        """Test creating the client with trusts, password auth."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3(method='password')
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        ctx.trust_id = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNotNone(heat_ks_client.client_v2)
+        self.assertIsNotNone(heat_ks_client.client_v3)
+
+    def test_init_v3_bad_nocreds(self):
+
+        """Test creating the client with trusts, no credentials."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        ctx.trust_id = None
+        ctx.username = None
+        ctx.password = None
+        self.assertRaises(exception.AuthorizationFailure,
+                          heat_keystoneclient.KeystoneClient, ctx)
+
+    def test_init_v3_bad_denied(self):
+
+        """Test creating the client with trusts, auth failure."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3(method='password', auth_ok=False)
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        ctx.trust_id = None
+        self.assertRaises(exception.AuthorizationFailure,
+                          heat_keystoneclient.KeystoneClient, ctx)
+
+    def test_create_trust_context_notrust(self):
+
+        """Test create_trust_context with trusts disabled."""
+
+        self._stubs_v2(method='password')
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.auth_token = None
+        ctx.trust_id = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNone(heat_ks_client.create_trust_context())
+
+    def test_create_trust_context_trust_id(self):
+
+        """Test create_trust_context with existing trust_id."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3()
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNone(heat_ks_client.create_trust_context())
+
+    def test_create_trust_context_trust_create(self):
+
+        """Test create_trust_context when creating a new trust."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        class MockTrust(object):
+            id = 'atrust123'
+
+        self._stubs_v3()
+        mock_admin_client = heat_keystoneclient.kc.Client(
+            auth_url=mox.IgnoreArg(),
+            username='heat',
+            password='verybadpass',
+            tenant_name='service')
+        mock_admin_client.authenticate().AndReturn(True)
+        mock_admin_client.auth_ref = self.m.CreateMockAnything()
+        mock_admin_client.auth_ref.__getitem__('user').AndReturn(
+            {'id': '1234'})
+        self.mock_ks_v3_client.auth_ref.__getitem__('user').AndReturn(
+            {'id': '5678'})
+        self.mock_ks_v3_client.auth_ref.__getitem__('project').AndReturn(
+            {'id': '42'})
+        self.mock_ks_v3_client.trusts = self.m.CreateMockAnything()
+        self.mock_ks_v3_client.trusts.create(
+            trustor_user='5678',
+            trustee_user='1234',
+            project='42',
+            impersonation=True,
+            role_names=['heat_stack_owner']).AndReturn(MockTrust())
+
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.trust_id = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNone(heat_ks_client.create_trust_context())
+        self.assertEqual(ctx.trust_id, 'atrust123')
+        self.assertEqual(ctx.trustor_user_id, '5678')
+
+    def test_create_trust_context_denied(self):
+
+        """Test create_trust_context when creating admin auth fails."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3()
+        mock_admin_client = heat_keystoneclient.kc.Client(
+            auth_url=mox.IgnoreArg(),
+            username='heat',
+            password='verybadpass',
+            tenant_name='service')
+        mock_admin_client.authenticate().AndReturn(False)
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.trust_id = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertRaises(exception.AuthorizationFailure,
+                          heat_ks_client.create_trust_context)
+
+    def test_trust_init(self):
+
+        """Test consuming a trust when initializing."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3(method='trust')
+        self.m.ReplayAll()
+
+        ctx = utils.dummy_context()
+        ctx.username = None
+        ctx.password = None
+        ctx.auth_token = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+
+    def test_delete_trust_context(self):
+
+        """Test delete_trust_context when deleting trust."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3()
+        self.mock_ks_v3_client.trusts = self.m.CreateMockAnything()
+        self.mock_ks_v3_client.trusts.delete('atrust123').AndReturn(None)
+
+        self.m.ReplayAll()
+        ctx = utils.dummy_context()
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNone(heat_ks_client.delete_trust_context())
+
+    def test_delete_trust_context_notrust(self):
+
+        """Test delete_trust_context no trust_id specified."""
+
+        cfg.CONF.set_override('deferred_auth_method', 'trusts')
+
+        self._stubs_v3()
+        self.m.ReplayAll()
+        ctx = utils.dummy_context()
+        ctx.trust_id = None
+        heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
+        self.assertIsNone(heat_ks_client.delete_trust_context())
index a3f0598adb2cdb2474670cc54724e7027f9c02f5..5221ca9d6e3bf4424e949fca3bacb3921b145bbe 100644 (file)
@@ -20,7 +20,6 @@ from heat.tests import fakes
 from heat.tests.common import HeatTestCase
 from heat.tests import utils
 
-from heat.db import api as db_api
 from heat.engine import environment
 from heat.common import identifier
 from heat.common import template_format
@@ -203,8 +202,8 @@ class WaitCondMetadataUpdateTest(HeatTestCase):
     def create_stack(self, stack_name='test_stack'):
         temp = template_format.parse(test_template_waitcondition)
         template = parser.Template(temp)
-        stack = parser.Stack(utils.dummy_context(), stack_name, template,
-                             disable_rollback=True)
+        ctx = utils.dummy_context()
+        stack = parser.Stack(ctx, stack_name, template, disable_rollback=True)
 
         self.stack_id = stack.store()
 
@@ -223,7 +222,9 @@ class WaitCondMetadataUpdateTest(HeatTestCase):
         wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id)
 
         self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep')
-        self.m.StubOutWithMock(db_api, 'user_creds_get')
+        self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
+        service.EngineService._load_user_creds(
+            mox.IgnoreArg()).MultipleTimes().AndReturn(ctx)
 
         return stack
 
@@ -258,8 +259,6 @@ class WaitCondMetadataUpdateTest(HeatTestCase):
 
         scheduler.TaskRunner._sleep(mox.IsA(int)).WithSideEffects(check_empty)
         scheduler.TaskRunner._sleep(mox.IsA(int)).WithSideEffects(post_success)
-        db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn(
-            self.stack.context.to_dict())
         scheduler.TaskRunner._sleep(mox.IsA(int)).AndReturn(None)
 
         self.m.ReplayAll()
index 68d42835a6b091637fc4e994e04846120cb31321..5cbbf9cbb9c4e9e491a759a4a76740287a12d6b2 100644 (file)
@@ -21,7 +21,6 @@ from heat.tests import fakes
 from heat.tests.common import HeatTestCase
 from heat.tests import utils
 
-from heat.common import context
 from heat.common import exception
 from heat.common import template_format
 
@@ -72,7 +71,7 @@ class SignalTest(HeatTestCase):
     def create_stack(self, stack_name='test_stack', stub=True):
         temp = template_format.parse(test_template_signal)
         template = parser.Template(temp)
-        ctx = context.get_admin_context()
+        ctx = utils.dummy_context()
         ctx.tenant_id = 'test_tenant'
         stack = parser.Stack(ctx, stack_name, template,
                              disable_rollback=True)
@@ -85,6 +84,9 @@ class SignalTest(HeatTestCase):
             self.m.StubOutWithMock(sr.SignalResponder, 'keystone')
             sr.SignalResponder.keystone().MultipleTimes().AndReturn(
                 self.fc)
+
+        self.m.ReplayAll()
+
         return stack
 
     @utils.stack_delete_after
index 01a285ed403b698ec93da375876b884605fe07e2..f5b0c2c2de95a72e032fa99c2e96cbcfb5227ed2 100644 (file)
@@ -136,6 +136,7 @@ def dummy_context(user='test_username', tenant_id='test_tenant_id',
         'username': user,
         'password': password,
         'roles': roles,
+        'trust_id': 'atrust123',
         'auth_url': 'http://_testnoexisthost_:5000/v2.0',
         'auth_token': 'abcd1234'
     })
index c9f0f9cd5db49defc5bce421d2aa5f0218ae9f0b..6fc84de2896f4a0c63688e6a604f4a8b01768005 100644 (file)
@@ -15,12 +15,13 @@ PasteDeploy>=1.5.0
 Routes>=1.12.3
 SQLAlchemy>=0.7.8,<=0.7.99
 WebOb>=1.2.3,<1.3
-python-keystoneclient>=0.3.0
+python-keystoneclient>=0.3.2
 python-swiftclient>=1.2
 python-neutronclient>=2.2.3,<3
 python-ceilometerclient>=1.0.2
 python-cinderclient>=1.0.4
 PyYAML>=3.1.0
-oslo.config>=1.1.0
 paramiko>=1.8.0
 Babel>=0.9.6
+-f http://tarballs.openstack.org/oslo.config/oslo.config-1.2.0a3.tar.gz#egg=oslo.config-1.2.0a3
+oslo.config>=1.2.0a3