Browse Source

Merge "Delete secret key on image deletion"

tags/19.0.0.0b1
Zuul 2 weeks ago
parent
commit
e475581c72

+ 12
- 0
etc/schema-image.json View File

@@ -28,5 +28,17 @@
28 28
     "description": {
29 29
         "description": "A human-readable string describing this image.",
30 30
         "type": "string"
31
+    },
32
+    "cinder_encryption_key_id": {
33
+        "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",
34
+        "type": "string"
35
+    },
36
+    "cinder_encryption_key_deletion_policy": {
37
+        "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",
38
+        "type": "string",
39
+        "enum": [
40
+            "on_image_deletion",
41
+            "do_not_delete"
42
+        ]
31 43
     }
32 44
 }

+ 31
- 0
glance/api/v2/images.py View File

@@ -17,6 +17,8 @@ import hashlib
17 17
 import os
18 18
 import re
19 19
 
20
+from castellan.common import exception as castellan_exception
21
+from castellan import key_manager
20 22
 import glance_store
21 23
 from oslo_config import cfg
22 24
 from oslo_log import log as logging
@@ -60,6 +62,8 @@ class ImagesController(object):
60 62
         self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
61 63
                                               self.notifier, self.policy)
62 64
 
65
+        self._key_manager = key_manager.API(CONF)
66
+
63 67
     @utils.mutating
64 68
     def create(self, req, image, extra_properties, tags):
65 69
         image_factory = self.gateway.get_image_factory(req.context)
@@ -330,6 +334,32 @@ class ImagesController(object):
330 334
                 msg = _("Property %s does not exist.")
331 335
                 raise webob.exc.HTTPConflict(msg % path_root)
332 336
 
337
+    def _delete_encryption_key(self, context, image):
338
+        props = image.extra_properties
339
+
340
+        cinder_encryption_key_id = props.get('cinder_encryption_key_id')
341
+        if cinder_encryption_key_id is None:
342
+            return
343
+
344
+        deletion_policy = props.get('cinder_encryption_key_deletion_policy',
345
+                                    '')
346
+        if deletion_policy != 'on_image_deletion':
347
+            return
348
+
349
+        try:
350
+            self._key_manager.delete(context, cinder_encryption_key_id)
351
+        except castellan_exception.Forbidden:
352
+            msg = ('Not allowed to delete encryption key %s' %
353
+                   cinder_encryption_key_id)
354
+            LOG.warn(msg)
355
+        except (castellan_exception.ManagedObjectNotFoundError, KeyError):
356
+            msg = 'Could not find encryption key %s' % cinder_encryption_key_id
357
+            LOG.warn(msg)
358
+        except castellan_exception.KeyManagerError:
359
+            msg = ('Failed to delete cinder encryption key %s' %
360
+                   cinder_encryption_key_id)
361
+            LOG.warn(msg)
362
+
333 363
     @utils.mutating
334 364
     def delete(self, req, image_id):
335 365
         image_repo = self.gateway.get_repo(req.context)
@@ -358,6 +388,7 @@ class ImagesController(object):
358 388
                         "it cannot be found at %(fn)s"), {'fn': file_path})
359 389
 
360 390
             image.delete()
391
+            self._delete_encryption_key(req.context, image)
361 392
             image_repo.remove(image)
362 393
         except (glance_store.Forbidden, exception.Forbidden) as e:
363 394
             LOG.debug("User not permitted to delete image '%s'", image_id)

+ 0
- 0
glance/tests/unit/keymgr/__init__.py View File


+ 25
- 0
glance/tests/unit/keymgr/fake.py View File

@@ -0,0 +1,25 @@
1
+# Copyright 2011 Justin Santa Barbara
2
+# Copyright 2012 OpenStack Foundation
3
+# Copyright 2019 Red Hat
4
+# All Rights Reserved.
5
+#
6
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+#    not use this file except in compliance with the License. You may obtain
8
+#    a copy of the License at
9
+#
10
+#         http://www.apache.org/licenses/LICENSE-2.0
11
+#
12
+#    Unless required by applicable law or agreed to in writing, software
13
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+#    License for the specific language governing permissions and limitations
16
+#    under the License.
17
+
18
+"""Implementation of a fake key manager."""
19
+
20
+
21
+from castellan.tests.unit.key_manager import mock_key_manager
22
+
23
+
24
+def fake_api(configuration=None):
25
+    return mock_key_manager.MockKeyManager(configuration)

+ 196
- 0
glance/tests/unit/v2/test_images_resource.py View File

@@ -19,6 +19,7 @@ import hashlib
19 19
 import os
20 20
 import uuid
21 21
 
22
+from castellan.common import exception as castellan_exception
22 23
 import glance_store as store
23 24
 import mock
24 25
 from oslo_serialization import jsonutils
@@ -36,6 +37,7 @@ from glance.common import store_utils
36 37
 from glance import domain
37 38
 import glance.schema
38 39
 from glance.tests.unit import base
40
+from glance.tests.unit.keymgr import fake as fake_keymgr
39 41
 import glance.tests.unit.utils as unit_test_utils
40 42
 import glance.tests.utils as test_utils
41 43
 
@@ -155,6 +157,7 @@ class TestImagesController(base.IsolatedUnitTest):
155 157
                                                          self.notifier,
156 158
                                                          self.store))
157 159
         self.controller.gateway.store_utils = self.store_utils
160
+        self.controller._key_manager = fake_keymgr.fake_api()
158 161
         store.create_stores()
159 162
 
160 163
     def _create_images(self):
@@ -2721,6 +2724,199 @@ class TestImagesController(base.IsolatedUnitTest):
2721 2724
                               request, UUID4, {'method': {'name':
2722 2725
                                                           'glance-direct'}})
2723 2726
 
2727
+    def test_delete_encryption_key_no_encryption_key(self):
2728
+        request = unit_test_utils.get_fake_request()
2729
+        fake_encryption_key = self.controller._key_manager.store(
2730
+            request.context, mock.Mock())
2731
+        image = _domain_fixture(
2732
+            UUID2, name='image-2', owner=TENANT2,
2733
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2734
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2735
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2736
+            virtual_size=3072, extra_properties={})
2737
+        self.controller._delete_encryption_key(request.context, image)
2738
+        # Make sure the encrytion key is still there
2739
+        key = self.controller._key_manager.get(request.context,
2740
+                                               fake_encryption_key)
2741
+        self.assertEqual(fake_encryption_key, key._id)
2742
+
2743
+    def test_delete_encryption_key_no_deletion_policy(self):
2744
+        request = unit_test_utils.get_fake_request()
2745
+        fake_encryption_key = self.controller._key_manager.store(
2746
+            request.context, mock.Mock())
2747
+        props = {
2748
+            'cinder_encryption_key_id': fake_encryption_key,
2749
+        }
2750
+        image = _domain_fixture(
2751
+            UUID2, name='image-2', owner=TENANT2,
2752
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2753
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2754
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2755
+            virtual_size=3072, extra_properties=props)
2756
+        self.controller._delete_encryption_key(request.context, image)
2757
+        # Make sure the encrytion key is still there
2758
+        key = self.controller._key_manager.get(request.context,
2759
+                                               fake_encryption_key)
2760
+        self.assertEqual(fake_encryption_key, key._id)
2761
+
2762
+    def test_delete_encryption_key_do_not_delete(self):
2763
+        request = unit_test_utils.get_fake_request()
2764
+        fake_encryption_key = self.controller._key_manager.store(
2765
+            request.context, mock.Mock())
2766
+        props = {
2767
+            'cinder_encryption_key_id': fake_encryption_key,
2768
+            'cinder_encryption_key_deletion_policy': 'do_not_delete',
2769
+        }
2770
+        image = _domain_fixture(
2771
+            UUID2, name='image-2', owner=TENANT2,
2772
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2773
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2774
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2775
+            virtual_size=3072, extra_properties=props)
2776
+        self.controller._delete_encryption_key(request.context, image)
2777
+        # Make sure the encrytion key is still there
2778
+        key = self.controller._key_manager.get(request.context,
2779
+                                               fake_encryption_key)
2780
+        self.assertEqual(fake_encryption_key, key._id)
2781
+
2782
+    def test_delete_encryption_key_forbidden(self):
2783
+        request = unit_test_utils.get_fake_request()
2784
+        fake_encryption_key = self.controller._key_manager.store(
2785
+            request.context, mock.Mock())
2786
+        props = {
2787
+            'cinder_encryption_key_id': fake_encryption_key,
2788
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2789
+        }
2790
+        image = _domain_fixture(
2791
+            UUID2, name='image-2', owner=TENANT2,
2792
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2793
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2794
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2795
+            virtual_size=3072, extra_properties=props)
2796
+        with mock.patch.object(self.controller._key_manager, 'delete',
2797
+                               side_effect=castellan_exception.Forbidden):
2798
+            self.controller._delete_encryption_key(request.context, image)
2799
+        # Make sure the encrytion key is still there
2800
+        key = self.controller._key_manager.get(request.context,
2801
+                                               fake_encryption_key)
2802
+        self.assertEqual(fake_encryption_key, key._id)
2803
+
2804
+    def test_delete_encryption_key_not_found(self):
2805
+        request = unit_test_utils.get_fake_request()
2806
+        fake_encryption_key = self.controller._key_manager.store(
2807
+            request.context, mock.Mock())
2808
+        props = {
2809
+            'cinder_encryption_key_id': fake_encryption_key,
2810
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2811
+        }
2812
+        image = _domain_fixture(
2813
+            UUID2, name='image-2', owner=TENANT2,
2814
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2815
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2816
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2817
+            virtual_size=3072, extra_properties=props)
2818
+        with mock.patch.object(self.controller._key_manager, 'delete',
2819
+                               side_effect=castellan_exception.ManagedObjectNotFoundError):  # noqa
2820
+            self.controller._delete_encryption_key(request.context, image)
2821
+        # Make sure the encrytion key is still there
2822
+        key = self.controller._key_manager.get(request.context,
2823
+                                               fake_encryption_key)
2824
+        self.assertEqual(fake_encryption_key, key._id)
2825
+
2826
+    def test_delete_encryption_key_error(self):
2827
+        request = unit_test_utils.get_fake_request()
2828
+        fake_encryption_key = self.controller._key_manager.store(
2829
+            request.context, mock.Mock())
2830
+        props = {
2831
+            'cinder_encryption_key_id': fake_encryption_key,
2832
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2833
+        }
2834
+        image = _domain_fixture(
2835
+            UUID2, name='image-2', owner=TENANT2,
2836
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2837
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2838
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2839
+            virtual_size=3072, extra_properties=props)
2840
+        with mock.patch.object(self.controller._key_manager, 'delete',
2841
+                               side_effect=castellan_exception.KeyManagerError):  # noqa
2842
+            self.controller._delete_encryption_key(request.context, image)
2843
+        # Make sure the encrytion key is still there
2844
+        key = self.controller._key_manager.get(request.context,
2845
+                                               fake_encryption_key)
2846
+        self.assertEqual(fake_encryption_key, key._id)
2847
+
2848
+    def test_delete_encryption_key(self):
2849
+        request = unit_test_utils.get_fake_request()
2850
+        fake_encryption_key = self.controller._key_manager.store(
2851
+            request.context, mock.Mock())
2852
+        props = {
2853
+            'cinder_encryption_key_id': fake_encryption_key,
2854
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2855
+        }
2856
+        image = _domain_fixture(
2857
+            UUID2, name='image-2', owner=TENANT2,
2858
+            checksum='ca425b88f047ce8ec45ee90e813ada91',
2859
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
2860
+            created_at=DATETIME, updated_at=DATETIME, size=1024,
2861
+            virtual_size=3072, extra_properties=props)
2862
+        self.controller._delete_encryption_key(request.context, image)
2863
+        # Make sure the encrytion key is gone
2864
+        self.assertRaises(KeyError,
2865
+                          self.controller._key_manager.get,
2866
+                          request.context, fake_encryption_key)
2867
+
2868
+    def test_delete_no_encryption_key_id(self):
2869
+        request = unit_test_utils.get_fake_request()
2870
+        extra_props = {
2871
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2872
+        }
2873
+        created_image = self.controller.create(request,
2874
+                                               image={'name': 'image-1'},
2875
+                                               extra_properties=extra_props,
2876
+                                               tags=[])
2877
+        image_id = created_image.image_id
2878
+        self.controller.delete(request, image_id)
2879
+        # Ensure that image is deleted
2880
+        image = self.db.image_get(request.context, image_id,
2881
+                                  force_show_deleted=True)
2882
+        self.assertTrue(image['deleted'])
2883
+        self.assertEqual('deleted', image['status'])
2884
+
2885
+    def test_delete_invalid_encryption_key_id(self):
2886
+        request = unit_test_utils.get_fake_request()
2887
+        extra_props = {
2888
+            'cinder_encryption_key_id': 'invalid',
2889
+            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
2890
+        }
2891
+        created_image = self.controller.create(request,
2892
+                                               image={'name': 'image-1'},
2893
+                                               extra_properties=extra_props,
2894
+                                               tags=[])
2895
+        image_id = created_image.image_id
2896
+        self.controller.delete(request, image_id)
2897
+        # Ensure that image is deleted
2898
+        image = self.db.image_get(request.context, image_id,
2899
+                                  force_show_deleted=True)
2900
+        self.assertTrue(image['deleted'])
2901
+        self.assertEqual('deleted', image['status'])
2902
+
2903
+    def test_delete_invalid_encryption_key_deletion_policy(self):
2904
+        request = unit_test_utils.get_fake_request()
2905
+        extra_props = {
2906
+            'cinder_encryption_key_deletion_policy': 'invalid',
2907
+        }
2908
+        created_image = self.controller.create(request,
2909
+                                               image={'name': 'image-1'},
2910
+                                               extra_properties=extra_props,
2911
+                                               tags=[])
2912
+        image_id = created_image.image_id
2913
+        self.controller.delete(request, image_id)
2914
+        # Ensure that image is deleted
2915
+        image = self.db.image_get(request.context, image_id,
2916
+                                  force_show_deleted=True)
2917
+        self.assertTrue(image['deleted'])
2918
+        self.assertEqual('deleted', image['status'])
2919
+
2724 2920
 
2725 2921
 class TestImagesControllerPolicies(base.IsolatedUnitTest):
2726 2922
 

+ 2
- 0
requirements.txt View File

@@ -58,3 +58,5 @@ cursive>=0.2.1 # Apache-2.0
58 58
 iso8601>=0.1.11 # MIT
59 59
 
60 60
 os-win>=3.0.0 # Apache-2.0
61
+
62
+castellan>=0.17.0 # Apache-2.0

Loading…
Cancel
Save