From 654032459ce9ae21f2964bcb1387a9dc6077c690 Mon Sep 17 00:00:00 2001 From: Vijendar Komalla Date: Thu, 18 Sep 2014 16:59:20 -0500 Subject: [PATCH] Support for encrypt/decrypt parameters in heat-manage Adding support to encrypt/decrypt parameters through heat-manage command. Change-Id: I2cd7b8837156ebe892d591f48ad2b1c6ecca5f9d Implements: blueprint encrypt-hidden-parameters Co-Authored-By: Jason Dunsmore --- heat/cmd/manage.py | 24 ++++++++++++ heat/common/crypt.py | 26 ++++++++++--- heat/db/sqlalchemy/api.py | 54 +++++++++++++++++++++++++++ heat/db/utils.py | 8 ++++ heat/tests/db/test_sqlalchemy_api.py | 55 ++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 6 deletions(-) diff --git a/heat/cmd/manage.py b/heat/cmd/manage.py index fb625b5b02..c86523df8f 100644 --- a/heat/cmd/manage.py +++ b/heat/cmd/manage.py @@ -87,6 +87,18 @@ def purge_deleted(): utils.purge_deleted(CONF.command.age, CONF.command.granularity) +def do_crypt_parameters_and_properties(): + """ + Encrypt or decrypt template hidden parameters and resource properties data. + """ + ctxt = context.get_admin_context() + prev_encryption_key = CONF.command.previous_encryption_key + if CONF.command.crypt_operation == "encrypt": + utils.encrypt_parameters_and_properties(ctxt, prev_encryption_key) + elif CONF.command.crypt_operation == "decrypt": + utils.decrypt_parameters_and_properties(ctxt, prev_encryption_key) + + def add_command_parsers(subparsers): parser = subparsers.add_parser('db_version') parser.set_defaults(func=do_db_version) @@ -105,6 +117,18 @@ def add_command_parsers(subparsers): choices=['days', 'hours', 'minutes', 'seconds'], help=_('Granularity to use for age argument, defaults to days.')) + parser = subparsers.add_parser('update_params') + parser.set_defaults(func=do_crypt_parameters_and_properties) + parser.add_argument('crypt_operation', + nargs='?', + choices=['encrypt', 'decrypt'], + help=_('Valid values are encrypt or decrypt.')) + parser.add_argument('previous_encryption_key', + nargs='?', + default=None, + help=_('Provide old encryption key. New encryption' + ' key would be used from config file.')) + ServiceManageCommand.add_service_parsers(subparsers) command_opt = cfg.SubCommandOpt('command', diff --git a/heat/common/crypt.py b/heat/common/crypt.py index 09e76bcd7a..4fe904a6b2 100644 --- a/heat/common/crypt.py +++ b/heat/common/crypt.py @@ -32,24 +32,36 @@ auth_opts = [ cfg.CONF.register_opts(auth_opts) -def encrypt(auth_info): +def encrypt(auth_info, encryption_key=None): if auth_info is None: return None, None + + encryption_key = get_valid_encryption_key(encryption_key) sym = utils.SymmetricCrypto() - res = sym.encrypt(cfg.CONF.auth_encryption_key[:32], + res = sym.encrypt(encryption_key, auth_info, b64encode=True) return 'oslo_decrypt_v1', res -def oslo_decrypt_v1(auth_info): +def oslo_decrypt_v1(auth_info, encryption_key=None): if auth_info is None: return None + + encryption_key = get_valid_encryption_key(encryption_key) sym = utils.SymmetricCrypto() - return sym.decrypt(cfg.CONF.auth_encryption_key[:32], + return sym.decrypt(encryption_key, auth_info, b64decode=True) -def heat_decrypt(auth_info): +def get_valid_encryption_key(encryption_key): + if encryption_key is None: + encryption_key = cfg.CONF.auth_encryption_key[:32] + else: + encryption_key = encryption_key[0:32] + return encryption_key + + +def heat_decrypt(auth_info, encryption_key=None): """Decrypt function for data that has been encrypted using an older version of Heat. Note: the encrypt function returns the function that is needed to @@ -60,9 +72,11 @@ def heat_decrypt(auth_info): """ if auth_info is None: return None + + encryption_key = get_valid_encryption_key(encryption_key) auth = base64.b64decode(auth_info) iv = auth[:AES.block_size] - cipher = AES.new(cfg.CONF.auth_encryption_key[:32], AES.MODE_CFB, iv) + cipher = AES.new(encryption_key, AES.MODE_CFB, iv) res = cipher.decrypt(auth[AES.block_size:]) return res diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index 0ac4fcd509..6f82adefb5 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -18,6 +18,7 @@ import sys from oslo_config import cfg from oslo_db.sqlalchemy import session as db_session from oslo_db.sqlalchemy import utils +from oslo_utils import encodeutils from oslo_utils import timeutils import osprofiler.sqlalchemy import six @@ -1088,3 +1089,56 @@ def db_sync(engine, version=None): def db_version(engine): """Display the current database version.""" return migration.db_version(engine) + + +def db_encrypt_parameters_and_properties(ctxt, encryption_key): + from heat.engine import template + session = get_session() + session.begin() + + raw_templates = session.query(models.RawTemplate).all() + + for raw_template in raw_templates: + tmpl = template.Template.load(ctxt, raw_template.id, raw_template) + + encrypted_params = [] + for param_name, param in tmpl.param_schemata().items(): + if (param_name in encrypted_params) or (not param.hidden): + continue + + try: + param_val = raw_template.environment['parameters'][ + param_name] + except KeyError: + param_val = param.default + + encoded_val = encodeutils.safe_encode(param_val) + encrypted_val = crypt.encrypt(encoded_val, encryption_key) + raw_template.environment['parameters'][param_name] = \ + encrypted_val + encrypted_params.append(param_name) + + raw_template.environment['encrypted_param_names'] = \ + encrypted_params + + session.commit() + + +def db_decrypt_parameters_and_properties(ctxt, encryption_key): + session = get_session() + session.begin() + raw_templates = session.query(models.RawTemplate).all() + + for raw_template in raw_templates: + parameters = raw_template.environment['parameters'] + encrypted_params = raw_template.environment[ + 'encrypted_param_names'] + for param_name in encrypted_params: + decrypt_function_name = parameters[param_name][0] + decrypt_function = getattr(crypt, decrypt_function_name) + decrypted_val = decrypt_function(parameters[param_name][1], + encryption_key) + parameters[param_name] = encodeutils.safe_decode(decrypted_val) + raw_template.environment['encrypted_param_names'] = [] + + session.commit() diff --git a/heat/db/utils.py b/heat/db/utils.py index 41b66f173b..79373bbc19 100644 --- a/heat/db/utils.py +++ b/heat/db/utils.py @@ -45,3 +45,11 @@ IMPL = LazyPluggable('backend', def purge_deleted(age, granularity='days'): IMPL.purge_deleted(age, granularity) + + +def encrypt_parameters_and_properties(ctxt, encryption_key): + IMPL.db_encrypt_parameters_and_properties(ctxt, encryption_key) + + +def decrypt_parameters_and_properties(ctxt, encryption_key): + IMPL.db_decrypt_parameters_and_properties(ctxt, encryption_key) diff --git a/heat/tests/db/test_sqlalchemy_api.py b/heat/tests/db/test_sqlalchemy_api.py index e6505884ee..e9b57b4007 100644 --- a/heat/tests/db/test_sqlalchemy_api.py +++ b/heat/tests/db/test_sqlalchemy_api.py @@ -25,6 +25,7 @@ from heat.common import context from heat.common import exception from heat.common import template_format from heat.db.sqlalchemy import api as db_api +from heat.db.sqlalchemy import models from heat.engine.clients.os import glance from heat.engine.clients.os import nova from heat.engine import environment @@ -2602,3 +2603,57 @@ class DBAPISyncPointTest(common.HeatTestCase): self.ctx, self.stack.id, self.stack.current_traversal, True ) self.assertEqual(None, ret_sync_point_stack) + + +class DBAPICryptParamsPropsTest(common.HeatTestCase): + def setUp(self): + super(DBAPICryptParamsPropsTest, self).setUp() + self.ctx = utils.dummy_context() + t = 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 + ''') + template = { + 'template': t, + 'files': {'foo': 'bar'}, + 'environment': {'parameters': {'param1': 'foo', + 'param2': 'bar'}}} + self.template = db_api.raw_template_create(self.ctx, template) + + def test_db_encrypt_decrypt(self): + session = db_api.get_session() + + env = session.query(models.RawTemplate).all()[0].environment + self.assertEqual('bar', env['parameters']['param2']) + + db_api.db_encrypt_parameters_and_properties( + self.ctx, cfg.CONF.auth_encryption_key) + + env = session.query(models.RawTemplate).all()[0].environment + self.assertEqual('oslo_decrypt_v1', + env['parameters']['param2'][0]) + + db_api.db_decrypt_parameters_and_properties( + self.ctx, cfg.CONF.auth_encryption_key) + + env = session.query(models.RawTemplate).all()[0].environment + self.assertEqual('bar', env['parameters']['param2']) + + # Use a different encryption key to decrypt + db_api.db_encrypt_parameters_and_properties( + self.ctx, cfg.CONF.auth_encryption_key) + db_api.db_decrypt_parameters_and_properties( + self.ctx, '774c15be099ea74123a9b9592ff12680') + + env = session.query(models.RawTemplate).all()[0].environment + self.assertNotEqual('bar', env['parameters']['param2'])