Encrypt properties data
Encrypt properties data before storing it in database and decrypt it when the resource is being loaded from the database. Change-Id: I646542b1d03296f62a83041dc2a0ca2719775289 Implements: blueprint encrypt-hidden-parameters
This commit is contained in:
parent
e4264917fa
commit
89d4206954
@ -0,0 +1,25 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = sqlalchemy.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
resource = sqlalchemy.Table('resource', meta, autoload=True)
|
||||
properties_data_encrypted = sqlalchemy.Column('properties_data_encrypted',
|
||||
sqlalchemy.Boolean,
|
||||
default=False)
|
||||
properties_data_encrypted.create(resource)
|
@ -298,6 +298,8 @@ class Resource(BASE, HeatBase, StateAware):
|
||||
# created/modified. (bug #1193269)
|
||||
updated_at = sqlalchemy.Column(sqlalchemy.DateTime)
|
||||
properties_data = sqlalchemy.Column('properties_data', types.Json)
|
||||
properties_data_encrypted = sqlalchemy.Column('properties_data_encrypted',
|
||||
sqlalchemy.Boolean)
|
||||
engine_id = sqlalchemy.Column(sqlalchemy.String(36))
|
||||
atomic_key = sqlalchemy.Column(sqlalchemy.Integer)
|
||||
|
||||
|
@ -976,6 +976,10 @@ class Resource(object):
|
||||
|
||||
def _store(self, metadata=None):
|
||||
'''Create the resource in the database.'''
|
||||
|
||||
properties_data_encrypted, properties_data = \
|
||||
resource_objects.Resource.encrypt_properties_data(
|
||||
self._stored_properties_data)
|
||||
try:
|
||||
rs = {'action': self.action,
|
||||
'status': self.status,
|
||||
@ -984,7 +988,8 @@ class Resource(object):
|
||||
'nova_instance': self.resource_id,
|
||||
'name': self.name,
|
||||
'rsrc_metadata': metadata,
|
||||
'properties_data': self._stored_properties_data,
|
||||
'properties_data': properties_data,
|
||||
'properties_data_encrypted': properties_data_encrypted,
|
||||
'needed_by': self.needed_by,
|
||||
'requires': self.requires,
|
||||
'replaces': self.replaces,
|
||||
@ -1014,13 +1019,17 @@ class Resource(object):
|
||||
self.status = status
|
||||
self.status_reason = reason
|
||||
|
||||
properties_data_encrypted, properties_data = \
|
||||
resource_objects.Resource.encrypt_properties_data(
|
||||
self._stored_properties_data)
|
||||
data = {
|
||||
'action': self.action,
|
||||
'status': self.status,
|
||||
'status_reason': reason,
|
||||
'stack_id': self.stack.id,
|
||||
'updated_at': self.updated_time,
|
||||
'properties_data': self._stored_properties_data,
|
||||
'properties_data': properties_data,
|
||||
'properties_data_encrypted': properties_data_encrypted,
|
||||
'needed_by': self.needed_by,
|
||||
'requires': self.requires,
|
||||
'replaces': self.replaces,
|
||||
|
@ -18,15 +18,21 @@ Resource object
|
||||
"""
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_versionedobjects import base
|
||||
from oslo_versionedobjects import fields
|
||||
import six
|
||||
|
||||
from heat.common import crypt
|
||||
from heat.db import api as db_api
|
||||
from heat.objects import fields as heat_fields
|
||||
from heat.objects import resource_data
|
||||
from heat.objects import stack
|
||||
|
||||
cfg.CONF.import_opt('encrypt_parameters_and_properties', 'heat.common.config')
|
||||
|
||||
|
||||
class Resource(
|
||||
base.VersionedObject,
|
||||
@ -46,6 +52,7 @@ class Resource(
|
||||
'action': fields.StringField(nullable=True),
|
||||
'rsrc_metadata': heat_fields.JsonField(nullable=True),
|
||||
'properties_data': heat_fields.JsonField(nullable=True),
|
||||
'properties_data_encrypted': fields.BooleanField(default=False),
|
||||
'data': fields.ListOfObjectsField(
|
||||
resource_data.ResourceData,
|
||||
nullable=True
|
||||
@ -74,6 +81,17 @@ class Resource(
|
||||
)
|
||||
else:
|
||||
resource[field] = db_resource[field]
|
||||
|
||||
if resource.properties_data_encrypted and resource.properties_data:
|
||||
properties_data = {}
|
||||
for prop_name, prop_value in resource.properties_data.items():
|
||||
decrypt_function_name = prop_value[0]
|
||||
decrypt_function = getattr(crypt, decrypt_function_name, None)
|
||||
decrypted_value = decrypt_function(prop_value[1])
|
||||
prop_string = jsonutils.loads(decrypted_value)
|
||||
properties_data[prop_name] = prop_string
|
||||
resource.properties_data = properties_data
|
||||
|
||||
resource._context = context
|
||||
resource.obj_reset_changes()
|
||||
return resource
|
||||
@ -157,3 +175,15 @@ class Resource(
|
||||
resource_db = db_api.resource_get(self._context, self.id)
|
||||
resource_db.refresh(attrs=attrs)
|
||||
return self._refresh()
|
||||
|
||||
@staticmethod
|
||||
def encrypt_properties_data(data):
|
||||
if cfg.CONF.encrypt_parameters_and_properties and data:
|
||||
result = {}
|
||||
for prop_name, prop_value in data.items():
|
||||
prop_string = jsonutils.dumps(prop_value)
|
||||
encoded_value = encodeutils.safe_encode(prop_string)
|
||||
encrypted_value = crypt.encrypt(encoded_value)
|
||||
result[prop_name] = encrypted_value
|
||||
return (True, result)
|
||||
return (False, data)
|
||||
|
@ -609,6 +609,10 @@ class HeatMigrationsCheckers(test_migrations.WalkVersionsMixin,
|
||||
def _check_062(self, engine, data):
|
||||
self.assertColumnExists(engine, 'stack', 'parent_resource_name')
|
||||
|
||||
def _check_063(self, engine, data):
|
||||
self.assertColumnExists(engine, 'resource',
|
||||
'properties_data_encrypted')
|
||||
|
||||
|
||||
class TestHeatMigrationsMySQL(HeatMigrationsCheckers,
|
||||
test_base.MySQLOpportunisticTestCase):
|
||||
|
@ -25,6 +25,7 @@ from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common import short_id
|
||||
from heat.common import timeutils
|
||||
from heat.db import api as db_api
|
||||
from heat.engine import attributes
|
||||
from heat.engine.cfn import functions as cfn_funcs
|
||||
from heat.engine import constraints
|
||||
@ -1302,6 +1303,81 @@ class ResourceTest(common.HeatTestCase):
|
||||
res.FnGetAtt('attr2')
|
||||
self.assertIn("Attribute attr2 is not of type Map", self.LOG.output)
|
||||
|
||||
def test_properties_data_stored_encrypted_decrypted_on_load(self):
|
||||
cfg.CONF.set_override('encrypt_parameters_and_properties', True)
|
||||
|
||||
tmpl = rsrc_defn.ResourceDefinition('test_resource', 'Foo')
|
||||
stored_properties_data = {'prop1': 'string',
|
||||
'prop2': {'a': 'dict'},
|
||||
'prop3': 1,
|
||||
'prop4': ['a', 'list'],
|
||||
'prop5': True}
|
||||
|
||||
# The db data should be encrypted when _store_or_update() is called
|
||||
res = generic_rsrc.GenericResource('test_res_enc', tmpl, self.stack)
|
||||
res._stored_properties_data = stored_properties_data
|
||||
res._store_or_update(res.CREATE, res.IN_PROGRESS, 'test_store')
|
||||
db_res = db_api.resource_get(res.context, res.id)
|
||||
self.assertNotEqual('string',
|
||||
db_res.properties_data['prop1'])
|
||||
|
||||
# The db data should be encrypted when _store() is called
|
||||
res = generic_rsrc.GenericResource('test_res_enc', tmpl, self.stack)
|
||||
res._stored_properties_data = stored_properties_data
|
||||
res._store()
|
||||
db_res = db_api.resource_get(res.context, res.id)
|
||||
self.assertNotEqual('string',
|
||||
db_res.properties_data['prop1'])
|
||||
|
||||
# The properties data should be decrypted when the object is
|
||||
# loaded using get_obj
|
||||
res_obj = resource_objects.Resource.get_obj(res.context, res.id)
|
||||
self.assertEqual('string', res_obj.properties_data['prop1'])
|
||||
|
||||
# The properties data should be decrypted when the object is
|
||||
# loaded using get_all_by_stack
|
||||
res_objs = resource_objects.Resource.get_all_by_stack(res.context,
|
||||
self.stack.id)
|
||||
res_obj = res_objs['test_res_enc']
|
||||
self.assertEqual('string', res_obj.properties_data['prop1'])
|
||||
|
||||
def test_properties_data_no_encryption(self):
|
||||
cfg.CONF.set_override('encrypt_parameters_and_properties', False)
|
||||
|
||||
tmpl = rsrc_defn.ResourceDefinition('test_resource', 'Foo')
|
||||
stored_properties_data = {'prop1': 'string',
|
||||
'prop2': {'a': 'dict'},
|
||||
'prop3': 1,
|
||||
'prop4': ['a', 'list'],
|
||||
'prop5': True}
|
||||
|
||||
# The db data should not be encrypted when _store_or_update()
|
||||
# is called
|
||||
res = generic_rsrc.GenericResource('test_res_enc', tmpl, self.stack)
|
||||
res._stored_properties_data = stored_properties_data
|
||||
res._store_or_update(res.CREATE, res.IN_PROGRESS, 'test_store')
|
||||
db_res = db_api.resource_get(res.context, res.id)
|
||||
self.assertEqual('string', db_res.properties_data['prop1'])
|
||||
|
||||
# The db data should not be encrypted when _store() is called
|
||||
res = generic_rsrc.GenericResource('test_res_enc', tmpl, self.stack)
|
||||
res._stored_properties_data = stored_properties_data
|
||||
res._store()
|
||||
db_res = db_api.resource_get(res.context, res.id)
|
||||
self.assertEqual('string', db_res.properties_data['prop1'])
|
||||
|
||||
# The properties data should not be modified when the object
|
||||
# is loaded using get_obj
|
||||
res_obj = resource_objects.Resource.get_obj(res.context, res.id)
|
||||
self.assertEqual('string', res_obj.properties_data['prop1'])
|
||||
|
||||
# The properties data should not be modified when the object
|
||||
# is loaded using get_all_by_stack
|
||||
res_objs = resource_objects.Resource.get_all_by_stack(res.context,
|
||||
self.stack.id)
|
||||
res_obj = res_objs['test_res_enc']
|
||||
self.assertEqual('string', res_obj.properties_data['prop1'])
|
||||
|
||||
|
||||
class ResourceAdoptTest(common.HeatTestCase):
|
||||
def setUp(self):
|
||||
|
Loading…
Reference in New Issue
Block a user