Merge "Delete secret key on image deletion"
This commit is contained in:
commit
e475581c72
@ -28,5 +28,17 @@
|
|||||||
"description": {
|
"description": {
|
||||||
"description": "A human-readable string describing this image.",
|
"description": "A human-readable string describing this image.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cinder_encryption_key_id": {
|
||||||
|
"description": "Identifier in the OpenStack Key Management Service for the encryption key for the Block Storage Service to use when mounting a volume created from this image",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cinder_encryption_key_deletion_policy": {
|
||||||
|
"description": "States the condition under which the Image Service will delete the object associated with the 'cinder_encryption_key_id' image property. If this property is missing, the Image Service will take no action",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"on_image_deletion",
|
||||||
|
"do_not_delete"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from castellan.common import exception as castellan_exception
|
||||||
|
from castellan import key_manager
|
||||||
import glance_store
|
import glance_store
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -60,6 +62,8 @@ class ImagesController(object):
|
|||||||
self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
|
self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
|
||||||
self.notifier, self.policy)
|
self.notifier, self.policy)
|
||||||
|
|
||||||
|
self._key_manager = key_manager.API(CONF)
|
||||||
|
|
||||||
@utils.mutating
|
@utils.mutating
|
||||||
def create(self, req, image, extra_properties, tags):
|
def create(self, req, image, extra_properties, tags):
|
||||||
image_factory = self.gateway.get_image_factory(req.context)
|
image_factory = self.gateway.get_image_factory(req.context)
|
||||||
@ -330,6 +334,32 @@ class ImagesController(object):
|
|||||||
msg = _("Property %s does not exist.")
|
msg = _("Property %s does not exist.")
|
||||||
raise webob.exc.HTTPConflict(msg % path_root)
|
raise webob.exc.HTTPConflict(msg % path_root)
|
||||||
|
|
||||||
|
def _delete_encryption_key(self, context, image):
|
||||||
|
props = image.extra_properties
|
||||||
|
|
||||||
|
cinder_encryption_key_id = props.get('cinder_encryption_key_id')
|
||||||
|
if cinder_encryption_key_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
deletion_policy = props.get('cinder_encryption_key_deletion_policy',
|
||||||
|
'')
|
||||||
|
if deletion_policy != 'on_image_deletion':
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._key_manager.delete(context, cinder_encryption_key_id)
|
||||||
|
except castellan_exception.Forbidden:
|
||||||
|
msg = ('Not allowed to delete encryption key %s' %
|
||||||
|
cinder_encryption_key_id)
|
||||||
|
LOG.warn(msg)
|
||||||
|
except (castellan_exception.ManagedObjectNotFoundError, KeyError):
|
||||||
|
msg = 'Could not find encryption key %s' % cinder_encryption_key_id
|
||||||
|
LOG.warn(msg)
|
||||||
|
except castellan_exception.KeyManagerError:
|
||||||
|
msg = ('Failed to delete cinder encryption key %s' %
|
||||||
|
cinder_encryption_key_id)
|
||||||
|
LOG.warn(msg)
|
||||||
|
|
||||||
@utils.mutating
|
@utils.mutating
|
||||||
def delete(self, req, image_id):
|
def delete(self, req, image_id):
|
||||||
image_repo = self.gateway.get_repo(req.context)
|
image_repo = self.gateway.get_repo(req.context)
|
||||||
@ -358,6 +388,7 @@ class ImagesController(object):
|
|||||||
"it cannot be found at %(fn)s"), {'fn': file_path})
|
"it cannot be found at %(fn)s"), {'fn': file_path})
|
||||||
|
|
||||||
image.delete()
|
image.delete()
|
||||||
|
self._delete_encryption_key(req.context, image)
|
||||||
image_repo.remove(image)
|
image_repo.remove(image)
|
||||||
except (glance_store.Forbidden, exception.Forbidden) as e:
|
except (glance_store.Forbidden, exception.Forbidden) as e:
|
||||||
LOG.debug("User not permitted to delete image '%s'", image_id)
|
LOG.debug("User not permitted to delete image '%s'", image_id)
|
||||||
|
0
glance/tests/unit/keymgr/__init__.py
Normal file
0
glance/tests/unit/keymgr/__init__.py
Normal file
25
glance/tests/unit/keymgr/fake.py
Normal file
25
glance/tests/unit/keymgr/fake.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Copyright 2011 Justin Santa Barbara
|
||||||
|
# Copyright 2012 OpenStack Foundation
|
||||||
|
# Copyright 2019 Red Hat
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Implementation of a fake key manager."""
|
||||||
|
|
||||||
|
|
||||||
|
from castellan.tests.unit.key_manager import mock_key_manager
|
||||||
|
|
||||||
|
|
||||||
|
def fake_api(configuration=None):
|
||||||
|
return mock_key_manager.MockKeyManager(configuration)
|
@ -19,6 +19,7 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from castellan.common import exception as castellan_exception
|
||||||
import glance_store as store
|
import glance_store as store
|
||||||
import mock
|
import mock
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
@ -36,6 +37,7 @@ from glance.common import store_utils
|
|||||||
from glance import domain
|
from glance import domain
|
||||||
import glance.schema
|
import glance.schema
|
||||||
from glance.tests.unit import base
|
from glance.tests.unit import base
|
||||||
|
from glance.tests.unit.keymgr import fake as fake_keymgr
|
||||||
import glance.tests.unit.utils as unit_test_utils
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
import glance.tests.utils as test_utils
|
import glance.tests.utils as test_utils
|
||||||
|
|
||||||
@ -155,6 +157,7 @@ class TestImagesController(base.IsolatedUnitTest):
|
|||||||
self.notifier,
|
self.notifier,
|
||||||
self.store))
|
self.store))
|
||||||
self.controller.gateway.store_utils = self.store_utils
|
self.controller.gateway.store_utils = self.store_utils
|
||||||
|
self.controller._key_manager = fake_keymgr.fake_api()
|
||||||
store.create_stores()
|
store.create_stores()
|
||||||
|
|
||||||
def _create_images(self):
|
def _create_images(self):
|
||||||
@ -2721,6 +2724,199 @@ class TestImagesController(base.IsolatedUnitTest):
|
|||||||
request, UUID4, {'method': {'name':
|
request, UUID4, {'method': {'name':
|
||||||
'glance-direct'}})
|
'glance-direct'}})
|
||||||
|
|
||||||
|
def test_delete_encryption_key_no_encryption_key(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties={})
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key_no_deletion_policy(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key_do_not_delete(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
'cinder_encryption_key_deletion_policy': 'do_not_delete',
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key_forbidden(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
with mock.patch.object(self.controller._key_manager, 'delete',
|
||||||
|
side_effect=castellan_exception.Forbidden):
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key_not_found(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
with mock.patch.object(self.controller._key_manager, 'delete',
|
||||||
|
side_effect=castellan_exception.ManagedObjectNotFoundError): # noqa
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key_error(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
with mock.patch.object(self.controller._key_manager, 'delete',
|
||||||
|
side_effect=castellan_exception.KeyManagerError): # noqa
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is still there
|
||||||
|
key = self.controller._key_manager.get(request.context,
|
||||||
|
fake_encryption_key)
|
||||||
|
self.assertEqual(fake_encryption_key, key._id)
|
||||||
|
|
||||||
|
def test_delete_encryption_key(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
fake_encryption_key = self.controller._key_manager.store(
|
||||||
|
request.context, mock.Mock())
|
||||||
|
props = {
|
||||||
|
'cinder_encryption_key_id': fake_encryption_key,
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
image = _domain_fixture(
|
||||||
|
UUID2, name='image-2', owner=TENANT2,
|
||||||
|
checksum='ca425b88f047ce8ec45ee90e813ada91',
|
||||||
|
os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
|
||||||
|
created_at=DATETIME, updated_at=DATETIME, size=1024,
|
||||||
|
virtual_size=3072, extra_properties=props)
|
||||||
|
self.controller._delete_encryption_key(request.context, image)
|
||||||
|
# Make sure the encrytion key is gone
|
||||||
|
self.assertRaises(KeyError,
|
||||||
|
self.controller._key_manager.get,
|
||||||
|
request.context, fake_encryption_key)
|
||||||
|
|
||||||
|
def test_delete_no_encryption_key_id(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
extra_props = {
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
created_image = self.controller.create(request,
|
||||||
|
image={'name': 'image-1'},
|
||||||
|
extra_properties=extra_props,
|
||||||
|
tags=[])
|
||||||
|
image_id = created_image.image_id
|
||||||
|
self.controller.delete(request, image_id)
|
||||||
|
# Ensure that image is deleted
|
||||||
|
image = self.db.image_get(request.context, image_id,
|
||||||
|
force_show_deleted=True)
|
||||||
|
self.assertTrue(image['deleted'])
|
||||||
|
self.assertEqual('deleted', image['status'])
|
||||||
|
|
||||||
|
def test_delete_invalid_encryption_key_id(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
extra_props = {
|
||||||
|
'cinder_encryption_key_id': 'invalid',
|
||||||
|
'cinder_encryption_key_deletion_policy': 'on_image_deletion',
|
||||||
|
}
|
||||||
|
created_image = self.controller.create(request,
|
||||||
|
image={'name': 'image-1'},
|
||||||
|
extra_properties=extra_props,
|
||||||
|
tags=[])
|
||||||
|
image_id = created_image.image_id
|
||||||
|
self.controller.delete(request, image_id)
|
||||||
|
# Ensure that image is deleted
|
||||||
|
image = self.db.image_get(request.context, image_id,
|
||||||
|
force_show_deleted=True)
|
||||||
|
self.assertTrue(image['deleted'])
|
||||||
|
self.assertEqual('deleted', image['status'])
|
||||||
|
|
||||||
|
def test_delete_invalid_encryption_key_deletion_policy(self):
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
extra_props = {
|
||||||
|
'cinder_encryption_key_deletion_policy': 'invalid',
|
||||||
|
}
|
||||||
|
created_image = self.controller.create(request,
|
||||||
|
image={'name': 'image-1'},
|
||||||
|
extra_properties=extra_props,
|
||||||
|
tags=[])
|
||||||
|
image_id = created_image.image_id
|
||||||
|
self.controller.delete(request, image_id)
|
||||||
|
# Ensure that image is deleted
|
||||||
|
image = self.db.image_get(request.context, image_id,
|
||||||
|
force_show_deleted=True)
|
||||||
|
self.assertTrue(image['deleted'])
|
||||||
|
self.assertEqual('deleted', image['status'])
|
||||||
|
|
||||||
|
|
||||||
class TestImagesControllerPolicies(base.IsolatedUnitTest):
|
class TestImagesControllerPolicies(base.IsolatedUnitTest):
|
||||||
|
|
||||||
|
@ -58,3 +58,5 @@ cursive>=0.2.1 # Apache-2.0
|
|||||||
iso8601>=0.1.11 # MIT
|
iso8601>=0.1.11 # MIT
|
||||||
|
|
||||||
os-win>=3.0.0 # Apache-2.0
|
os-win>=3.0.0 # Apache-2.0
|
||||||
|
|
||||||
|
castellan>=0.17.0 # Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user