Encrypt Heat template hidden parameters
1. Add a configuration option to enable/disable template parameters encryption. 2. Encrypt hidden parameters before storing them in the database and decrypt on stack load. Change-Id: Ie46c6a149f414f655600616da8deee463e55671c Implements: blueprint encrypt-hidden-parameters Co-Authored-By: Jason Dunsmore <jasondunsmore@gmail.com>
This commit is contained in:
parent
4032e40cce
commit
1b451c2be1
@ -216,8 +216,12 @@ engine_opts = [
|
||||
' set to a list of tuples,'
|
||||
' (stackresourcename, stackname) with list[0] being'
|
||||
' (None, rootstackname), and heat_resource_name will'
|
||||
' be set to the resource\'s name.'))]
|
||||
|
||||
' be set to the resource\'s name.')),
|
||||
cfg.BoolOpt('encrypt_parameters_and_properties',
|
||||
default=False,
|
||||
help=_('Encrypt template parameters that were marked as'
|
||||
' hidden and also all the resource properties before'
|
||||
' storing them in database.'))]
|
||||
|
||||
rpc_opts = [
|
||||
cfg.StrOpt('host',
|
||||
|
@ -16,9 +16,11 @@ from heat.common import template_format
|
||||
|
||||
|
||||
SECTIONS = (
|
||||
PARAMETERS, RESOURCE_REGISTRY, PARAMETER_DEFAULTS
|
||||
PARAMETERS, RESOURCE_REGISTRY, PARAMETER_DEFAULTS,
|
||||
ENCRYPTED_PARAM_NAMES
|
||||
) = (
|
||||
'parameters', 'resource_registry', 'parameter_defaults'
|
||||
'parameters', 'resource_registry', 'parameter_defaults',
|
||||
'encrypted_param_names'
|
||||
)
|
||||
|
||||
|
||||
@ -50,4 +52,7 @@ def default_for_missing(env):
|
||||
"""Checks a parsed environment for missing sections."""
|
||||
for param in SECTIONS:
|
||||
if param not in env:
|
||||
env[param] = {}
|
||||
if param == ENCRYPTED_PARAM_NAMES:
|
||||
env[param] = []
|
||||
else:
|
||||
env[param] = {}
|
||||
|
@ -472,6 +472,8 @@ class Environment(object):
|
||||
else:
|
||||
self.param_defaults = {}
|
||||
|
||||
self.encrypted_param_names = env.get(env_fmt.ENCRYPTED_PARAM_NAMES, [])
|
||||
|
||||
if env_fmt.PARAMETERS in env:
|
||||
self.params = env[env_fmt.PARAMETERS]
|
||||
else:
|
||||
@ -504,7 +506,8 @@ class Environment(object):
|
||||
"""Get the environment as a dict, ready for storing in the db."""
|
||||
return {env_fmt.RESOURCE_REGISTRY: self.registry.as_dict(),
|
||||
env_fmt.PARAMETERS: self.params,
|
||||
env_fmt.PARAMETER_DEFAULTS: self.param_defaults}
|
||||
env_fmt.PARAMETER_DEFAULTS: self.param_defaults,
|
||||
env_fmt.ENCRYPTED_PARAM_NAMES: self.encrypted_param_names}
|
||||
|
||||
def register_class(self, resource_type, resource_class):
|
||||
self.registry.register_class(resource_type, resource_class)
|
||||
|
@ -430,7 +430,6 @@ class Stack(collections.Mapping):
|
||||
status, action and status_reason.
|
||||
- We sometimes only want the DB attributes.
|
||||
"""
|
||||
|
||||
stack = {
|
||||
'owner_id': self.owner_id,
|
||||
'username': self.username,
|
||||
@ -472,6 +471,7 @@ class Stack(collections.Mapping):
|
||||
s['backup'] = backup
|
||||
s['updated_at'] = self.updated_time
|
||||
if self.t.id is None:
|
||||
stack_object.Stack.encrypt_hidden_parameters(self.t)
|
||||
s['raw_template_id'] = self.t.store(self.context)
|
||||
else:
|
||||
s['raw_template_id'] = self.t.id
|
||||
|
@ -17,9 +17,13 @@
|
||||
RawTemplate object
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_versionedobjects import base
|
||||
from oslo_versionedobjects import fields
|
||||
|
||||
from heat.common import crypt
|
||||
from heat.common import environment_format as env_fmt
|
||||
from heat.db import api as db_api
|
||||
from heat.objects import fields as heat_fields
|
||||
|
||||
@ -41,6 +45,17 @@ class RawTemplate(
|
||||
def _from_db_object(context, tpl, db_tpl):
|
||||
for field in tpl.fields:
|
||||
tpl[field] = db_tpl[field]
|
||||
|
||||
# If any of the parameters were encrypted, then decrypt them
|
||||
parameters = tpl.environment[env_fmt.PARAMETERS]
|
||||
encrypted_param_names = tpl.environment[env_fmt.ENCRYPTED_PARAM_NAMES]
|
||||
for param_name in encrypted_param_names:
|
||||
decrypt_function_name = parameters[param_name][0]
|
||||
decrypt_function = getattr(crypt, decrypt_function_name)
|
||||
decrypted_val = decrypt_function(parameters[param_name][1])
|
||||
parameters[param_name] = encodeutils.safe_decode(decrypted_val)
|
||||
tpl.environment[env_fmt.PARAMETERS] = parameters
|
||||
|
||||
tpl._context = context
|
||||
tpl.obj_reset_changes()
|
||||
return tpl
|
||||
@ -51,6 +66,17 @@ class RawTemplate(
|
||||
raw_template = cls._from_db_object(context, cls(), raw_template_db)
|
||||
return raw_template
|
||||
|
||||
@classmethod
|
||||
def encrypt_hidden_parameters(cls, tmpl):
|
||||
if cfg.CONF.encrypt_parameters_and_properties:
|
||||
for param_name, param in tmpl.env.params.items():
|
||||
if not tmpl.param_schemata()[param_name].hidden:
|
||||
continue
|
||||
clear_text_val = tmpl.env.params.get(param_name)
|
||||
encoded_val = encodeutils.safe_encode(clear_text_val)
|
||||
tmpl.env.params[param_name] = crypt.encrypt(encoded_val)
|
||||
tmpl.env.encrypted_param_names.append(param_name)
|
||||
|
||||
@classmethod
|
||||
def create(cls, context, values):
|
||||
return db_api.raw_template_create(context, values)
|
||||
|
@ -164,3 +164,7 @@ class Stack(
|
||||
self,
|
||||
db_stack
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def encrypt_hidden_parameters(cls, tmpl):
|
||||
raw_template.RawTemplate.encrypt_hidden_parameters(tmpl)
|
||||
|
@ -166,6 +166,7 @@ blarg: wibble
|
||||
def test_parameters(self):
|
||||
params = {'foo': 'bar', 'blarg': 'wibble'}
|
||||
body = {'parameters': params,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}}
|
||||
data = stacks.InstantiationData(body)
|
||||
@ -182,6 +183,7 @@ blarg: wibble
|
||||
'environment': {'parameters': {'blarg': 'wibble'}}}
|
||||
expect = {'parameters': {'blarg': 'wibble',
|
||||
'foo': 'bar'},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}}
|
||||
data = stacks.InstantiationData(body)
|
||||
@ -197,6 +199,7 @@ blarg: wibble
|
||||
expect = {'parameters': {'blarg': 'wibble',
|
||||
'foo': 'bar',
|
||||
'tester': 'Yes'},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}}
|
||||
data = stacks.InstantiationData(body)
|
||||
@ -212,8 +215,8 @@ blarg: wibble
|
||||
env = {'foo': 'bar', 'blarg': 'wibble'}
|
||||
body = {'not the environment': env}
|
||||
data = stacks.InstantiationData(body)
|
||||
self.assertEqual({'parameters': {}, 'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
self.assertEqual({'parameters': {}, 'encrypted_param_names': [],
|
||||
'parameter_defaults': {}, 'resource_registry': {}},
|
||||
data.environment())
|
||||
|
||||
def test_args(self):
|
||||
@ -744,6 +747,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': identity.stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -806,6 +810,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': identity.stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -894,6 +899,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': identity.stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {'my.yaml': 'This is the file contents.'},
|
||||
@ -938,6 +944,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -955,6 +962,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -972,6 +980,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1026,6 +1035,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1107,6 +1117,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_name': stack_name,
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1507,6 +1518,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1543,6 +1555,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {u'parameters': parameters,
|
||||
u'encrypted_param_names': [],
|
||||
u'parameter_defaults': {},
|
||||
u'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1627,6 +1640,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': {},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1663,6 +1677,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1725,6 +1740,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': {},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1765,6 +1781,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
@ -1903,6 +1920,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
('validate_template',
|
||||
{'template': template,
|
||||
'params': {'parameters': {},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}}})
|
||||
).AndReturn(engine_response)
|
||||
@ -1927,6 +1945,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
|
||||
('validate_template',
|
||||
{'template': template,
|
||||
'params': {'parameters': {},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {}}})
|
||||
).AndReturn({'Error': 'fubar'})
|
||||
|
@ -38,6 +38,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
def test_load_old_parameters(self):
|
||||
old = {u'a': u'ff', u'b': u'ss'}
|
||||
expected = {u'parameters': old,
|
||||
u'encrypted_param_names': [],
|
||||
u'parameter_defaults': {},
|
||||
u'resource_registry': {u'resources': {}}}
|
||||
env = environment.Environment(old)
|
||||
@ -45,6 +46,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
|
||||
def test_load_new_env(self):
|
||||
new_env = {u'parameters': {u'a': u'ff', u'b': u'ss'},
|
||||
u'encrypted_param_names': [],
|
||||
u'parameter_defaults': {u'ff': 'new_def'},
|
||||
u'resource_registry': {u'OS::Food': u'fruity.yaml',
|
||||
u'resources': {}}}
|
||||
@ -56,6 +58,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
prev_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
params = {}
|
||||
expected = {'parameters': prev_params,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
@ -70,6 +73,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
prev_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
params = {'tester': 'patched'}
|
||||
expected = {'parameters': {'foo': 'bar', 'tester': 'patched'},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
@ -85,6 +89,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
'another_tester': 'Yes'}
|
||||
params = {'tester': 'patched'}
|
||||
expected = {'parameters': {'foo': 'bar', 'tester': 'patched'},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
@ -99,6 +104,7 @@ class EnvironmentTest(common.HeatTestCase):
|
||||
prev_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
params = {}
|
||||
expected = {'parameters': {'foo': 'bar'},
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
@ -487,6 +493,7 @@ class ChildEnvTest(common.HeatTestCase):
|
||||
new_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
penv = environment.Environment()
|
||||
expected = {'parameters': new_params,
|
||||
'encrypted_param_names': [],
|
||||
'parameter_defaults': {},
|
||||
'resource_registry': {'resources': {}}}
|
||||
cenv = environment.get_child_environment(penv, new_params)
|
||||
@ -496,6 +503,7 @@ class ChildEnvTest(common.HeatTestCase):
|
||||
new_params = {'parameters': {'foo': 'bar', 'tester': 'Yes'}}
|
||||
penv = environment.Environment()
|
||||
expected = {'parameter_defaults': {},
|
||||
'encrypted_param_names': [],
|
||||
'resource_registry': {'resources': {}}}
|
||||
expected.update(new_params)
|
||||
cenv = environment.get_child_environment(penv, new_params)
|
||||
@ -506,6 +514,7 @@ class ChildEnvTest(common.HeatTestCase):
|
||||
parent_params = {'parameters': {'gone': 'hopefully'}}
|
||||
penv = environment.Environment(env=parent_params)
|
||||
expected = {'parameter_defaults': {},
|
||||
'encrypted_param_names': [],
|
||||
'resource_registry': {'resources': {}}}
|
||||
expected.update(new_params)
|
||||
cenv = environment.get_child_environment(penv, new_params)
|
||||
|
@ -24,6 +24,7 @@ class YamlEnvironmentTest(common.HeatTestCase):
|
||||
yaml1 = ''
|
||||
yaml2 = '''
|
||||
parameters: {}
|
||||
encrypted_param_names: []
|
||||
parameter_defaults: {}
|
||||
resource_registry: {}
|
||||
'''
|
||||
|
@ -24,6 +24,7 @@ import six
|
||||
from heat.common import context
|
||||
from heat.common import exception
|
||||
from heat.common import template_format
|
||||
from heat.db import api as db_api
|
||||
from heat.engine.clients.os import keystone
|
||||
from heat.engine.clients.os import nova
|
||||
from heat.engine import environment
|
||||
@ -1962,6 +1963,73 @@ class StackTest(common.HeatTestCase):
|
||||
# resource state.
|
||||
self.assertFalse(mock_drg.called)
|
||||
|
||||
def test_encrypt_parameters_false_parameters_stored_plaintext(self):
|
||||
'''
|
||||
Test stack loading with disabled parameter value validation.
|
||||
'''
|
||||
tmpl = template_format.parse('''
|
||||
heat_template_version: 2013-05-23
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
description: value1.
|
||||
param2:
|
||||
type: string
|
||||
description: value2.
|
||||
hidden: true
|
||||
resources:
|
||||
a_resource:
|
||||
type: GenericResourceType
|
||||
''')
|
||||
env1 = environment.Environment({'param1': 'foo', 'param2': 'bar'})
|
||||
self.stack = stack.Stack(self.ctx, 'test',
|
||||
template.Template(tmpl, env=env1))
|
||||
cfg.CONF.set_override('encrypt_parameters_and_properties', False)
|
||||
|
||||
# Verify that hidden parameters stored in plain text
|
||||
self.stack.store()
|
||||
db_stack = stack_object.Stack.get_by_id(self.ctx, self.stack.id)
|
||||
params = db_stack.raw_template.environment['parameters']
|
||||
self.assertEqual('foo', params['param1'])
|
||||
self.assertEqual('bar', params['param2'])
|
||||
|
||||
def test_parameters_stored_encrypted_decrypted_on_load(self):
|
||||
'''
|
||||
Test stack loading with disabled parameter value validation.
|
||||
'''
|
||||
tmpl = template_format.parse('''
|
||||
heat_template_version: 2013-05-23
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
description: value1.
|
||||
param2:
|
||||
type: string
|
||||
description: value2.
|
||||
hidden: true
|
||||
resources:
|
||||
a_resource:
|
||||
type: GenericResourceType
|
||||
''')
|
||||
env1 = environment.Environment({'param1': 'foo', 'param2': 'bar'})
|
||||
self.stack = stack.Stack(self.ctx, 'test',
|
||||
template.Template(tmpl, env=env1))
|
||||
cfg.CONF.set_override('encrypt_parameters_and_properties', True)
|
||||
|
||||
# Verify that hidden parameters are stored encrypted
|
||||
self.stack.store()
|
||||
db_tpl = db_api.raw_template_get(self.ctx, self.stack.t.id)
|
||||
db_params = db_tpl.environment['parameters']
|
||||
self.assertEqual('foo', db_params['param1'])
|
||||
self.assertEqual('oslo_decrypt_v1', db_params['param2'][0])
|
||||
self.assertIsNotNone(db_params['param2'][1])
|
||||
|
||||
# Verify that loaded stack has decrypted paramters
|
||||
loaded_stack = stack.Stack.load(self.ctx, stack_id=self.stack.id)
|
||||
params = loaded_stack.t.env.params
|
||||
self.assertEqual('foo', params.get('param1'))
|
||||
self.assertEqual('bar', params.get('param2'))
|
||||
|
||||
|
||||
class StackKwargsForCloningTest(common.HeatTestCase):
|
||||
scenarios = [
|
||||
|
@ -754,7 +754,8 @@ class WithTemplateTest(StackResourceBaseTest):
|
||||
def test_create_with_template(self):
|
||||
child_env = {'parameter_defaults': {},
|
||||
'parameters': self.params,
|
||||
'resource_registry': {'resources': {}}}
|
||||
'resource_registry': {'resources': {}},
|
||||
'encrypted_param_names': []}
|
||||
self.parent_resource.child_params = mock.Mock(
|
||||
return_value=self.params)
|
||||
res_name = self.parent_resource.physical_resource_name()
|
||||
@ -788,7 +789,8 @@ class WithTemplateTest(StackResourceBaseTest):
|
||||
|
||||
child_env = {'parameter_defaults': {},
|
||||
'parameters': self.params,
|
||||
'resource_registry': {'resources': {}}}
|
||||
'resource_registry': {'resources': {}},
|
||||
'encrypted_param_names': []}
|
||||
self.parent_resource.child_params = mock.Mock(
|
||||
return_value=self.params)
|
||||
rpcc = mock.Mock()
|
||||
|
Loading…
x
Reference in New Issue
Block a user