Merge "Allowing member to set status of image membership"
This commit is contained in:
commit
61d31716e6
|
@ -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
|
||||
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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