diff --git a/glance/api/authorization.py b/glance/api/authorization.py index e37b2be9c4..e5d3708715 100644 --- a/glance/api/authorization.py +++ b/glance/api/authorization.py @@ -333,6 +333,7 @@ class ImmutableImageProxy(object): virtual_size = _immutable_attr('base', 'virtual_size') extra_properties = _immutable_attr('base', 'extra_properties', proxy=ImmutableProperties) + member = _immutable_attr('base', 'member') tags = _immutable_attr('base', 'tags', proxy=ImmutableTags) def delete(self): diff --git a/glance/api/policy.py b/glance/api/policy.py index 7fa1fe5fb2..e0925ff2e3 100644 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -548,12 +548,13 @@ class ImageTarget(abc.Mapping): yield alias def key_transforms(self, key): - if key == 'id': - key = 'image_id' - elif key == 'project_id': - key = 'owner' + transforms = { + 'id': 'image_id', + 'project_id': 'owner', + 'member_id': 'member', + } - return key + return transforms.get(key, key) # Metadef Namespace classes diff --git a/glance/db/__init__.py b/glance/db/__init__.py index 144af0c20d..182832a89f 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -105,6 +105,18 @@ class ImageRepo(object): key = CONF.metadata_encryption_key for l in locations: l['url'] = crypt.urlsafe_decrypt(key, l['url']) + + # NOTE(danms): If the image is shared and we are not the + # owner, we must have found it because we are a member. Set + # our tenant on the image as 'member' for policy checks in the + # upper layers. For any other image stage, we found the image + # some other way, so leave member=None. + if (db_image['visibility'] == 'shared' and + self.context.owner != db_image['owner']): + member = self.context.owner + else: + member = None + return glance.domain.Image( image_id=db_image['id'], name=db_image['name'], @@ -127,6 +139,7 @@ class ImageRepo(object): extra_properties=properties, tags=db_tags, os_hidden=db_image['os_hidden'], + member=member, ) def _format_image_to_db(self, image): diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index d45abdf6e9..5e72b8056e 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -138,6 +138,7 @@ class Image(object): extra_properties = kwargs.pop('extra_properties', {}) self.extra_properties = ExtraProperties(extra_properties) self.tags = kwargs.pop('tags', []) + self.member = kwargs.pop('member', None) if kwargs: message = _("__init__() got unexpected keyword argument '%s'") raise TypeError(message % list(kwargs.keys())[0]) diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index 8c7325cccb..617f9c4ee2 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -194,6 +194,7 @@ class Image(object): virtual_size = _proxy('base', 'virtual_size') extra_properties = _proxy('base', 'extra_properties') tags = _proxy('base', 'tags') + member = _proxy('base', 'member') def delete(self): self.base.delete() diff --git a/glance/tests/unit/test_cache_middleware.py b/glance/tests/unit/test_cache_middleware.py index 5a502bf2b0..49f1198f9a 100644 --- a/glance/tests/unit/test_cache_middleware.py +++ b/glance/tests/unit/test_cache_middleware.py @@ -56,6 +56,7 @@ class ImageStub(object): self.owner = owner self.virtual_size = 0 self.tags = [] + self.member = self.owner class TestCacheMiddlewareURLMatching(testtools.TestCase): diff --git a/glance/tests/unit/test_db.py b/glance/tests/unit/test_db.py index 87a643a6dc..db8e1e7fff 100644 --- a/glance/tests/unit/test_db.py +++ b/glance/tests/unit/test_db.py @@ -21,6 +21,7 @@ import uuid from oslo_config import cfg from oslo_db import exception as db_exc from oslo_utils import encodeutils +from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils import timeutils from sqlalchemy import orm as sa_orm @@ -309,6 +310,27 @@ class TestImageRepo(test_utils.BaseTestCase): image_ids = set([i.image_id for i in images]) self.assertEqual(set([UUID2]), image_ids) + def test_list_shared_images_other_tenant(self): + # Create a private image owned by TENANT3 + image5 = _db_fixture(uuids.image5, owner=TENANT3, + name='5', size=512, is_public=False) + self.db.image_create(None, image5) + + # Get a repo as TENANT3, since it has access to public, + # shared, and private images + context = glance.context.RequestContext(user=USER1, tenant=TENANT3) + image_repo = glance.db.ImageRepo(context, self.db) + images = {i.image_id: i for i in image_repo.list()} + + # No member set for public image UUID1 + self.assertIsNone(images[UUID1].member) + + # Member should be set to our tenant id for shared image UUID2 + self.assertEqual(TENANT3, images[UUID2].member) + + # No member set for private image5 + self.assertIsNone(images[uuids.image5].member) + def test_list_all_images(self): filters = {'visibility': 'all'} images = self.image_repo.list(filters=filters) diff --git a/glance/tests/unit/test_gateway.py b/glance/tests/unit/test_gateway.py index 6521c0ab5a..d7d12bf26a 100644 --- a/glance/tests/unit/test_gateway.py +++ b/glance/tests/unit/test_gateway.py @@ -17,8 +17,10 @@ from unittest import mock from glance.api import authorization from glance.api import property_protections +from glance import context from glance import gateway from glance import notifier +from glance.tests.unit import utils as unit_test_utils import glance.tests.utils as test_utils @@ -90,3 +92,24 @@ class TestGateway(test_utils.BaseTestCase): repo = self.gateway.get_repo(self.context, authorization_layer=False) self.assertIsInstance(repo, property_protections.ProtectedImageRepoProxy) + + def test_get_repo_member_property(self): + """Test that the image.member property is propagated all the way from + the DB to the top of the gateway repo stack. + """ + db_api = unit_test_utils.FakeDB() + gw = gateway.Gateway(db_api=db_api) + + # Get the UUID1 image as TENANT1 + ctxt = context.RequestContext(tenant=unit_test_utils.TENANT1) + repo = gw.get_repo(ctxt) + image = repo.get(unit_test_utils.UUID1) + # We own the image, so member is None + self.assertIsNone(image.member) + + # Get the UUID1 image as TENANT2 + ctxt = context.RequestContext(tenant=unit_test_utils.TENANT2) + repo = gw.get_repo(ctxt) + image = repo.get(unit_test_utils.UUID1) + # We are a member, so member is our tenant id + self.assertEqual(unit_test_utils.TENANT2, image.member) diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py index 2857d7afac..0a2550a806 100644 --- a/glance/tests/unit/test_policy.py +++ b/glance/tests/unit/test_policy.py @@ -96,6 +96,7 @@ class ImageStub(object): self.virtual_size = 0 self.tags = [] self.os_hidden = os_hidden + self.member = self.owner def delete(self): self.status = 'deleted' @@ -1115,3 +1116,21 @@ class TestImageTarget(base.IsolatedUnitTest): self.assertIn('project_id', target) self.assertEqual(image.owner, target['project_id']) self.assertEqual(image.owner, target['owner']) + + def test_image_target_transforms(self): + fake_image = mock.MagicMock() + fake_image.image_id = mock.sentinel.image_id + fake_image.owner = mock.sentinel.owner + fake_image.member = mock.sentinel.member + + target = glance.api.policy.ImageTarget(fake_image) + + # Make sure the key transforms work + self.assertEqual(mock.sentinel.image_id, target['id']) + self.assertEqual(mock.sentinel.owner, target['project_id']) + self.assertEqual(mock.sentinel.member, target['member_id']) + + # Also make sure the base properties still work + self.assertEqual(mock.sentinel.image_id, target['image_id']) + self.assertEqual(mock.sentinel.owner, target['owner']) + self.assertEqual(mock.sentinel.member, target['member']) diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 8b1309038d..4765f9f483 100644 --- a/glance/tests/unit/v2/test_images_resource.py +++ b/glance/tests/unit/v2/test_images_resource.py @@ -187,6 +187,7 @@ class FakeImage(object): self.name = 'foo' self.tags = [] self.extra_properties = {} + self.member = self.owner # NOTE(danms): This fixture looks more like the db object than # the proxy model. This needs fixing all through the tests