Merge "Allowing member to set status of image membership"

This commit is contained in:
Jenkins 2013-02-18 04:34:30 +00:00 committed by Gerrit Code Review
commit 61d31716e6
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
@ -192,34 +193,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))
@ -241,11 +246,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

@ -192,11 +192,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):
@ -224,12 +225,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,
@ -241,6 +250,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)