Migrate stored credentials to keystone trusts

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
This commit is contained in:
Steven Hardy 2013-09-02 16:32:40 +01:00
parent ff0122f83f
commit e686699b00
12 changed files with 636 additions and 73 deletions

View File

@ -24,6 +24,13 @@
# The directory to search for environment files (string value) # The directory to search for environment files (string value)
#environment_dir=/etc/heat/environment.d #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 # Name of the engine node. This can be an opaque identifier.It
# is not necessarily a hostname, FQDN, or IP address. (string # is not necessarily a hostname, FQDN, or IP address. (string
# value) # value)
@ -85,6 +92,17 @@
#cloud_backend=<None> #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 # Options defined in heat.openstack.common.eventlet_backdoor
# #
@ -460,6 +478,55 @@
#use_tpool=false #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] [paste_deploy]
# #

View File

@ -89,7 +89,16 @@ engine_opts = [
help='List of directories to search for Plugins'), help='List of directories to search for Plugins'),
cfg.StrOpt('environment_dir', cfg.StrOpt('environment_dir',
default='/etc/heat/environment.d', 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 = [ rpc_opts = [
cfg.StrOpt('host', cfg.StrOpt('host',

View File

@ -13,12 +13,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from heat.openstack.common import exception from heat.common import exception
import eventlet import eventlet
import hashlib
from keystoneclient.v2_0 import client as kc from keystoneclient.v2_0 import client as kc
from keystoneclient.v3 import client as kc_v3
from oslo.config import cfg from oslo.config import cfg
from heat.openstack.common import importutils
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
logger = logging.getLogger('heat.common.keystoneclient') logger = logging.getLogger('heat.common.keystoneclient')
@ -35,24 +39,162 @@ class KeystoneClient(object):
""" """
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
kwargs = { # We have to maintain two clients authenticated with keystone:
'auth_url': context.auth_url, # - 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()
if context.password is not None: # Set context auth_token to md5sum of v3 token
kwargs['username'] = context.username auth_token = self.client_v3.auth_ref.get('auth_token')
kwargs['password'] = context.password self.context.auth_token = self._md5_token(auth_token)
kwargs['tenant_name'] = context.tenant
kwargs['tenant_id'] = context.tenant_id # Create the connection to the v2 API, reusing the md5-ified token
elif context.auth_token is not None: self.client_v2 = self._v2_client_init()
kwargs['tenant_name'] = context.tenant
kwargs['token'] = context.auth_token
else: else:
logger.error("Keystone connection failed, no password or " + # 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': 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!") "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
@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:
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 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=''): def create_stack_user(self, username, password=''):
""" """
@ -66,35 +208,34 @@ class KeystoneClient(object):
"characters." % username) "characters." % username)
#get the last 64 characters of the username #get the last 64 characters of the username
username = username[-64:] username = username[-64:]
user = self.client.users.create(username, user = self.client_v2.users.create(username,
password, password,
'%s@heat-api.org' % '%s@heat-api.org' %
username, username,
tenant_id=self.context.tenant_id, tenant_id=self.context.tenant_id,
enabled=True) enabled=True)
# We add the new user to a special keystone role # We add the new user to a special keystone role
# This role is designed to allow easier differentiation of the # This role is designed to allow easier differentiation of the
# heat-generated "stack users" which will generally have credentials # heat-generated "stack users" which will generally have credentials
# deployed on an instance (hence are implicitly untrusted) # 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 stack_user_role = [r.id for r in roles
if r.name == cfg.CONF.heat_stack_user_role] if r.name == cfg.CONF.heat_stack_user_role]
if len(stack_user_role) == 1: if len(stack_user_role) == 1:
role_id = stack_user_role[0] role_id = stack_user_role[0]
logger.debug("Adding user %s to role %s" % (user.id, role_id)) logger.debug("Adding user %s to role %s" % (user.id, role_id))
self.client.roles.add_user_role(user.id, role_id, self.client_v2.roles.add_user_role(user.id, role_id,
self.context.tenant_id) self.context.tenant_id)
else: else:
logger.error("Failed to add user %s to role %s, check role exists!" logger.error("Failed to add user %s to role %s, check role exists!"
% (username, % (username, cfg.CONF.heat_stack_user_role))
cfg.CONF.heat_stack_user_role))
return user.id return user.id
def delete_stack_user(self, 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? # FIXME (shardy) : need to test, do we still need this retry logic?
# Copied from user.py, but seems like something we really shouldn't # Copied from user.py, but seems like something we really shouldn't
@ -128,16 +269,16 @@ class KeystoneClient(object):
raise exception.Error(reason) raise exception.Error(reason)
def delete_ec2_keypair(self, user_id, accesskey): 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): def get_ec2_keypair(self, user_id):
# We make the assumption that each user will only have one # We make the assumption that each user will only have one
# ec2 keypair, it's not clear if AWS allow multiple AccessKey resources # ec2 keypair, it's not clear if AWS allow multiple AccessKey resources
# to be associated with a single User resource, but for simplicity # to be associated with a single User resource, but for simplicity
# we assume that here for now # 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: 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: if len(cred) == 1:
return cred[0] return cred[0]
else: else:
@ -146,15 +287,15 @@ class KeystoneClient(object):
def disable_stack_user(self, user_id): def disable_stack_user(self, user_id):
# FIXME : This won't work with the v3 keystone API # 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): def enable_stack_user(self, user_id):
# FIXME : This won't work with the v3 keystone API # 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): def url_for(self, **kwargs):
return self.client.service_catalog.url_for(**kwargs) return self.client_v2.service_catalog.url_for(**kwargs)
@property @property
def auth_token(self): def auth_token(self):
return self.client.auth_token return self.client_v2.auth_token

View File

@ -30,6 +30,7 @@ from heat.engine.event import Event
from heat.engine import environment from heat.engine import environment
from heat.common import exception from heat.common import exception
from heat.common import identifier from heat.common import identifier
from heat.common import heat_keystoneclient as hkc
from heat.engine import parameters from heat.engine import parameters
from heat.engine import parser from heat.engine import parser
from heat.engine import properties from heat.engine import properties
@ -252,6 +253,11 @@ class EngineService(service.Service):
stack.validate() 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() stack_id = stack.store()
self._start_in_thread(stack_id, _stack_create, stack) self._start_in_thread(stack_id, _stack_create, stack)
@ -384,6 +390,13 @@ class EngineService(service.Service):
stack = parser.Stack.load(cnxt, stack=st) 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() # Kill any pending threads by calling ThreadGroup.stop()
if st.id in self.stg: if st.id in self.stg:
self.stg[st.id].stop() self.stg[st.id].stop()
@ -529,8 +542,7 @@ class EngineService(service.Service):
# but this happens because the keystone user associated with the # but this happens because the keystone user associated with the
# signal doesn't have permission to read the secret key of # signal doesn't have permission to read the secret key of
# the user associated with the cfn-credentials file # the user associated with the cfn-credentials file
user_creds = db_api.user_creds_get(s.user_creds_id) stack_context = self._load_user_creds(s.user_creds_id)
stack_context = context.RequestContext.from_dict(user_creds)
stack = parser.Stack.load(stack_context, stack=s) stack = parser.Stack.load(stack_context, stack=s)
if resource_name not in stack: if resource_name not in stack:
@ -614,6 +626,15 @@ class EngineService(service.Service):
stack = parser.Stack.load(cnxt, stack=s) stack = parser.Stack.load(cnxt, stack=s)
self._start_in_thread(stack.id, _stack_resume, stack) 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 @request_context
def metadata_update(self, cnxt, stack_identity, def metadata_update(self, cnxt, stack_identity,
resource_name, metadata): resource_name, metadata):
@ -634,8 +655,7 @@ class EngineService(service.Service):
# but this happens because the keystone user associated with the # but this happens because the keystone user associated with the
# WaitCondition doesn't have permission to read the secret key of # WaitCondition doesn't have permission to read the secret key of
# the user associated with the cfn-credentials file # the user associated with the cfn-credentials file
user_creds = db_api.user_creds_get(s.user_creds_id) stack_context = self._load_user_creds(s.user_creds_id)
stack_context = context.RequestContext.from_dict(user_creds)
refresh_stack = parser.Stack.load(stack_context, stack=s) refresh_stack = parser.Stack.load(stack_context, stack=s)
# Refresh the metadata for all other resources, since we expect # 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" % logger.error("Unable to retrieve stack %s for periodic task" %
sid) sid)
return return
user_creds = db_api.user_creds_get(stack.user_creds_id) stack_context = self._load_user_creds(stack.user_creds_id)
stack_context = context.RequestContext.from_dict(user_creds)
# Get all watchrules for this stack and evaluate them # Get all watchrules for this stack and evaluate them
try: try:

View File

@ -137,3 +137,9 @@ class FakeKeystoneClient(object):
def url_for(self, **kwargs): def url_for(self, **kwargs):
return 'http://example.com:1234/v1' return 'http://example.com:1234/v1'
def create_trust_context(self):
pass
def delete_trust_context(self):
pass

View File

@ -25,7 +25,6 @@ from heat.tests import generic_resource
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
from heat.common import context
from heat.common import template_format from heat.common import template_format
from heat.openstack.common.importutils import try_import from heat.openstack.common.importutils import try_import
@ -105,7 +104,7 @@ class CeilometerAlarmTest(HeatTestCase):
template = alarm_template template = alarm_template
temp = template_format.parse(template) temp = template_format.parse(template)
template = parser.Template(temp) template = parser.Template(temp)
ctx = context.get_admin_context() ctx = utils.dummy_context()
ctx.tenant_id = 'test_tenant' ctx.tenant_id = 'test_tenant'
stack = parser.Stack(ctx, utils.random_name(), template, stack = parser.Stack(ctx, utils.random_name(), template,
disable_rollback=True) disable_rollback=True)

View File

@ -23,6 +23,7 @@ from testtools import matchers
from oslo.config import cfg from oslo.config import cfg
from heat.engine import environment from heat.engine import environment
from heat.common import heat_keystoneclient as hkc
from heat.common import exception from heat.common import exception
from heat.tests.v1_1 import fakes from heat.tests.v1_1 import fakes
import heat.rpc.api as engine_api import heat.rpc.api as engine_api
@ -298,6 +299,16 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
self.m.StubOutWithMock(stack, 'validate') self.m.StubOutWithMock(stack, 'validate')
stack.validate().AndReturn(None) 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') self.m.StubOutWithMock(threadgroup, 'ThreadGroup')
threadgroup.ThreadGroup().AndReturn(DummyThreadGroup()) threadgroup.ThreadGroup().AndReturn(DummyThreadGroup())
@ -413,6 +424,16 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
parser.Stack.load(self.ctx, stack=s).AndReturn(stack) 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.man.tg = DummyThreadGroup()
self.m.ReplayAll() self.m.ReplayAll()
@ -1185,9 +1206,9 @@ class StackServiceTest(HeatTestCase):
service.EngineService._get_stack(self.ctx, service.EngineService._get_stack(self.ctx,
self.stack.identifier()).AndReturn(s) self.stack.identifier()).AndReturn(s)
self.m.StubOutWithMock(db_api, 'user_creds_get') self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn( service.EngineService._load_user_creds(
self.ctx.to_dict()) mox.IgnoreArg()).AndReturn(self.ctx)
self.m.StubOutWithMock(rsrs.Resource, 'signal') self.m.StubOutWithMock(rsrs.Resource, 'signal')
rsrs.Resource.signal(mox.IgnoreArg()).AndReturn(None) rsrs.Resource.signal(mox.IgnoreArg()).AndReturn(None)
@ -1215,9 +1236,9 @@ class StackServiceTest(HeatTestCase):
service.EngineService._get_stack(self.ctx, service.EngineService._get_stack(self.ctx,
self.stack.identifier()).AndReturn(s) self.stack.identifier()).AndReturn(s)
self.m.StubOutWithMock(db_api, 'user_creds_get') self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn( service.EngineService._load_user_creds(
self.ctx.to_dict()) mox.IgnoreArg()).AndReturn(self.ctx)
self.m.ReplayAll() self.m.ReplayAll()
self.assertRaises(exception.ResourceNotFound, self.assertRaises(exception.ResourceNotFound,
@ -1238,10 +1259,10 @@ class StackServiceTest(HeatTestCase):
service.EngineService._get_stack(self.ctx, service.EngineService._get_stack(self.ctx,
self.stack.identifier()).AndReturn(s) self.stack.identifier()).AndReturn(s)
self.m.StubOutWithMock(instances.Instance, 'metadata_update') self.m.StubOutWithMock(instances.Instance, 'metadata_update')
self.m.StubOutWithMock(db_api, 'user_creds_get')
instances.Instance.metadata_update(new_metadata=test_metadata) instances.Instance.metadata_update(new_metadata=test_metadata)
db_api.user_creds_get(mox.IgnoreArg()).MultipleTimes().AndReturn( self.m.StubOutWithMock(service.EngineService, '_load_user_creds')
self.ctx.to_dict()) service.EngineService._load_user_creds(
mox.IgnoreArg()).AndReturn(self.ctx)
self.m.ReplayAll() self.m.ReplayAll()
result = self.eng.metadata_update(self.ctx, result = self.eng.metadata_update(self.ctx,

View File

@ -14,32 +14,103 @@
import mox import mox
from oslo.config import cfg
from heat.common import exception
from heat.common import heat_keystoneclient from heat.common import heat_keystoneclient
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
from heat.openstack.common import importutils
class KeystoneClientTest(HeatTestCase): class KeystoneClientTest(HeatTestCase):
"""Test cases for heat.common.heat_keystoneclient.""" """Test cases for heat.common.heat_keystoneclient."""
def setUp(self): def setUp(self):
super(KeystoneClientTest, self).setUp() super(KeystoneClientTest, self).setUp()
# load config so role checking doesn't barf
# mock the internal keystone client and its authentication # Import auth_token to have keystone_authtoken settings setup.
self.m.StubOutClassWithMocks(heat_keystoneclient.kc, "Client") importutils.import_module('keystoneclient.middleware.auth_token')
self.mock_ks_client = heat_keystoneclient.kc.Client(
auth_url=mox.IgnoreArg(), dummy_url = 'http://_testnoexisthost_:5000/v2.0'
password=mox.IgnoreArg(), cfg.CONF.set_override('auth_uri', dummy_url,
tenant_id=mox.IgnoreArg(), group='keystone_authtoken')
tenant_name=mox.IgnoreArg(), cfg.CONF.set_override('admin_user', 'heat',
username=mox.IgnoreArg()) group='keystone_authtoken')
self.mock_ks_client.authenticate().AndReturn(True) cfg.CONF.set_override('admin_password', 'verybadpass',
# verify all the things group='keystone_authtoken')
cfg.CONF.set_override('admin_tenant_name', 'service',
group='keystone_authtoken')
self.addCleanup(self.m.VerifyAll) 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): def test_username_length(self):
"""Test that user names >64 characters are properly truncated.""" """Test that user names >64 characters are properly truncated."""
self._stubs_v2()
# a >64 character user name and the expected version # a >64 character user name and the expected version
long_user_name = 'U' * 64 + 'S' long_user_name = 'U' * 64 + 'S'
good_user_name = long_user_name[-64:] good_user_name = long_user_name[-64:]
@ -64,3 +135,230 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client = heat_keystoneclient.KeystoneClient( heat_ks_client = heat_keystoneclient.KeystoneClient(
utils.dummy_context()) utils.dummy_context())
heat_ks_client.create_stack_user(long_user_name, password='password') 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())

View File

@ -20,7 +20,6 @@ from heat.tests import fakes
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
from heat.db import api as db_api
from heat.engine import environment from heat.engine import environment
from heat.common import identifier from heat.common import identifier
from heat.common import template_format from heat.common import template_format
@ -203,8 +202,8 @@ class WaitCondMetadataUpdateTest(HeatTestCase):
def create_stack(self, stack_name='test_stack'): def create_stack(self, stack_name='test_stack'):
temp = template_format.parse(test_template_waitcondition) temp = template_format.parse(test_template_waitcondition)
template = parser.Template(temp) template = parser.Template(temp)
stack = parser.Stack(utils.dummy_context(), stack_name, template, ctx = utils.dummy_context()
disable_rollback=True) stack = parser.Stack(ctx, stack_name, template, disable_rollback=True)
self.stack_id = stack.store() self.stack_id = stack.store()
@ -223,7 +222,9 @@ class WaitCondMetadataUpdateTest(HeatTestCase):
wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id) wc.WaitConditionHandle.identifier().MultipleTimes().AndReturn(id)
self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep') 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 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(check_empty)
scheduler.TaskRunner._sleep(mox.IsA(int)).WithSideEffects(post_success) 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) scheduler.TaskRunner._sleep(mox.IsA(int)).AndReturn(None)
self.m.ReplayAll() self.m.ReplayAll()

View File

@ -21,7 +21,6 @@ from heat.tests import fakes
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
from heat.common import context
from heat.common import exception from heat.common import exception
from heat.common import template_format from heat.common import template_format
@ -72,7 +71,7 @@ class SignalTest(HeatTestCase):
def create_stack(self, stack_name='test_stack', stub=True): def create_stack(self, stack_name='test_stack', stub=True):
temp = template_format.parse(test_template_signal) temp = template_format.parse(test_template_signal)
template = parser.Template(temp) template = parser.Template(temp)
ctx = context.get_admin_context() ctx = utils.dummy_context()
ctx.tenant_id = 'test_tenant' ctx.tenant_id = 'test_tenant'
stack = parser.Stack(ctx, stack_name, template, stack = parser.Stack(ctx, stack_name, template,
disable_rollback=True) disable_rollback=True)
@ -85,6 +84,9 @@ class SignalTest(HeatTestCase):
self.m.StubOutWithMock(sr.SignalResponder, 'keystone') self.m.StubOutWithMock(sr.SignalResponder, 'keystone')
sr.SignalResponder.keystone().MultipleTimes().AndReturn( sr.SignalResponder.keystone().MultipleTimes().AndReturn(
self.fc) self.fc)
self.m.ReplayAll()
return stack return stack
@utils.stack_delete_after @utils.stack_delete_after

View File

@ -136,6 +136,7 @@ def dummy_context(user='test_username', tenant_id='test_tenant_id',
'username': user, 'username': user,
'password': password, 'password': password,
'roles': roles, 'roles': roles,
'trust_id': 'atrust123',
'auth_url': 'http://_testnoexisthost_:5000/v2.0', 'auth_url': 'http://_testnoexisthost_:5000/v2.0',
'auth_token': 'abcd1234' 'auth_token': 'abcd1234'
}) })

View File

@ -15,12 +15,13 @@ PasteDeploy>=1.5.0
Routes>=1.12.3 Routes>=1.12.3
SQLAlchemy>=0.7.8,<=0.7.99 SQLAlchemy>=0.7.8,<=0.7.99
WebOb>=1.2.3,<1.3 WebOb>=1.2.3,<1.3
python-keystoneclient>=0.3.0 python-keystoneclient>=0.3.2
python-swiftclient>=1.2 python-swiftclient>=1.2
python-neutronclient>=2.2.3,<3 python-neutronclient>=2.2.3,<3
python-ceilometerclient>=1.0.2 python-ceilometerclient>=1.0.2
python-cinderclient>=1.0.4 python-cinderclient>=1.0.4
PyYAML>=3.1.0 PyYAML>=3.1.0
oslo.config>=1.1.0
paramiko>=1.8.0 paramiko>=1.8.0
Babel>=0.9.6 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