Allowing member to set status of image membership

Only the owner of an image can add members to the image.
By default the status of the image member is requested.
The member can change the status of the image membership
to accepted or rejected. This is done to prevent spamming
the user's image list. A member cannot see the other members
of the image.

Related to bp glance-api-v2-image-sharing

Change-Id: I0d0deba4b0df52b2f8d105b779fb7de746229d3a
This commit is contained in:
isethi 2013-02-09 03:26:42 +00:00 committed by iccha-sethi
parent b4b126d41c
commit 9c24bead99
14 changed files with 521 additions and 51 deletions

View File

@ -30,9 +30,27 @@ def is_image_mutable(context, image):
def proxy_image(context, image):
if is_image_mutable(context, image):
return ImageProxy(image)
return ImageProxy(image, context)
else:
return ImmutableImageProxy(image)
return ImmutableImageProxy(image, context)
def is_member_mutable(context, member):
"""Return True if the image is mutable in this context."""
if context.is_admin:
return True
if context.owner is None:
return False
return member.member_id == context.owner
def proxy_member(context, member):
if is_member_mutable(context, member):
return member
else:
return ImmutableMemberProxy(member)
class ImageRepoProxy(glance.domain.ImageRepoProxy):
@ -51,6 +69,62 @@ class ImageRepoProxy(glance.domain.ImageRepoProxy):
return [proxy_image(self.context, i) for i in images]
class ImageMembershipRepoProxy(glance.domain.ImageMembershipRepoProxy):
def __init__(self, member_repo, context):
self.context = context
self.member_repo = member_repo
super(ImageMembershipRepoProxy, self).__init__(member_repo)
def get(self, member_id):
if (self.context.is_admin or
self.context.owner == self.member_repo.image.owner or
self.context.owner == member_id):
member = self.member_repo.get(member_id)
return proxy_member(self.context, member)
else:
message = _("You cannot get image member for %s")
raise exception.Forbidden(message % member_id)
def list(self, *args, **kwargs):
members = self.member_repo.list(*args, **kwargs)
if (self.context.is_admin or
self.context.owner == self.member_repo.image.owner):
return [proxy_member(self.context, m) for m in members]
for member in members:
if member.member_id == self.context.owner:
return [proxy_member(self.context, member)]
message = _("You cannot get image member for %s")
raise exception.Forbidden(message % self.member_repo.image.image_id)
def remove(self, image_member):
if (self.member_repo.image.owner == self.context.owner or
self.context.is_admin):
self.member_repo.remove(image_member)
else:
message = _("You cannot delete image member for %s")
raise exception.Forbidden(message
% self.member_repo.image.image_id)
def add(self, image_member):
if (self.member_repo.image.owner == self.context.owner or
self.context.is_admin):
return self.member_repo.add(image_member)
else:
message = _("You cannot add image member for %s")
raise exception.Forbidden(message
% self.member_repo.image.image_id)
def save(self, image_member):
if (self.context.is_admin or
self.context.owner == image_member.member_id):
updated_member = self.member_repo.save(image_member)
return proxy_member(self.context, updated_member)
else:
message = _("You cannot update image member %s")
raise exception.Forbidden(message % image_member.member_id)
class ImageFactoryProxy(object):
def __init__(self, image_factory, context):
@ -139,8 +213,9 @@ class ImmutableTags(set):
class ImmutableImageProxy(object):
def __init__(self, base):
def __init__(self, base, context):
self.base = base
self.context = context
name = _immutable_attr('base', 'name')
image_id = _immutable_attr('base', 'image_id')
@ -167,14 +242,27 @@ class ImmutableImageProxy(object):
raise exception.Forbidden(message)
def get_member_repo(self):
message = _("You are not permitted to access this image.")
raise exception.Forbidden(message)
member_repo = self.base.get_member_repo()
return ImageMembershipRepoProxy(member_repo, self.context)
class ImmutableMemberProxy(object):
def __init__(self, base):
self.base = base
id = _immutable_attr('base', 'id')
image_id = _immutable_attr('base', 'image_id')
member_id = _immutable_attr('base', 'member_id')
status = _immutable_attr('base', 'status')
created_at = _immutable_attr('base', 'created_at')
updated_at = _immutable_attr('base', 'updated_at')
class ImageProxy(glance.domain.ImageProxy):
def __init__(self, image):
def __init__(self, image, context):
self.image = image
self.context = context
super(ImageProxy, self).__init__(image)
def get_member_repo(self, **kwargs):
@ -182,4 +270,5 @@ class ImageProxy(glance.domain.ImageProxy):
message = _("Public images do not have members.")
raise exception.Forbidden(message)
else:
return self.image.get_member_repo(**kwargs)
member_repo = self.image.get_member_repo(**kwargs)
return ImageMembershipRepoProxy(member_repo, self.context)

View File

@ -63,6 +63,7 @@ class ImageMembersController(object):
new_member = image_member_factory.new_image_member(image,
member_id)
member = member_repo.add(new_member)
self._update_store_acls(req, image)
return member
except exception.NotFound as e:
@ -70,6 +71,38 @@ class ImageMembersController(object):
except exception.Forbidden as e:
raise webob.exc.HTTPForbidden(explanation=unicode(e))
@utils.mutating
def update(self, req, image_id, member_id, status):
"""
Adds a membership to the image.
:param req: the Request object coming from the wsgi layer
:param image_id: the image identifier
:param member_id: the member identifier
:retval The response body is a mapping of the following form::
{'member_id': <MEMBER>,
'image_id': <IMAGE>,
'created_at': ..,
'updated_at': ..}
"""
image_repo = self.gateway.get_repo(req.context)
image_member_factory = self.gateway\
.get_image_member_factory(req.context)
try:
image = image_repo.get(image_id)
member_repo = image.get_member_repo()
member = member_repo.get(member_id)
member.status = status
member = member_repo.save(member)
return member
except exception.NotFound as e:
raise webob.exc.HTTPNotFound(explanation=unicode(e))
except exception.Forbidden as e:
raise webob.exc.HTTPForbidden(explanation=unicode(e))
except ValueError as e:
raise webob.exc.HTTPBadRequest(explanation=unicode(e))
def index(self, req, image_id):
"""
Return a list of dictionaries indicating the members of the
@ -140,13 +173,44 @@ class ImageMembersController(object):
content_type='text/plain')
class RequestDeserializer(wsgi.JSONRequestDeserializer):
def __init__(self):
super(RequestDeserializer, self).__init__()
def _get_request_body(self, request):
output = super(RequestDeserializer, self).default(request)
if 'body' not in output:
msg = _('Body expected in request.')
raise webob.exc.HTTPBadRequest(explanation=msg)
return output['body']
def create(self, request):
body = self._get_request_body(request)
try:
member_id = body['member']
except KeyError:
msg = _("Member to be added not specified")
raise webob.exc.HTTPBadRequest(explanation=msg)
return dict(member_id=member_id)
def update(self, request):
body = self._get_request_body(request)
try:
status = body['status']
except KeyError:
msg = _("Status not specified")
raise webob.exc.HTTPBadRequest(explanation=msg)
return dict(status=status)
class ResponseSerializer(wsgi.JSONResponseSerializer):
def __init__(self, schema=None):
super(ResponseSerializer, self).__init__()
def _format_image_member(self, member):
member_view = {}
attributes = ['member_id', 'image_id']
attributes = ['member_id', 'image_id', 'status']
for key in attributes:
member_view[key] = getattr(member, key)
member_view['created_at'] = timeutils.isotime(member.created_at)
@ -159,6 +223,12 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
response.unicode_body = unicode(body)
response.content_type = 'application/json'
def update(self, response, image_member):
image_member_view = self._format_image_member(image_member)
body = json.dumps(image_member_view, ensure_ascii=False)
response.unicode_body = unicode(body)
response.content_type = 'application/json'
def index(self, response, image_members):
image_members = image_members['members']
image_members_view = []
@ -172,7 +242,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
def create_resource():
"""Image Members resource factory method"""
deserializer = wsgi.JSONRequestDeserializer()
deserializer = RequestDeserializer()
serializer = ResponseSerializer()
controller = ImageMembersController()
return wsgi.Resource(controller, deserializer, serializer)

View File

@ -89,8 +89,12 @@ class API(wsgi.Router):
conditions={'method': ['GET']})
mapper.connect('/images/{image_id}/members/{member_id}',
controller=image_members_resource,
action='create',
action='update',
conditions={'method': ['PUT']})
mapper.connect('/images/{image_id}/members',
controller=image_members_resource,
action='create',
conditions={'method': ['POST']})
mapper.connect('/images/{image_id}/members/{member_id}',
controller=image_members_resource,
action='delete',

View File

@ -17,6 +17,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from glance.api import authorization
from glance.common import exception
import glance.domain
from glance.openstack.common import cfg
@ -189,34 +190,38 @@ class ImageProxy(glance.domain.ImageProxy):
super(ImageProxy, self).__init__(image)
def get_member_repo(self):
return ImageMemberRepo(self.context, self.db_api, self.image.image_id)
member_repo = ImageMemberRepo(self.context, self.db_api,
self.image)
return member_repo
class ImageMemberRepo(object):
def __init__(self, context, db_api, image_id):
def __init__(self, context, db_api, image):
self.context = context
self.db_api = db_api
self.image_id = image_id
self.image = image
def _format_image_member_from_db(self, db_image_member):
return glance.domain.ImageMembership(
id=db_image_member['id'],
image_id=self.image_id,
image_id=db_image_member['image_id'],
member_id=db_image_member['member'],
status=db_image_member['status'],
created_at=db_image_member['created_at'],
updated_at=db_image_member['updated_at']
)
def _format_image_member_to_db(self, image_member):
image_member = {'image_id': self.image_id,
image_member = {'image_id': self.image.image_id,
'member': image_member.member_id,
'status': image_member.status,
'created_at': image_member.created_at}
return image_member
def list(self):
db_members = self.db_api.image_member_find(self.context,
image_id=self.image_id)
db_members = self.db_api.image_member_find(
self.context, image_id=self.image.image_id)
image_members = []
for db_member in db_members:
image_members.append(self._format_image_member_from_db(db_member))
@ -238,11 +243,23 @@ class ImageMemberRepo(object):
except (exception.NotFound, exception.Forbidden):
raise exception.NotFound(member_id=image_member.id)
def save(self, image_member):
image_member_values = self._format_image_member_to_db(image_member)
try:
new_values = self.db_api.image_member_update(self.context,
image_member.id,
image_member_values)
except (exception.NotFound, exception.Forbidden):
raise exception.NotFound()
image_member.updated_at = new_values['updated_at']
return self._format_image_member_from_db(new_values)
def get(self, member_id):
try:
db_api_image_member = self.db_api.image_member_find(self.context,
self.image_id,
member_id)
db_api_image_member = self.db_api.image_member_find(
self.context,
self.image.image_id,
member_id)
if len(db_api_image_member) == 0:
raise exception.NotFound()
except (exception.NotFound, exception.Forbidden):

View File

@ -77,13 +77,14 @@ def _image_property_format(image_id, name, value):
}
def _image_member_format(image_id, tenant_id, can_share):
def _image_member_format(image_id, tenant_id, can_share, status='pending'):
dt = timeutils.utcnow()
return {
'id': uuidutils.generate_uuid(),
'image_id': image_id,
'member': tenant_id,
'can_share': can_share,
'status': status,
'created_at': dt,
'updated_at': dt,
}
@ -263,12 +264,14 @@ def image_property_delete(context, prop_ref, session=None):
@log_call
def image_member_find(context, image_id=None, member=None):
def image_member_find(context, image_id=None, member=None, status=None):
filters = []
if image_id is not None:
filters.append(lambda m: m['image_id'] == image_id)
if member is not None:
filters.append(lambda m: m['member'] == member)
if status is not None:
filters.append(lambda m: m['status'] == status)
members = DATA['members']
for f in filters:
@ -280,7 +283,8 @@ def image_member_find(context, image_id=None, member=None):
def image_member_create(context, values):
member = _image_member_format(values['image_id'],
values['member'],
values.get('can_share', False))
values.get('can_share', False),
values.get('status', 'pending'))
global DATA
DATA['members'].append(member)
return copy.deepcopy(member)

View File

@ -734,6 +734,7 @@ def _image_member_format(member_ref):
'image_id': member_ref['image_id'],
'member': member_ref['member'],
'can_share': member_ref['can_share'],
'status': member_ref['status'],
'created_at': member_ref['created_at'],
'updated_at': member_ref['updated_at']
}
@ -775,18 +776,19 @@ def _image_member_get(context, memb_id, session):
return query.one()
def image_member_find(context, image_id=None, member=None):
def image_member_find(context, image_id=None, member=None, status=None):
"""Find all members that meet the given criteria
:param image_id: identifier of image entity
:param member: tenant to which membership has been granted
"""
session = get_session()
members = _image_member_find(context, session, image_id, member)
members = _image_member_find(context, session, image_id, member, status)
return [_image_member_format(m) for m in members]
def _image_member_find(context, session, image_id=None, member=None):
def _image_member_find(context, session, image_id=None,
member=None, status=None):
# Note lack of permissions check; this function is called from
# is_image_visible(), so avoid recursive calls
query = session.query(models.ImageMember)
@ -796,6 +798,8 @@ def _image_member_find(context, session, image_id=None, member=None):
query = query.filter_by(image_id=image_id)
if member is not None:
query = query.filter_by(member=member)
if status is not None:
query = query.filter_by(status=status)
return query.all()

View File

@ -178,12 +178,25 @@ class ImageProxy(object):
class ImageMembership(object):
def __init__(self, image_id, member_id, created_at, updated_at, id=None):
def __init__(self, image_id, member_id, created_at, updated_at,
id=None, status=None):
self.id = id
self.image_id = image_id
self.member_id = member_id
self.created_at = created_at
self.updated_at = updated_at
self.status = status
@property
def status(self):
return self._status
@status.setter
def status(self, status):
if status not in ('pending', 'accepted', 'rejected'):
msg = _('Status must be "pending", "accepted" or "rejected".')
raise ValueError(msg)
self._status = status
class ImageMemberFactory(object):
@ -193,7 +206,8 @@ class ImageMemberFactory(object):
updated_at = created_at
return ImageMembership(image_id=image.image_id, member_id=member_id,
created_at=created_at, updated_at=updated_at)
created_at=created_at, updated_at=updated_at,
status='pending')
class ImageMembershipRepoProxy(object):
@ -210,5 +224,8 @@ class ImageMembershipRepoProxy(object):
def add(self, image_member):
return self.base.add(image_member)
def save(self, image_member):
return self.base.save(image_member)
def remove(self, image_member):
return self.base.remove(image_member)

View File

@ -518,6 +518,7 @@ class DriverTests(object):
'member': TENANT1,
'image_id': UUID1,
'can_share': False,
'status': 'pending',
}
self.assertEqual(expected, actual)
@ -532,6 +533,7 @@ class DriverTests(object):
expected = {'member': TENANT1,
'image_id': UUID1,
'status': 'pending',
'can_share': False}
self.assertEqual(expected, member)
@ -545,6 +547,7 @@ class DriverTests(object):
member.pop('updated_at')
expected = {'member': TENANT1,
'image_id': UUID1,
'status': 'pending',
'can_share': True}
self.assertEqual(expected, member)
@ -557,13 +560,51 @@ class DriverTests(object):
member.pop('updated_at')
self.assertEqual(expected, member)
def test_image_member_update_status(self):
TENANT1 = uuidutils.generate_uuid()
member = self.db_api.image_member_create(self.context,
{'member': TENANT1,
'image_id': UUID1})
member_id = member.pop('id')
member.pop('created_at')
member.pop('updated_at')
expected = {'member': TENANT1,
'image_id': UUID1,
'status': 'pending',
'can_share': False}
self.assertEqual(expected, member)
member = self.db_api.image_member_update(self.context,
member_id,
{'status': 'accepted'})
self.assertNotEqual(member['created_at'], member['updated_at'])
member.pop('id')
member.pop('created_at')
member.pop('updated_at')
expected = {'member': TENANT1,
'image_id': UUID1,
'status': 'accepted',
'can_share': False}
self.assertEqual(expected, member)
members = self.db_api.image_member_find(self.context,
member=TENANT1,
image_id=UUID1)
member = members[0]
member.pop('id')
member.pop('created_at')
member.pop('updated_at')
self.assertEqual(expected, member)
def test_image_member_find(self):
TENANT1 = uuidutils.generate_uuid()
TENANT2 = uuidutils.generate_uuid()
fixtures = [
{'member': TENANT1, 'image_id': UUID1},
{'member': TENANT1, 'image_id': UUID2},
{'member': TENANT2, 'image_id': UUID1},
{'member': TENANT1, 'image_id': UUID2, 'status': 'rejected'},
{'member': TENANT2, 'image_id': UUID1, 'status': 'accepted'},
]
for f in fixtures:
self.db_api.image_member_create(self.context, copy.deepcopy(f))
@ -586,6 +627,23 @@ class DriverTests(object):
image_id=UUID1)
_assertMemberListMatch([fixtures[2]], output)
output = self.db_api.image_member_find(self.context,
status='accepted')
_assertMemberListMatch([fixtures[2]], output)
output = self.db_api.image_member_find(self.context,
status='rejected')
_assertMemberListMatch([fixtures[1]], output)
output = self.db_api.image_member_find(self.context,
status='pending')
_assertMemberListMatch([fixtures[0]], output)
output = self.db_api.image_member_find(self.context,
status='pending',
image_id=UUID2)
_assertMemberListMatch([], output)
image_id = uuidutils.generate_uuid()
output = self.db_api.image_member_find(self.context,
member=TENANT2,

View File

@ -776,7 +776,7 @@ class TestImageMembers(functional.FunctionalTest):
images = json.loads(response.text)['images']
self.assertEqual(0, len(images))
owners = ['tenant1', 'tenant2']
owners = ['tenant1', 'tenant2', 'admin']
visibilities = ['public', 'private']
image_fixture = []
for owner in owners:
@ -799,18 +799,20 @@ class TestImageMembers(functional.FunctionalTest):
response = requests.get(path, headers=get_header('tenant1'))
self.assertEqual(200, response.status_code)
images = json.loads(response.text)['images']
self.assertEqual(3, len(images))
self.assertEqual(4, len(images))
# Add Image member for tenant1-private image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
response = requests.put(path, headers=get_header('tenant1'))
path = self._url('/v2/images/%s/members' % image_fixture[1]['id'])
body = json.dumps({'member': TENANT3})
response = requests.post(path, headers=get_header('tenant1'),
data=body)
self.assertEqual(200, response.status_code)
image_member = json.loads(response.text)
self.assertEqual(image_fixture[1]['id'], image_member['image_id'])
self.assertEqual(TENANT3, image_member['member_id'])
self.assertTrue('created_at' in image_member)
self.assertTrue('updated_at' in image_member)
self.assertEqual('pending', image_member['status'])
# Image tenant2-private's image members list should contain no members
path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
@ -819,10 +821,58 @@ class TestImageMembers(functional.FunctionalTest):
body = json.loads(response.text)
self.assertEqual(0, len(body['members']))
# Tenant 1, who is the owner cannot change status of image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
body = json.dumps({'status': 'accepted'})
response = requests.put(path, headers=get_header('tenant1'), data=body)
self.assertEqual(403, response.status_code)
# Image list should contain 4 images for TENANT3
path = self._url('/v2/images')
response = requests.get(path, headers=get_header(TENANT3))
self.assertEqual(200, response.status_code)
images = json.loads(response.text)['images']
self.assertEqual(4, len(images))
# Tenant 3 can change status of image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
body = json.dumps({'status': 'accepted'})
response = requests.put(path, headers=get_header(TENANT3), data=body)
self.assertEqual(200, response.status_code)
image_member = json.loads(response.text)
self.assertEqual(image_fixture[1]['id'], image_member['image_id'])
self.assertEqual(TENANT3, image_member['member_id'])
self.assertEqual('accepted', image_member['status'])
# Image list should contain 4 images for TENANT3 because status is
# accepted
path = self._url('/v2/images')
response = requests.get(path, headers=get_header(TENANT3))
self.assertEqual(200, response.status_code)
images = json.loads(response.text)['images']
self.assertEqual(4, len(images))
# Tenant 3 invalid status change
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
body = json.dumps({'status': 'invalid-status'})
response = requests.put(path, headers=get_header(TENANT3), data=body)
self.assertEqual(400, response.status_code)
# Owner cannot change status of image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
body = json.dumps({'status': 'accepted'})
response = requests.put(path, headers=get_header('tenant1'), data=body)
self.assertEqual(403, response.status_code)
# Add Image member for tenant2-private image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
TENANT4))
response = requests.put(path, headers=get_header('tenant2'))
path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
body = json.dumps({'member': TENANT4})
response = requests.post(path, headers=get_header('tenant2'),
data=body)
self.assertEqual(200, response.status_code)
image_member = json.loads(response.text)
self.assertEqual(image_fixture[3]['id'], image_member['image_id'])
@ -831,9 +881,10 @@ class TestImageMembers(functional.FunctionalTest):
self.assertTrue('updated_at' in image_member)
# Add Image member to public image
path = self._url('/v2/images/%s/members/%s' % (image_fixture[0]['id'],
TENANT2))
response = requests.put(path, headers=get_header('tenant1'))
path = self._url('/v2/images/%s/members' % image_fixture[0]['id'])
body = json.dumps({'member': TENANT2})
response = requests.post(path, headers=get_header('tenant1'),
data=body)
self.assertEqual(403, response.status_code)
# Image tenant1-private's members list should contain 1 member
@ -860,6 +911,12 @@ class TestImageMembers(functional.FunctionalTest):
response = requests.get(path, headers=get_header('tenant1'))
self.assertEqual(403, response.status_code)
# Image Member Cannot delete Image membership
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))
response = requests.delete(path, headers=get_header(TENANT3))
self.assertEqual(403, response.status_code)
# Delete Image member
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
TENANT3))

View File

@ -588,6 +588,7 @@ class TestImmutableImage(utils.BaseTestCase):
def setUp(self):
super(TestImmutableImage, self).setUp()
image_factory = glance.domain.ImageFactory()
self.context = glance.context.RequestContext(tenant=TENANT1)
image = image_factory.new_image(
image_id=UUID1,
name='Marvin',
@ -597,7 +598,7 @@ class TestImmutableImage(utils.BaseTestCase):
extra_properties={'foo': 'bar'},
tags=['ping', 'pong'],
)
self.image = authorization.ImmutableImageProxy(image)
self.image = authorization.ImmutableImageProxy(image, self.context)
def _test_change(self, attr, value):
self.assertRaises(exception.Forbidden,

View File

@ -193,11 +193,12 @@ class TestImageMemberRepo(test_utils.BaseTestCase):
self.context = glance.context.RequestContext(
user=USER1, tenant=TENANT1)
self.image_repo = glance.db.ImageRepo(self.context, self.db)
self.image_member_repo = glance.db.ImageMemberRepo(self.context,
self.db, UUID1)
self.image_member_factory = glance.domain.ImageMemberFactory()
self._create_images()
self._create_image_members()
image = self.image_repo.get(UUID1)
self.image_member_repo = glance.db.ImageMemberRepo(self.context,
self.db, image)
super(TestImageMemberRepo, self).setUp()
def _create_images(self):
@ -225,12 +226,20 @@ class TestImageMemberRepo(test_utils.BaseTestCase):
self.assertEqual(set([TENANT2, TENANT3]), image_member_ids)
def test_list_no_members(self):
image = self.image_repo.get(UUID2)
self.image_member_repo_uuid2 = glance.db.ImageMemberRepo(
self.context, self.db, UUID2)
self.context, self.db, image)
image_members = self.image_member_repo_uuid2.list()
image_member_ids = set([i.member_id for i in image_members])
self.assertEqual(set([]), image_member_ids)
def test_save_image_member(self):
image_member = self.image_member_repo.get(TENANT2)
image_member.status = 'accepted'
image_member_updated = self.image_member_repo.save(image_member)
self.assertTrue(image_member.id, image_member_updated.id)
self.assertEqual(image_member_updated.status, 'accepted')
def test_add_image_member(self):
image = self.image_repo.get(UUID1)
image_member = self.image_member_factory.new_image_member(image,
@ -242,6 +251,8 @@ class TestImageMemberRepo(test_utils.BaseTestCase):
image_member.image_id)
self.assertEqual(retreived_image_member.member_id,
image_member.member_id)
self.assertEqual(retreived_image_member.status,
'pending')
def test_remove_image_member(self):
image_member = self.image_member_repo.get(TENANT2)

View File

@ -151,6 +151,25 @@ class TestImage(test_utils.BaseTestCase):
self.assertRaises(exception.ProtectedImageDelete, self.image.delete)
class TestImageMember(test_utils.BaseTestCase):
def setUp(self):
super(TestImageMember, self).setUp()
self.image_member_factory = domain.ImageMemberFactory()
self.image_factory = domain.ImageFactory()
self.image = self.image_factory.new_image()
self.image_member = self.image_member_factory\
.new_image_member(image=self.image,
member_id=TENANT1)
def test_status_enumerated(self):
self.image_member.status = 'pending'
self.image_member.status = 'accepted'
self.image_member.status = 'rejected'
self.assertRaises(ValueError, setattr,
self.image_member, 'status', 'ellison')
class TestImageMemberFactory(test_utils.BaseTestCase):
def setUp(self):
@ -168,4 +187,5 @@ class TestImageMemberFactory(test_utils.BaseTestCase):
self.assertEqual(image_member.image_id, image.image_id)
self.assertTrue(image_member.created_at is not None)
self.assertEqual(image_member.created_at, image_member.updated_at)
self.assertEqual(image_member.status, 'pending')
self.assertTrue(image_member.member_id is not None)

View File

@ -35,19 +35,19 @@ USER3 = '2hss8dkl-d8jh-88yd-uhs9-879sdjsd8skd'
BASE_URI = 'swift+http://storeurl.com/container'
def get_fake_request(path='', method='POST', is_admin=False, user=USER1):
def get_fake_request(path='', method='POST', is_admin=False, user=USER1,
tenant=TENANT1):
req = wsgi.Request.blank(path)
req.method = method
kwargs = {
'user': user,
'tenant': TENANT1,
'tenant': tenant,
'roles': [],
'is_admin': is_admin,
}
req.context = glance.context.RequestContext(**kwargs)
return req

View File

@ -143,10 +143,22 @@ class TestImageMembersController(test_utils.BaseTestCase):
self.assertEqual(0, len(output['members']))
self.assertEqual({'members': []}, output)
def test_index_member_view(self):
# UUID3 is a private image owned by TENANT3
# UUID3 has members TENANT2 and TENANT4
# When TENANT4 lists members for UUID3, should not see TENANT2
request = unit_test_utils.get_fake_request(tenant=TENANT4)
output = self.controller.index(request, UUID3)
self.assertEqual(1, len(output['members']))
actual = set([image_member.member_id
for image_member in output['members']])
expected = set([TENANT4])
self.assertEqual(actual, expected)
def test_index_private_image(self):
request = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPForbidden, self.controller.index,
request, UUID4)
request = unit_test_utils.get_fake_request(tenant=TENANT2)
self.assertRaises(webob.exc.HTTPNotFound, self.controller.index,
request, UUID5)
def test_index_public_image(self):
request = unit_test_utils.get_fake_request()
@ -171,6 +183,31 @@ class TestImageMembersController(test_utils.BaseTestCase):
self.assertEqual(UUID2, output.image_id)
self.assertEqual(TENANT3, output.member_id)
def test_update_done_by_member(self):
request = unit_test_utils.get_fake_request(tenant=TENANT4)
image_id = UUID2
member_id = TENANT4
output = self.controller.update(request, image_id=image_id,
member_id=member_id,
status='accepted')
self.assertEqual(UUID2, output.image_id)
self.assertEqual(TENANT4, output.member_id)
self.assertEqual('accepted', output.status)
def test_update_done_by_owner(self):
request = unit_test_utils.get_fake_request(tenant=TENANT1)
image_id = UUID2
member_id = TENANT4
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
request, UUID2, TENANT4, status='accepted')
def test_update_invalid_status(self):
request = unit_test_utils.get_fake_request(tenant=TENANT4)
image_id = UUID2
member_id = TENANT4
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
request, UUID2, TENANT4, status='accept')
def test_create_private_image(self):
request = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
@ -197,6 +234,18 @@ class TestImageMembersController(test_utils.BaseTestCase):
found_member = self.db.image_member_find(image_id, member_id)
self.assertEqual(found_member, [])
def test_delete_by_member(self):
request = unit_test_utils.get_fake_request(tenant=TENANT4)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
request, UUID2, TENANT4)
request = unit_test_utils.get_fake_request()
output = self.controller.index(request, UUID2)
self.assertEqual(1, len(output['members']))
actual = set([image_member.member_id
for image_member in output['members']])
expected = set([TENANT4])
self.assertEqual(actual, expected)
def test_delete_private_image(self):
request = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
@ -231,8 +280,10 @@ class TestImageMembersSerializer(test_utils.BaseTestCase):
self.serializer = glance.api.v2.image_members.ResponseSerializer()
self.fixtures = [
_domain_fixture(id='1', image_id=UUID2, member_id=TENANT1,
status='accepted',
created_at=DATETIME, updated_at=DATETIME),
_domain_fixture(id='2', image_id=UUID2, member_id=TENANT2,
status='pending',
created_at=DATETIME, updated_at=DATETIME),
]
@ -242,12 +293,14 @@ class TestImageMembersSerializer(test_utils.BaseTestCase):
{
'image_id': UUID2,
'member_id': TENANT1,
'status': 'accepted',
'created_at': ISOTIME,
'updated_at': ISOTIME,
},
{
'image_id': UUID2,
'member_id': TENANT2,
'status': 'pending',
'created_at': ISOTIME,
'updated_at': ISOTIME,
},
@ -264,6 +317,7 @@ class TestImageMembersSerializer(test_utils.BaseTestCase):
def test_create(self):
expected = {'image_id': UUID2,
'member_id': TENANT1,
'status': 'accepted',
'created_at': ISOTIME,
'updated_at': ISOTIME}
request = webob.Request.blank('/v2/images/%s/members/%s'
@ -274,3 +328,67 @@ class TestImageMembersSerializer(test_utils.BaseTestCase):
actual = json.loads(response.body)
self.assertEqual(expected, actual)
self.assertEqual('application/json', response.content_type)
def test_update(self):
expected = {'image_id': UUID2,
'member_id': TENANT1,
'status': 'accepted',
'created_at': ISOTIME,
'updated_at': ISOTIME}
request = webob.Request.blank('/v2/images/%s/members/%s'
% (UUID2, TENANT1))
response = webob.Response(request=request)
result = self.fixtures[0]
self.serializer.update(response, result)
actual = json.loads(response.body)
self.assertEqual(expected, actual)
self.assertEqual('application/json', response.content_type)
class TestImagesDeserializer(test_utils.BaseTestCase):
def setUp(self):
super(TestImagesDeserializer, self).setUp()
self.deserializer = glance.api.v2.image_members.RequestDeserializer()
def test_create(self):
request = unit_test_utils.get_fake_request()
request.body = json.dumps({'member': TENANT1})
image_id = UUID1
output = self.deserializer.create(request)
expected = {'member_id': TENANT1}
self.assertEqual(expected, output)
def test_create_invalid(self):
request = unit_test_utils.get_fake_request()
image_id = UUID1
request.body = json.dumps({'mem': TENANT1})
self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.create,
request)
def test_create_no_body(self):
request = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.create,
request)
def test_update(self):
request = unit_test_utils.get_fake_request()
request.body = json.dumps({'status': 'accepted'})
image_id = UUID1
member_id = TENANT1
output = self.deserializer.update(request)
expected = {'status': 'accepted'}
self.assertEqual(expected, output)
def test_update_invalid(self):
request = unit_test_utils.get_fake_request()
image_id = UUID1
member_id = TENANT1
request.body = json.dumps({'mem': TENANT1})
self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.update,
request)
def test_update_no_body(self):
request = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.update,
request)