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:
parent
b4b126d41c
commit
9c24bead99
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue