diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index fc8c653..ffd298e 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -349,7 +349,7 @@ class Controller(controller.BaseController): try: image_meta = registry.add_image_metadata(req.context, image_meta) - self.notifier.info("image.create", image_meta) + self.notifier.info("image.create", utils.redact_loc(image_meta)) return image_meta except exception.Duplicate: msg = (_("An image with identifier %s already exists") % @@ -421,7 +421,7 @@ class Controller(controller.BaseController): "to %(scheme)s store"), locals()) try: - self.notifier.info("image.prepare", image_meta) + self.notifier.info("image.prepare", utils.redact_loc(image_meta)) location, size, checksum = store.add( image_meta['id'], utils.CooperativeReader(image_data), @@ -456,7 +456,7 @@ class Controller(controller.BaseController): image_meta = registry.update_image_metadata(req.context, image_id, update_data) - self.notifier.info('image.upload', image_meta) + self.notifier.info('image.upload', utils.redact_loc(image_meta)) return location @@ -526,8 +526,8 @@ class Controller(controller.BaseController): image_meta_data = registry.update_image_metadata(req.context, image_id, image_meta) - self.notifier.info("image.activate", image_meta_data) - self.notifier.info("image.update", image_meta_data) + self.notifier.info("image.activate", utils.redact_loc(image_meta_data)) + self.notifier.info("image.update", utils.redact_loc(image_meta_data)) return image_meta_data except exception.Invalid, e: msg = (_("Failed to activate image. Got error: %(e)s") @@ -779,7 +779,7 @@ class Controller(controller.BaseController): request=req, content_type="text/plain") else: - self.notifier.info('image.update', image_meta) + self.notifier.info('image.update', utils.redact_loc(image_meta)) # Prevent client from learning the location, as it # could contain security credentials @@ -856,7 +856,7 @@ class Controller(controller.BaseController): request=req, content_type="text/plain") else: - self.notifier.info('image.delete', image) + self.notifier.info('image.delete', utils.redact_loc(image)) return Response(body='', status=200) def get_store_or_400(self, request, scheme): diff --git a/glance/api/v2/image_data.py b/glance/api/v2/image_data.py index a5cb8b6..d3b7ddc 100644 --- a/glance/api/v2/image_data.py +++ b/glance/api/v2/image_data.py @@ -55,7 +55,7 @@ class ImageDataController(object): def upload(self, req, image_id, data, size): try: image = self._get_image(req.context, image_id) - self.notifier.info("image.prepare", image) + self.notifier.info("image.prepare", utils.redact_loc(image)) location, size, checksum = self.store_api.add_to_backend( req.context, 'file', image_id, data, size) @@ -98,8 +98,8 @@ class ImageDataController(object): 'status': 'active'} self.db_api.image_update(req.context, image_id, values) updated_image = self._get_image(req.context, image_id) - self.notifier.info('image.upload', updated_image) - self.notifier.info('image.activate', updated_image) + self.notifier.info('image.upload', utils.redact_loc(updated_image)) + self.notifier.info('image.activate', utils.redact_loc(updated_image)) def download(self, req, image_id): self._enforce(req, 'download_image') diff --git a/glance/common/utils.py b/glance/common/utils.py index 02978f7..3e1936f 100644 --- a/glance/common/utils.py +++ b/glance/common/utils.py @@ -20,6 +20,7 @@ System-level utilities and helper functions. """ +import copy import errno try: @@ -434,6 +435,19 @@ def setup_remote_pydev_debug(host, port): raise +def redact_loc(image_meta): + """ + Create a shallow copy of image meta with 'location' redacted + for security (as it can contain credentials). + """ + if 'location' in image_meta: + tmp_image_meta = copy.copy(image_meta) + tmp_image_meta['location'] = '*' * len(str(image_meta['location'])) + return tmp_image_meta + + return image_meta + + class LazyPluggable(object): """A pluggable backend loaded lazily based on some value.""" diff --git a/glance/notifier/__init__.py b/glance/notifier/__init__.py index 4a68a50..515def0 100644 --- a/glance/notifier/__init__.py +++ b/glance/notifier/__init__.py @@ -20,6 +20,7 @@ import socket import uuid from glance.common import exception +from glance.common import utils import glance.domain from glance.openstack.common import cfg from glance.openstack.common import importutils @@ -125,15 +126,17 @@ class ImageRepoProxy(glance.domain.ImageRepoProxy): def save(self, image): self.image_repo.save(image) - self.notifier.info('image.update', format_image_notification(image)) + self.notifier.info('image.update', + utils.redact_loc(format_image_notification(image))) def add(self, image): self.image_repo.add(image) - self.notifier.info('image.create', format_image_notification(image)) + self.notifier.info('image.create', + utils.redact_loc(format_image_notification(image))) def remove(self, image): self.image_repo.remove(image) payload = format_image_notification(image) payload['deleted'] = True payload['deleted_at'] = timeutils.isotime() - self.notifier.info('image.delete', payload) + self.notifier.info('image.delete', utils.redact_loc(payload)) diff --git a/glance/tests/unit/test_notifier.py b/glance/tests/unit/test_notifier.py index 23aa399..84e7f3b 100644 --- a/glance/tests/unit/test_notifier.py +++ b/glance/tests/unit/test_notifier.py @@ -26,6 +26,7 @@ except ImportError: import stubout from glance.common import exception +from glance.common import utils from glance import notifier import glance.notifier.notify_kombu from glance.openstack.common import importutils @@ -430,7 +431,8 @@ class TestImageNotifications(utils.BaseTestCase): created_at=DATETIME, updated_at=DATETIME, owner=TENANT1, visibility='public', container_format='ami', tags=['one', 'two'], disk_format='ami', min_ram=128, - min_disk=10, checksum='ca425b88f047ce8ec45ee90e813ada91') + min_disk=10, checksum='ca425b88f047ce8ec45ee90e813ada91', + location='http://127.0.0.1') self.image_repo_stub = ImageRepoStub() self.notifier = unit_test_utils.FakeNotifier() self.image_repo_proxy = glance.notifier.ImageRepoProxy( @@ -445,6 +447,7 @@ class TestImageNotifications(utils.BaseTestCase): self.assertEqual(output_log['notification_type'], 'INFO') self.assertEqual(output_log['event_type'], 'image.update') self.assertEqual(output_log['payload']['id'], self.image.image_id) + self.assertEqual(output_log['payload']['location'], '****************') def test_image_add_notification(self): self.image_repo_proxy.add(self.image) @@ -454,6 +457,7 @@ class TestImageNotifications(utils.BaseTestCase): self.assertEqual(output_log['notification_type'], 'INFO') self.assertEqual(output_log['event_type'], 'image.create') self.assertEqual(output_log['payload']['id'], self.image.image_id) + self.assertEqual(output_log['payload']['location'], '****************') def test_image_delete_notification(self): self.image_repo_proxy.remove(self.image) @@ -464,3 +468,4 @@ class TestImageNotifications(utils.BaseTestCase): self.assertEqual(output_log['event_type'], 'image.delete') self.assertEqual(output_log['payload']['id'], self.image.image_id) self.assertTrue(output_log['payload']['deleted']) + self.assertEqual(output_log['payload']['location'], '****************') diff --git a/glance/tests/unit/test_utils.py b/glance/tests/unit/test_utils.py index ac7dacf..0f73a2f 100644 --- a/glance/tests/unit/test_utils.py +++ b/glance/tests/unit/test_utils.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import StringIO import tempfile @@ -103,3 +104,25 @@ class TestUtils(test_utils.BaseTestCase): byte = reader.read(1) self.assertRaises(exception.ImageSizeLimitExceeded, _consume_all_read) + + def test_redact_location(self): + """Ensure location redaction does not change original metadata""" + image_meta = {'size': 3, 'id': '123', 'location': 'http://localhost'} + redacted_image_meta = {'size': 3, 'id': '123', + 'location': '****************'} + copy_image_meta = copy.deepcopy(image_meta) + tmp_image_meta = utils.redact_loc(image_meta) + + self.assertEqual(image_meta, copy_image_meta) + self.assertEqual(tmp_image_meta, redacted_image_meta) + + def test_noop_redact_location(self): + """Check no-op location redaction does not change original metadata""" + image_meta = {'size': 3, 'id': '123'} + redacted_image_meta = {'size': 3, 'id': '123'} + copy_image_meta = copy.deepcopy(image_meta) + tmp_image_meta = utils.redact_loc(image_meta) + + self.assertEqual(image_meta, copy_image_meta) + self.assertEqual(tmp_image_meta, redacted_image_meta) + self.assertEqual(image_meta, redacted_image_meta) diff --git a/glance/tests/unit/v2/test_image_data_resource.py b/glance/tests/unit/v2/test_image_data_resource.py index 7aaf10c..2a1f887 100644 --- a/glance/tests/unit/v2/test_image_data_resource.py +++ b/glance/tests/unit/v2/test_image_data_resource.py @@ -18,6 +18,7 @@ import StringIO import webob import glance.api.v2.image_data +from glance.common import utils from glance.openstack.common import uuidutils from glance.tests.unit import base import glance.tests.unit.utils as unit_test_utils @@ -76,7 +77,7 @@ class TestImagesController(base.StoreClearingUnitTest): prepare_log = { 'notification_type': "INFO", 'event_type': "image.prepare", - 'payload': prepare_payload, + 'payload': utils.redact_loc(prepare_payload), } self.assertEqual(len(output_log), 3) prepare_updated_at = output_log[0]['payload']['updated_at'] @@ -93,7 +94,7 @@ class TestImagesController(base.StoreClearingUnitTest): upload_log = { 'notification_type': "INFO", 'event_type': "image.upload", - 'payload': upload_payload, + 'payload': utils.redact_loc(upload_payload), } self.assertEqual(len(output_log), 3) self.assertEqual(output_log[1], upload_log) @@ -107,7 +108,7 @@ class TestImagesController(base.StoreClearingUnitTest): activate_log = { 'notification_type': "INFO", 'event_type': "image.activate", - 'payload': activate_payload, + 'payload': utils.redact_loc(activate_payload), } self.assertEqual(len(output_log), 3) self.assertEqual(output_log[2], activate_log)