Only validate credentials on create based on resources
This change relaxes the validation which checked for credentials on stack create and update. As implemented, having any of the following resources in the template will result in credentials being mandatory on create and update: * AWS::AutoScaling::ScalingPolicy * OS::Heat::HARestarter * AWS::CloudFormation::WaitConditionHandle For all other templates, credentials are not needed. When trusts are merged, this logic could also be used to decide whether a trust token needs to be created at all. Fixes bug: #1217617 Change-Id: I3e4b8698d3712053dc3c0851433ef0cbbadbdfed
This commit is contained in:
parent
8a0043f5d6
commit
af238fbd08
@ -271,6 +271,14 @@ class Stack(object):
|
|||||||
if result:
|
if result:
|
||||||
raise StackValidationFailed(message=result)
|
raise StackValidationFailed(message=result)
|
||||||
|
|
||||||
|
def requires_deferred_auth(self):
|
||||||
|
'''
|
||||||
|
Returns whether this stack may need to perform API requests
|
||||||
|
during its lifecycle using the configured deferred authentication
|
||||||
|
method.
|
||||||
|
'''
|
||||||
|
return any(res.requires_deferred_auth for res in self)
|
||||||
|
|
||||||
def state_set(self, action, status, reason):
|
def state_set(self, action, status, reason):
|
||||||
'''Update the stack state in the database.'''
|
'''Update the stack state in the database.'''
|
||||||
if action not in self.ACTIONS:
|
if action not in self.ACTIONS:
|
||||||
|
@ -116,6 +116,10 @@ class Resource(object):
|
|||||||
# that describes the appropriate resource attributes
|
# that describes the appropriate resource attributes
|
||||||
attributes_schema = {}
|
attributes_schema = {}
|
||||||
|
|
||||||
|
# If True, this resource may perform authenticated API requests
|
||||||
|
# throughout its lifecycle
|
||||||
|
requires_deferred_auth = False
|
||||||
|
|
||||||
def __new__(cls, name, json, stack):
|
def __new__(cls, name, json, stack):
|
||||||
'''Create a new Resource of the appropriate class for its type.'''
|
'''Create a new Resource of the appropriate class for its type.'''
|
||||||
|
|
||||||
|
@ -206,7 +206,13 @@ class EngineService(service.Service):
|
|||||||
stacks = db_api.stack_get_all_by_tenant(cnxt) or []
|
stacks = db_api.stack_get_all_by_tenant(cnxt) or []
|
||||||
return list(format_stack_details(stacks))
|
return list(format_stack_details(stacks))
|
||||||
|
|
||||||
def _validate_mandatory_credentials(self, cnxt):
|
def _validate_deferred_auth_context(self, cnxt, stack):
|
||||||
|
if cfg.CONF.deferred_auth_method != 'password':
|
||||||
|
return
|
||||||
|
|
||||||
|
if not stack.requires_deferred_auth():
|
||||||
|
return
|
||||||
|
|
||||||
if cnxt.username is None:
|
if cnxt.username is None:
|
||||||
raise exception.MissingCredentialError(required='X-Auth-User')
|
raise exception.MissingCredentialError(required='X-Auth-User')
|
||||||
if cnxt.password is None:
|
if cnxt.password is None:
|
||||||
@ -229,8 +235,6 @@ class EngineService(service.Service):
|
|||||||
"""
|
"""
|
||||||
logger.info('template is %s' % template)
|
logger.info('template is %s' % template)
|
||||||
|
|
||||||
self._validate_mandatory_credentials(cnxt)
|
|
||||||
|
|
||||||
def _stack_create(stack):
|
def _stack_create(stack):
|
||||||
# Create the stack, and create the periodic task if successful
|
# Create the stack, and create the periodic task if successful
|
||||||
stack.create()
|
stack.create()
|
||||||
@ -251,6 +255,8 @@ class EngineService(service.Service):
|
|||||||
stack = parser.Stack(cnxt, stack_name, tmpl,
|
stack = parser.Stack(cnxt, stack_name, tmpl,
|
||||||
env, **common_params)
|
env, **common_params)
|
||||||
|
|
||||||
|
self._validate_deferred_auth_context(cnxt, stack)
|
||||||
|
|
||||||
stack.validate()
|
stack.validate()
|
||||||
|
|
||||||
# Creates a trust and sets the trust_id and trustor_user_id in
|
# Creates a trust and sets the trust_id and trustor_user_id in
|
||||||
@ -280,8 +286,6 @@ class EngineService(service.Service):
|
|||||||
"""
|
"""
|
||||||
logger.info('template is %s' % template)
|
logger.info('template is %s' % template)
|
||||||
|
|
||||||
self._validate_mandatory_credentials(cnxt)
|
|
||||||
|
|
||||||
# Get the database representation of the existing stack
|
# Get the database representation of the existing stack
|
||||||
db_stack = self._get_stack(cnxt, stack_identity)
|
db_stack = self._get_stack(cnxt, stack_identity)
|
||||||
|
|
||||||
@ -296,6 +300,7 @@ class EngineService(service.Service):
|
|||||||
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
||||||
env, **common_params)
|
env, **common_params)
|
||||||
|
|
||||||
|
self._validate_deferred_auth_context(cnxt, updated_stack)
|
||||||
updated_stack.validate()
|
updated_stack.validate()
|
||||||
|
|
||||||
self._start_in_thread(db_stack.id, current_stack.update, updated_stack)
|
self._start_in_thread(db_stack.id, current_stack.update, updated_stack)
|
||||||
|
@ -39,6 +39,10 @@ SIGNAL_VERB = {WAITCONDITION: 'PUT',
|
|||||||
|
|
||||||
class SignalResponder(resource.Resource):
|
class SignalResponder(resource.Resource):
|
||||||
|
|
||||||
|
# Anything which subclasses this may trigger authenticated
|
||||||
|
# API operations as a consequence of handling a signal
|
||||||
|
requires_deferred_auth = True
|
||||||
|
|
||||||
def handle_create(self):
|
def handle_create(self):
|
||||||
# Create a keystone user so we can create a signed URL via FnGetRefId
|
# Create a keystone user so we can create a signed URL via FnGetRefId
|
||||||
user_id = self.keystone().create_stack_user(
|
user_id = self.keystone().create_stack_user(
|
||||||
|
@ -372,19 +372,46 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
|
|||||||
stack.t, {}, None, {})
|
stack.t, {}, None, {})
|
||||||
|
|
||||||
def test_stack_create_no_credentials(self):
|
def test_stack_create_no_credentials(self):
|
||||||
stack_name = 'service_create_test_stack'
|
stack_name = 'test_stack_create_no_credentials'
|
||||||
params = {'foo': 'bar'}
|
params = {'foo': 'bar'}
|
||||||
template = '{ "Template": "data" }'
|
template = '{ "Template": "data" }'
|
||||||
|
|
||||||
ctx = self.ctx = utils.dummy_context(password=None)
|
stack = get_wordpress_stack(stack_name, self.ctx)
|
||||||
self.assertRaises(exception.MissingCredentialError,
|
# force check for credentials on create
|
||||||
self.man.create_stack, ctx, stack_name, template,
|
stack.resources['WebServer'].requires_deferred_auth = True
|
||||||
params, None, {})
|
|
||||||
|
|
||||||
ctx = self.ctx = utils.dummy_context(user=None)
|
self.m.StubOutWithMock(parser, 'Template')
|
||||||
self.assertRaises(exception.MissingCredentialError,
|
self.m.StubOutWithMock(environment, 'Environment')
|
||||||
self.man.create_stack, ctx, stack_name, template,
|
self.m.StubOutWithMock(parser, 'Stack')
|
||||||
params, None, {})
|
|
||||||
|
ctx_no_pwd = utils.dummy_context(password=None)
|
||||||
|
ctx_no_user = utils.dummy_context(user=None)
|
||||||
|
|
||||||
|
parser.Template(template, files=None).AndReturn(stack.t)
|
||||||
|
environment.Environment(params).AndReturn(stack.env)
|
||||||
|
parser.Stack(ctx_no_pwd, stack.name,
|
||||||
|
stack.t, stack.env).AndReturn(stack)
|
||||||
|
|
||||||
|
parser.Template(template, files=None).AndReturn(stack.t)
|
||||||
|
environment.Environment(params).AndReturn(stack.env)
|
||||||
|
parser.Stack(ctx_no_user, stack.name,
|
||||||
|
stack.t, stack.env).AndReturn(stack)
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
ex = self.assertRaises(exception.MissingCredentialError,
|
||||||
|
self.man.create_stack,
|
||||||
|
ctx_no_pwd, stack_name,
|
||||||
|
template, params, None, {})
|
||||||
|
self.assertEqual(
|
||||||
|
'Missing required credential: X-Auth-Key', ex.message)
|
||||||
|
|
||||||
|
ex = self.assertRaises(exception.MissingCredentialError,
|
||||||
|
self.man.create_stack,
|
||||||
|
ctx_no_user, stack_name,
|
||||||
|
template, params, None, {})
|
||||||
|
self.assertEqual(
|
||||||
|
'Missing required credential: X-Auth-User', ex.message)
|
||||||
|
|
||||||
def test_stack_validate(self):
|
def test_stack_validate(self):
|
||||||
stack_name = 'service_create_test_validate'
|
stack_name = 'service_create_test_validate'
|
||||||
@ -543,23 +570,82 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
|
|||||||
self.m.VerifyAll()
|
self.m.VerifyAll()
|
||||||
|
|
||||||
def test_stack_update_no_credentials(self):
|
def test_stack_update_no_credentials(self):
|
||||||
stack_name = 'service_update_nonexist_test_stack'
|
stack_name = 'test_stack_update_no_credentials'
|
||||||
params = {'foo': 'bar'}
|
params = {'foo': 'bar'}
|
||||||
template = '{ "Template": "data" }'
|
template = '{ "Template": "data" }'
|
||||||
|
|
||||||
stack = get_wordpress_stack(stack_name, self.ctx)
|
old_stack = get_wordpress_stack(stack_name, self.ctx)
|
||||||
|
# force check for credentials on create
|
||||||
|
old_stack.resources['WebServer'].requires_deferred_auth = True
|
||||||
|
|
||||||
ctx = self.ctx = utils.dummy_context(password=None)
|
sid = old_stack.store()
|
||||||
self.assertRaises(exception.MissingCredentialError,
|
s = db_api.stack_get(self.ctx, sid)
|
||||||
self.man.update_stack,
|
|
||||||
ctx, stack.identifier(), template, params,
|
|
||||||
None, {})
|
|
||||||
|
|
||||||
ctx = self.ctx = utils.dummy_context(user=None)
|
self.ctx = utils.dummy_context(password=None)
|
||||||
self.assertRaises(exception.MissingCredentialError,
|
|
||||||
self.man.update_stack,
|
self.m.StubOutWithMock(parser, 'Stack')
|
||||||
ctx, stack.identifier(), template, params,
|
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||||
None, {})
|
self.m.StubOutWithMock(parser, 'Template')
|
||||||
|
self.m.StubOutWithMock(environment, 'Environment')
|
||||||
|
|
||||||
|
parser.Stack.load(self.ctx, stack=s).AndReturn(old_stack)
|
||||||
|
|
||||||
|
parser.Template(template, files=None).AndReturn(old_stack.t)
|
||||||
|
environment.Environment(params).AndReturn(old_stack.env)
|
||||||
|
parser.Stack(self.ctx, old_stack.name,
|
||||||
|
old_stack.t, old_stack.env).AndReturn(old_stack)
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
ex = self.assertRaises(exception.MissingCredentialError,
|
||||||
|
self.man.update_stack, self.ctx,
|
||||||
|
old_stack.identifier(),
|
||||||
|
template, params, None, {})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
'Missing required credential: X-Auth-Key', ex.message)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_validate_deferred_auth_context_trusts(self):
|
||||||
|
stack = get_wordpress_stack('test_deferred_auth', self.ctx)
|
||||||
|
stack.resources['WebServer'].requires_deferred_auth = True
|
||||||
|
ctx = utils.dummy_context(user=None, password=None)
|
||||||
|
cfg.CONF.set_default('deferred_auth_method', 'trusts')
|
||||||
|
|
||||||
|
# using trusts, no username or password required
|
||||||
|
self.man._validate_deferred_auth_context(ctx, stack)
|
||||||
|
|
||||||
|
def test_validate_deferred_auth_context_not_required(self):
|
||||||
|
stack = get_wordpress_stack('test_deferred_auth', self.ctx)
|
||||||
|
stack.resources['WebServer'].requires_deferred_auth = False
|
||||||
|
ctx = utils.dummy_context(user=None, password=None)
|
||||||
|
cfg.CONF.set_default('deferred_auth_method', 'password')
|
||||||
|
|
||||||
|
# stack performs no deferred operations, so no username or
|
||||||
|
# password required
|
||||||
|
self.man._validate_deferred_auth_context(ctx, stack)
|
||||||
|
|
||||||
|
def test_validate_deferred_auth_context_missing_credentials(self):
|
||||||
|
stack = get_wordpress_stack('test_deferred_auth', self.ctx)
|
||||||
|
stack.resources['WebServer'].requires_deferred_auth = True
|
||||||
|
cfg.CONF.set_default('deferred_auth_method', 'password')
|
||||||
|
|
||||||
|
# missing username
|
||||||
|
ctx = utils.dummy_context(user=None)
|
||||||
|
ex = self.assertRaises(exception.MissingCredentialError,
|
||||||
|
self.man._validate_deferred_auth_context,
|
||||||
|
ctx, stack)
|
||||||
|
self.assertEqual(
|
||||||
|
'Missing required credential: X-Auth-User', ex.message)
|
||||||
|
|
||||||
|
# missing password
|
||||||
|
ctx = utils.dummy_context(password=None)
|
||||||
|
ex = self.assertRaises(exception.MissingCredentialError,
|
||||||
|
self.man._validate_deferred_auth_context,
|
||||||
|
ctx, stack)
|
||||||
|
self.assertEqual(
|
||||||
|
'Missing required credential: X-Auth-Key', ex.message)
|
||||||
|
|
||||||
|
|
||||||
class StackServiceSuspendResumeTest(HeatTestCase):
|
class StackServiceSuspendResumeTest(HeatTestCase):
|
||||||
|
@ -1717,3 +1717,18 @@ class StackTest(HeatTestCase):
|
|||||||
|
|
||||||
saved_stack = parser.Stack.load(self.ctx, stack_id=stack_ownee.id)
|
saved_stack = parser.Stack.load(self.ctx, stack_id=stack_ownee.id)
|
||||||
self.assertEqual(saved_stack.owner_id, self.stack.id)
|
self.assertEqual(saved_stack.owner_id, self.stack.id)
|
||||||
|
|
||||||
|
@utils.stack_delete_after
|
||||||
|
def test_requires_deferred_auth(self):
|
||||||
|
tmpl = {'Resources': {'AResource': {'Type': 'GenericResourceType'},
|
||||||
|
'BResource': {'Type': 'GenericResourceType'},
|
||||||
|
'CResource': {'Type': 'GenericResourceType'}}}
|
||||||
|
|
||||||
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
||||||
|
template.Template(tmpl),
|
||||||
|
disable_rollback=False)
|
||||||
|
|
||||||
|
self.assertFalse(self.stack.requires_deferred_auth())
|
||||||
|
|
||||||
|
self.stack['CResource'].requires_deferred_auth = True
|
||||||
|
self.assertTrue(self.stack.requires_deferred_auth())
|
||||||
|
@ -164,6 +164,7 @@ class SignalTest(HeatTestCase):
|
|||||||
|
|
||||||
rsrc = self.stack.resources['signal_handler']
|
rsrc = self.stack.resources['signal_handler']
|
||||||
self.assertEqual(rsrc.state, (rsrc.CREATE, rsrc.COMPLETE))
|
self.assertEqual(rsrc.state, (rsrc.CREATE, rsrc.COMPLETE))
|
||||||
|
self.assertTrue(rsrc.requires_deferred_auth)
|
||||||
|
|
||||||
rsrc.signal(details=test_d)
|
rsrc.signal(details=test_d)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user