Implement v2 API access resource
* Add functional tests * Implements bp api-v2-image-access Change-Id: If62f4c0c9b387bb1f99306d68b18ad21f5f875d1
This commit is contained in:
parent
f9afe586b6
commit
28d85923fa
@ -16,28 +16,64 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
from glance.api.v2 import base
|
from glance.api.v2 import base
|
||||||
from glance.api.v2 import schemas
|
from glance.api.v2 import schemas
|
||||||
|
from glance.common import exception
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
import glance.registry.db.api
|
import glance.registry.db.api
|
||||||
|
|
||||||
|
|
||||||
class ImageAccessController(base.Controller):
|
class Controller(base.Controller):
|
||||||
def __init__(self, conf, db=None):
|
def __init__(self, conf, db=None):
|
||||||
super(ImageAccessController, self).__init__(conf)
|
super(Controller, self).__init__(conf)
|
||||||
self.db_api = db or glance.registry.db.api
|
self.db_api = db or glance.registry.db.api
|
||||||
self.db_api.configure_db(conf)
|
self.db_api.configure_db(conf)
|
||||||
|
|
||||||
def index(self, req, image_id):
|
def index(self, req, image_id):
|
||||||
image = self.db_api.image_get(req.context, image_id)
|
image = self.db_api.image_get(req.context, image_id)
|
||||||
return image['members']
|
#TODO(bcwaldon): We have to filter on non-deleted members
|
||||||
|
# manually. This should be done for us in the db api
|
||||||
|
return filter(lambda m: not m['deleted'], image['members'])
|
||||||
|
|
||||||
def show(self, req, image_id, tenant_id):
|
def show(self, req, image_id, tenant_id):
|
||||||
return self.db_api.image_member_find(req.context, image_id, tenant_id)
|
try:
|
||||||
|
return self.db_api.image_member_find(req.context,
|
||||||
|
image_id, tenant_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
def create(self, req, access):
|
def create(self, req, image_id, access_record):
|
||||||
return self.db_api.image_member_create(req.context, access)
|
#TODO(bcwaldon): Refactor these methods so we don't need to
|
||||||
|
# explicitly retrieve a session object here
|
||||||
|
session = self.db_api.get_session()
|
||||||
|
try:
|
||||||
|
image = self.db_api.image_get(req.context, image_id,
|
||||||
|
session=session)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
except exception.Forbidden:
|
||||||
|
# If it's private and doesn't belong to them, don't let on
|
||||||
|
# that it exists
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
|
# Image is visible, but authenticated user still may not be able to
|
||||||
|
# share it
|
||||||
|
if not req.context.is_image_sharable(image):
|
||||||
|
msg = _("No permission to share that image")
|
||||||
|
raise webob.exc.HTTPForbidden(msg)
|
||||||
|
|
||||||
|
access_record['image_id'] = image_id
|
||||||
|
return self.db_api.image_member_create(req.context, access_record)
|
||||||
|
|
||||||
|
def delete(self, req, image_id, tenant_id):
|
||||||
|
#TODO(bcwaldon): Refactor these methods so we don't need to explicitly
|
||||||
|
# retrieve a session object here
|
||||||
|
session = self.db_api.get_session()
|
||||||
|
member = self.db_api.image_member_find(req.context, image_id,
|
||||||
|
tenant_id, session=session)
|
||||||
|
self.db_api.image_member_delete(req.context, member, session=session)
|
||||||
|
|
||||||
|
|
||||||
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||||
@ -54,35 +90,37 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||||||
body = output.pop('body')
|
body = output.pop('body')
|
||||||
self._validate(request, body)
|
self._validate(request, body)
|
||||||
body['member'] = body.pop('tenant_id')
|
body['member'] = body.pop('tenant_id')
|
||||||
output['access'] = body
|
output['access_record'] = body
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
class ResponseSerializer(wsgi.JSONResponseSerializer):
|
class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||||
def _get_access_href(self, image_member):
|
def _get_access_href(self, image_id, tenant_id=None):
|
||||||
image_id = image_member['image_id']
|
link = '/v2/images/%s/access' % image_id
|
||||||
tenant_id = image_member['member']
|
if tenant_id:
|
||||||
return '/v2/images/%s/access/%s' % (image_id, tenant_id)
|
link = '%s/%s' % (link, tenant_id)
|
||||||
|
return link
|
||||||
|
|
||||||
def _get_access_links(self, access):
|
def _get_access_links(self, access):
|
||||||
|
self_link = self._get_access_href(access['image_id'], access['member'])
|
||||||
return [
|
return [
|
||||||
{'rel': 'self', 'href': self._get_access_href(access)},
|
{'rel': 'self', 'href': self_link},
|
||||||
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
|
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
|
||||||
]
|
]
|
||||||
|
|
||||||
def _format_access(self, access):
|
def _format_access(self, access):
|
||||||
return {
|
return {
|
||||||
'image_id': access['image_id'],
|
|
||||||
'tenant_id': access['member'],
|
'tenant_id': access['member'],
|
||||||
'can_share': access['can_share'],
|
'can_share': access['can_share'],
|
||||||
'links': self._get_access_links(access),
|
'links': self._get_access_links(access),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_container_links(self, image_id):
|
def _get_container_links(self, image_id):
|
||||||
return [{'rel': 'self', 'href': '/v2/images/%s/access' % image_id}]
|
return [{'rel': 'self', 'href': self._get_access_href(image_id)}]
|
||||||
|
|
||||||
def show(self, response, access):
|
def show(self, response, access):
|
||||||
response.body = json.dumps({'access': self._format_access(access)})
|
record = {'access_record': self._format_access(access)}
|
||||||
|
response.body = json.dumps(record)
|
||||||
|
|
||||||
def index(self, response, access_records):
|
def index(self, response, access_records):
|
||||||
body = {
|
body = {
|
||||||
@ -92,5 +130,19 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||||||
response.body = json.dumps(body)
|
response.body = json.dumps(body)
|
||||||
|
|
||||||
def create(self, response, access):
|
def create(self, response, access):
|
||||||
|
response.status_int = 201
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
response.location = self._get_access_href(access['image_id'],
|
||||||
|
access['member'])
|
||||||
response.body = json.dumps({'access': self._format_access(access)})
|
response.body = json.dumps({'access': self._format_access(access)})
|
||||||
response.location = self._get_access_href(access)
|
|
||||||
|
def delete(self, response, result):
|
||||||
|
response.status_int = 204
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource(conf):
|
||||||
|
"""Image access resource factory method"""
|
||||||
|
deserializer = RequestDeserializer(conf)
|
||||||
|
serializer = ResponseSerializer()
|
||||||
|
controller = Controller(conf)
|
||||||
|
return wsgi.Resource(controller, deserializer, serializer)
|
||||||
|
@ -19,6 +19,7 @@ import logging
|
|||||||
|
|
||||||
import routes
|
import routes
|
||||||
|
|
||||||
|
from glance.api.v2 import image_access
|
||||||
from glance.api.v2 import image_data
|
from glance.api.v2 import image_data
|
||||||
from glance.api.v2 import image_tags
|
from glance.api.v2 import image_tags
|
||||||
from glance.api.v2 import images
|
from glance.api.v2 import images
|
||||||
@ -100,4 +101,22 @@ class API(wsgi.Router):
|
|||||||
action='delete',
|
action='delete',
|
||||||
conditions={'method': ['DELETE']})
|
conditions={'method': ['DELETE']})
|
||||||
|
|
||||||
|
image_access_resource = image_access.create_resource(conf)
|
||||||
|
mapper.connect('/images/{image_id}/access',
|
||||||
|
controller=image_access_resource,
|
||||||
|
action='index',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/images/{image_id}/access',
|
||||||
|
controller=image_access_resource,
|
||||||
|
action='create',
|
||||||
|
conditions={'method': ['POST']})
|
||||||
|
mapper.connect('/images/{image_id}/access/{tenant_id}',
|
||||||
|
controller=image_access_resource,
|
||||||
|
action='show',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/images/{image_id}/access/{tenant_id}',
|
||||||
|
controller=image_access_resource,
|
||||||
|
action='delete',
|
||||||
|
conditions={'method': ['DELETE']})
|
||||||
|
|
||||||
super(API, self).__init__(mapper)
|
super(API, self).__init__(mapper)
|
||||||
|
@ -38,12 +38,6 @@ IMAGE_SCHEMA = {
|
|||||||
ACCESS_SCHEMA = {
|
ACCESS_SCHEMA = {
|
||||||
'name': 'access',
|
'name': 'access',
|
||||||
'properties': {
|
'properties': {
|
||||||
"image_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The image identifier",
|
|
||||||
"required": True,
|
|
||||||
"maxLength": 36,
|
|
||||||
},
|
|
||||||
"tenant_id": {
|
"tenant_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The tenant identifier",
|
"description": "The tenant identifier",
|
||||||
|
@ -60,21 +60,21 @@ class RequestContext(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# No owner == image visible
|
# No owner == image visible
|
||||||
if image.owner is None:
|
if image['owner'] is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Image is_public == image visible
|
# Image is_public == image visible
|
||||||
if image.is_public:
|
if image['is_public']:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Perform tests based on whether we have an owner
|
# Perform tests based on whether we have an owner
|
||||||
if self.owner is not None:
|
if self.owner is not None:
|
||||||
if self.owner == image.owner:
|
if self.owner == image['owner']:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Figure out if this image is shared with that tenant
|
# Figure out if this image is shared with that tenant
|
||||||
try:
|
try:
|
||||||
tmp = db_api.image_member_find(self, image.id, self.owner)
|
tmp = db_api.image_member_find(self, image['id'], self.owner)
|
||||||
return not tmp['deleted']
|
return not tmp['deleted']
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
pass
|
pass
|
||||||
@ -89,11 +89,11 @@ class RequestContext(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# No owner == image not mutable
|
# No owner == image not mutable
|
||||||
if image.owner is None or self.owner is None:
|
if image['owner'] is None or self.owner is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Image only mutable by its owner
|
# Image only mutable by its owner
|
||||||
return image.owner == self.owner
|
return image['owner'] == self.owner
|
||||||
|
|
||||||
def is_image_sharable(self, image, **kwargs):
|
def is_image_sharable(self, image, **kwargs):
|
||||||
"""Return True if the image can be shared to others in this context."""
|
"""Return True if the image can be shared to others in this context."""
|
||||||
@ -106,7 +106,7 @@ class RequestContext(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# If we own the image, we can share it
|
# If we own the image, we can share it
|
||||||
if self.owner == image.owner:
|
if self.owner == image['owner']:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Let's get the membership association
|
# Let's get the membership association
|
||||||
@ -117,14 +117,14 @@ class RequestContext(object):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
membership = db_api.image_member_find(self, image.id,
|
membership = db_api.image_member_find(self, image['id'],
|
||||||
self.owner)
|
self.owner)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
# Not shared with us anyway
|
# Not shared with us anyway
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# It's the can_share attribute we're now interested in
|
# It's the can_share attribute we're now interested in
|
||||||
return membership.can_share
|
return membership['can_share']
|
||||||
|
|
||||||
|
|
||||||
class ContextMiddleware(wsgi.Middleware):
|
class ContextMiddleware(wsgi.Middleware):
|
||||||
|
145
glance/tests/functional/v2/test_image_access.py
Normal file
145
glance/tests/functional/v2/test_image_access.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2012 OpenStack, LLC
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from glance.tests import functional
|
||||||
|
from glance.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
TENANT1 = utils.generate_uuid()
|
||||||
|
TENANT2 = utils.generate_uuid()
|
||||||
|
TENANT3 = utils.generate_uuid()
|
||||||
|
TENANT4 = utils.generate_uuid()
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageAccess(functional.FunctionalTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestImageAccess, self).setUp()
|
||||||
|
self.cleanup()
|
||||||
|
self.api_server.deployment_flavor = 'noauth'
|
||||||
|
self.start_servers(**self.__dict__.copy())
|
||||||
|
|
||||||
|
# Create an image for our tests
|
||||||
|
path = 'http://0.0.0.0:%d/v2/images' % self.api_port
|
||||||
|
headers = self._headers({'Content-Type': 'application/json'})
|
||||||
|
data = json.dumps({'name': 'image-1'})
|
||||||
|
response = requests.post(path, headers=headers, data=data)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.image_url = response.headers['Location']
|
||||||
|
|
||||||
|
def _url(self, path):
|
||||||
|
return '%s%s' % (self.image_url, path)
|
||||||
|
|
||||||
|
def _headers(self, custom_headers=None):
|
||||||
|
base_headers = {
|
||||||
|
'X-Identity-Status': 'Confirmed',
|
||||||
|
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
|
||||||
|
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
|
||||||
|
'X-Tenant-Id': TENANT1,
|
||||||
|
'X-Roles': 'member',
|
||||||
|
}
|
||||||
|
base_headers.update(custom_headers or {})
|
||||||
|
return base_headers
|
||||||
|
|
||||||
|
@functional.runs_sql
|
||||||
|
def test_image_access_lifecycle(self):
|
||||||
|
# Image acccess list should be empty
|
||||||
|
path = self._url('/access')
|
||||||
|
response = requests.get(path, headers=self._headers())
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
access_records = json.loads(response.text)['access_records']
|
||||||
|
self.assertEqual(0, len(access_records))
|
||||||
|
|
||||||
|
# Other tenants shouldn't be able to share by default, and shouldn't
|
||||||
|
# even know the image exists
|
||||||
|
path = self._url('/access')
|
||||||
|
data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
|
||||||
|
request_headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': TENANT2,
|
||||||
|
}
|
||||||
|
headers = self._headers(request_headers)
|
||||||
|
response = requests.post(path, headers=headers, data=data)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
||||||
|
# Share the image with another tenant
|
||||||
|
path = self._url('/access')
|
||||||
|
data = json.dumps({'tenant_id': TENANT2, 'can_share': True})
|
||||||
|
headers = self._headers({'Content-Type': 'application/json'})
|
||||||
|
response = requests.post(path, headers=headers, data=data)
|
||||||
|
self.assertEqual(201, response.status_code)
|
||||||
|
access_location = response.headers['Location']
|
||||||
|
|
||||||
|
# Ensure the access record was actually created
|
||||||
|
response = requests.get(access_location, headers=self._headers())
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
# Make sure the sharee can further share the image
|
||||||
|
path = self._url('/access')
|
||||||
|
data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
|
||||||
|
request_headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': TENANT2,
|
||||||
|
}
|
||||||
|
headers = self._headers(request_headers)
|
||||||
|
response = requests.post(path, headers=headers, data=data)
|
||||||
|
self.assertEqual(201, response.status_code)
|
||||||
|
access_location = response.headers['Location']
|
||||||
|
|
||||||
|
# Ensure the access record was actually created
|
||||||
|
response = requests.get(access_location, headers=self._headers())
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
# The third tenant should not be able to share it further
|
||||||
|
path = self._url('/access')
|
||||||
|
data = json.dumps({'tenant_id': TENANT4, 'can_share': False})
|
||||||
|
request_headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': TENANT3,
|
||||||
|
}
|
||||||
|
headers = self._headers(request_headers)
|
||||||
|
response = requests.post(path, headers=headers, data=data)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
|
||||||
|
# Image acccess list should now contain 2 entries
|
||||||
|
path = self._url('/access')
|
||||||
|
response = requests.get(path, headers=self._headers())
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
access_records = json.loads(response.text)['access_records']
|
||||||
|
self.assertEqual(2, len(access_records))
|
||||||
|
print [a['tenant_id'] for a in access_records]
|
||||||
|
|
||||||
|
# Delete an access record
|
||||||
|
response = requests.delete(access_location, headers=self._headers())
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
|
||||||
|
# Ensure the access record was actually deleted
|
||||||
|
response = requests.get(access_location, headers=self._headers())
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
||||||
|
# Image acccess list should now contain 1 entry
|
||||||
|
path = self._url('/access')
|
||||||
|
response = requests.get(path, headers=self._headers())
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
access_records = json.loads(response.text)['access_records']
|
||||||
|
print [a['tenant_id'] for a in access_records]
|
||||||
|
self.assertEqual(1, len(access_records))
|
||||||
|
|
||||||
|
self.stop_servers()
|
@ -20,26 +20,16 @@ import unittest
|
|||||||
from glance.common import context
|
from glance.common import context
|
||||||
|
|
||||||
|
|
||||||
class FakeImage(object):
|
def _fake_image(owner, is_public):
|
||||||
"""
|
return {
|
||||||
Fake image for providing the image attributes needed for
|
'id': None,
|
||||||
TestContext.
|
'owner': owner,
|
||||||
"""
|
'is_public': is_public,
|
||||||
|
}
|
||||||
def __init__(self, owner, is_public):
|
|
||||||
self.id = None
|
|
||||||
self.owner = owner
|
|
||||||
self.is_public = is_public
|
|
||||||
|
|
||||||
|
|
||||||
class FakeMembership(object):
|
def _fake_membership(can_share=False):
|
||||||
"""
|
return {'can_share': can_share}
|
||||||
Fake membership for providing the membership attributes needed for
|
|
||||||
TestContext.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, can_share=False):
|
|
||||||
self.can_share = can_share
|
|
||||||
|
|
||||||
|
|
||||||
class TestContext(unittest.TestCase):
|
class TestContext(unittest.TestCase):
|
||||||
@ -52,7 +42,7 @@ class TestContext(unittest.TestCase):
|
|||||||
context.
|
context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
img = FakeImage(img_owner, img_public)
|
img = _fake_image(img_owner, img_public)
|
||||||
ctx = context.RequestContext(**kwargs)
|
ctx = context.RequestContext(**kwargs)
|
||||||
|
|
||||||
self.assertEqual(ctx.is_image_visible(img), exp_res)
|
self.assertEqual(ctx.is_image_visible(img), exp_res)
|
||||||
@ -68,7 +58,7 @@ class TestContext(unittest.TestCase):
|
|||||||
is_image_sharable().
|
is_image_sharable().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
img = FakeImage(img_owner, True)
|
img = _fake_image(img_owner, True)
|
||||||
ctx = context.RequestContext(**kwargs)
|
ctx = context.RequestContext(**kwargs)
|
||||||
|
|
||||||
sharable_args = {}
|
sharable_args = {}
|
||||||
@ -111,7 +101,7 @@ class TestContext(unittest.TestCase):
|
|||||||
not share an image, with or without membership.
|
not share an image, with or without membership.
|
||||||
"""
|
"""
|
||||||
self.do_sharable(False, 'pattieblack', None, is_admin=True)
|
self.do_sharable(False, 'pattieblack', None, is_admin=True)
|
||||||
self.do_sharable(False, 'pattieblack', FakeMembership(True),
|
self.do_sharable(False, 'pattieblack', _fake_membership(True),
|
||||||
is_admin=True)
|
is_admin=True)
|
||||||
|
|
||||||
def test_anon_public(self):
|
def test_anon_public(self):
|
||||||
@ -148,7 +138,7 @@ class TestContext(unittest.TestCase):
|
|||||||
not share an image, with or without membership.
|
not share an image, with or without membership.
|
||||||
"""
|
"""
|
||||||
self.do_sharable(False, 'pattieblack', None)
|
self.do_sharable(False, 'pattieblack', None)
|
||||||
self.do_sharable(False, 'pattieblack', FakeMembership(True))
|
self.do_sharable(False, 'pattieblack', _fake_membership(True))
|
||||||
|
|
||||||
def test_auth_public(self):
|
def test_auth_public(self):
|
||||||
"""
|
"""
|
||||||
@ -227,7 +217,7 @@ class TestContext(unittest.TestCase):
|
|||||||
False) cannot share an image it does not own even if it is
|
False) cannot share an image it does not own even if it is
|
||||||
shared with it, but with can_share = False.
|
shared with it, but with can_share = False.
|
||||||
"""
|
"""
|
||||||
self.do_sharable(False, 'pattieblack', FakeMembership(False),
|
self.do_sharable(False, 'pattieblack', _fake_membership(False),
|
||||||
tenant='froggy')
|
tenant='froggy')
|
||||||
|
|
||||||
def test_auth_sharable_can_share(self):
|
def test_auth_sharable_can_share(self):
|
||||||
@ -236,5 +226,5 @@ class TestContext(unittest.TestCase):
|
|||||||
False) can share an image it does not own if it is shared with
|
False) can share an image it does not own if it is shared with
|
||||||
it with can_share = True.
|
it with can_share = True.
|
||||||
"""
|
"""
|
||||||
self.do_sharable(True, 'pattieblack', FakeMembership(True),
|
self.do_sharable(True, 'pattieblack', _fake_membership(True),
|
||||||
tenant='froggy')
|
tenant='froggy')
|
||||||
|
@ -78,11 +78,15 @@ class FakeDB(object):
|
|||||||
def configure_db(*args, **kwargs):
|
def configure_db(*args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_session(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def _image_member_format(self, image_id, tenant_id, can_share):
|
def _image_member_format(self, image_id, tenant_id, can_share):
|
||||||
return {
|
return {
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
'member': tenant_id,
|
'member': tenant_id,
|
||||||
'can_share': can_share,
|
'can_share': can_share,
|
||||||
|
'deleted': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _image_format(self, image_id, **values):
|
def _image_format(self, image_id, **values):
|
||||||
@ -96,7 +100,7 @@ class FakeDB(object):
|
|||||||
image.update(values)
|
image.update(values)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def image_get(self, context, image_id):
|
def image_get(self, context, image_id, session=None):
|
||||||
try:
|
try:
|
||||||
image = self.images[image_id]
|
image = self.images[image_id]
|
||||||
LOG.info('Found image %s: %s' % (image_id, str(image)))
|
LOG.info('Found image %s: %s' % (image_id, str(image)))
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
import glance.api.v2.image_access
|
from glance.api.v2 import image_access
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
import glance.tests.unit.utils as test_utils
|
import glance.tests.unit.utils as test_utils
|
||||||
@ -30,8 +29,7 @@ class TestImageAccessController(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestImageAccessController, self).setUp()
|
super(TestImageAccessController, self).setUp()
|
||||||
self.db = test_utils.FakeDB()
|
self.db = test_utils.FakeDB()
|
||||||
self.controller = \
|
self.controller = image_access.Controller({}, self.db)
|
||||||
glance.api.v2.image_access.ImageAccessController({}, self.db)
|
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
req = test_utils.FakeRequest()
|
req = test_utils.FakeRequest()
|
||||||
@ -41,11 +39,13 @@ class TestImageAccessController(unittest.TestCase):
|
|||||||
'image_id': test_utils.UUID1,
|
'image_id': test_utils.UUID1,
|
||||||
'member': test_utils.TENANT1,
|
'member': test_utils.TENANT1,
|
||||||
'can_share': True,
|
'can_share': True,
|
||||||
|
'deleted': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'image_id': test_utils.UUID1,
|
'image_id': test_utils.UUID1,
|
||||||
'member': test_utils.TENANT2,
|
'member': test_utils.TENANT2,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
|
'deleted': False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
self.assertEqual(expected, output)
|
self.assertEqual(expected, output)
|
||||||
@ -71,6 +71,7 @@ class TestImageAccessController(unittest.TestCase):
|
|||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
'member': tenant_id,
|
'member': tenant_id,
|
||||||
'can_share': True,
|
'can_share': True,
|
||||||
|
'deleted': False,
|
||||||
}
|
}
|
||||||
self.assertEqual(expected, output)
|
self.assertEqual(expected, output)
|
||||||
|
|
||||||
@ -78,72 +79,67 @@ class TestImageAccessController(unittest.TestCase):
|
|||||||
req = test_utils.FakeRequest()
|
req = test_utils.FakeRequest()
|
||||||
image_id = utils.generate_uuid()
|
image_id = utils.generate_uuid()
|
||||||
tenant_id = test_utils.TENANT1
|
tenant_id = test_utils.TENANT1
|
||||||
self.assertRaises(exception.NotFound,
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
self.controller.show, req, image_id, tenant_id)
|
self.controller.show, req, image_id, tenant_id)
|
||||||
|
|
||||||
def test_show_nonexistant_tenant(self):
|
def test_show_nonexistant_tenant(self):
|
||||||
req = test_utils.FakeRequest()
|
req = test_utils.FakeRequest()
|
||||||
image_id = test_utils.UUID1
|
image_id = test_utils.UUID1
|
||||||
tenant_id = utils.generate_uuid()
|
tenant_id = utils.generate_uuid()
|
||||||
self.assertRaises(exception.NotFound,
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
self.controller.show, req, image_id, tenant_id)
|
self.controller.show, req, image_id, tenant_id)
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
|
member = utils.generate_uuid()
|
||||||
fixture = {
|
fixture = {
|
||||||
'image_id': test_utils.UUID1,
|
'member': member,
|
||||||
'member': utils.generate_uuid(),
|
|
||||||
'can_share': True,
|
'can_share': True,
|
||||||
}
|
}
|
||||||
|
expected = {
|
||||||
|
'image_id': test_utils.UUID1,
|
||||||
|
'member': member,
|
||||||
|
'can_share': True,
|
||||||
|
'deleted': False,
|
||||||
|
}
|
||||||
req = test_utils.FakeRequest()
|
req = test_utils.FakeRequest()
|
||||||
output = self.controller.create(req, fixture)
|
output = self.controller.create(req, test_utils.UUID1, fixture)
|
||||||
self.assertEqual(fixture, output)
|
self.assertEqual(expected, output)
|
||||||
|
|
||||||
|
|
||||||
class TestImageAccessDeserializer(unittest.TestCase):
|
class TestImageAccessDeserializer(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.deserializer = glance.api.v2.image_access.RequestDeserializer({})
|
self.deserializer = image_access.RequestDeserializer({})
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
fixture = {
|
fixture = {
|
||||||
'image_id': test_utils.UUID1,
|
|
||||||
'tenant_id': test_utils.TENANT1,
|
'tenant_id': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
}
|
}
|
||||||
expected = {
|
expected = {
|
||||||
'image_id': test_utils.UUID1,
|
'access_record': {
|
||||||
'member': test_utils.TENANT1,
|
'member': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
request = test_utils.FakeRequest()
|
request = test_utils.FakeRequest()
|
||||||
request.body = json.dumps(fixture)
|
request.body = json.dumps(fixture)
|
||||||
output = self.deserializer.create(request)
|
output = self.deserializer.create(request)
|
||||||
self.assertEqual(output, {'access': expected})
|
self.assertEqual(expected, output)
|
||||||
|
|
||||||
def _test_create_fails(self, fixture):
|
|
||||||
request = test_utils.FakeRequest()
|
|
||||||
request.body = json.dumps(fixture)
|
|
||||||
self.assertRaises(jsonschema.ValidationError,
|
|
||||||
self.deserializer.create, request)
|
|
||||||
|
|
||||||
def test_create_no_image(self):
|
|
||||||
fixture = {'tenant_id': test_utils.TENANT1, 'can_share': True}
|
|
||||||
self._test_create_fails(fixture)
|
|
||||||
|
|
||||||
|
|
||||||
class TestImageAccessSerializer(unittest.TestCase):
|
class TestImageAccessSerializer(unittest.TestCase):
|
||||||
serializer = glance.api.v2.image_access.ResponseSerializer()
|
serializer = image_access.ResponseSerializer()
|
||||||
|
|
||||||
def test_show(self):
|
def test_show(self):
|
||||||
fixture = {
|
fixture = {
|
||||||
'member': test_utils.TENANT1,
|
|
||||||
'image_id': test_utils.UUID1,
|
'image_id': test_utils.UUID1,
|
||||||
|
'member': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
}
|
}
|
||||||
self_href = ('/v2/images/%s/access/%s' %
|
self_href = ('/v2/images/%s/access/%s' %
|
||||||
(test_utils.UUID1, test_utils.TENANT1))
|
(test_utils.UUID1, test_utils.TENANT1))
|
||||||
expected = {
|
expected = {
|
||||||
'access': {
|
'access_record': {
|
||||||
'image_id': test_utils.UUID1,
|
|
||||||
'tenant_id': test_utils.TENANT1,
|
'tenant_id': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
'links': [
|
'links': [
|
||||||
@ -159,20 +155,19 @@ class TestImageAccessSerializer(unittest.TestCase):
|
|||||||
def test_index(self):
|
def test_index(self):
|
||||||
fixtures = [
|
fixtures = [
|
||||||
{
|
{
|
||||||
'member': test_utils.TENANT1,
|
|
||||||
'image_id': test_utils.UUID1,
|
'image_id': test_utils.UUID1,
|
||||||
|
'member': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
'image_id': test_utils.UUID1,
|
||||||
'member': test_utils.TENANT2,
|
'member': test_utils.TENANT2,
|
||||||
'image_id': test_utils.UUID2,
|
|
||||||
'can_share': True,
|
'can_share': True,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expected = {
|
expected = {
|
||||||
'access_records': [
|
'access_records': [
|
||||||
{
|
{
|
||||||
'image_id': test_utils.UUID1,
|
|
||||||
'tenant_id': test_utils.TENANT1,
|
'tenant_id': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
'links': [
|
'links': [
|
||||||
@ -188,14 +183,13 @@ class TestImageAccessSerializer(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'image_id': test_utils.UUID2,
|
|
||||||
'tenant_id': test_utils.TENANT2,
|
'tenant_id': test_utils.TENANT2,
|
||||||
'can_share': True,
|
'can_share': True,
|
||||||
'links': [
|
'links': [
|
||||||
{
|
{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'href': ('/v2/images/%s/access/%s' %
|
'href': ('/v2/images/%s/access/%s' %
|
||||||
(test_utils.UUID2, test_utils.TENANT2))
|
(test_utils.UUID1, test_utils.TENANT2))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'rel': 'describedby',
|
'rel': 'describedby',
|
||||||
@ -212,15 +206,14 @@ class TestImageAccessSerializer(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
fixture = {
|
fixture = {
|
||||||
'member': test_utils.TENANT1,
|
|
||||||
'image_id': test_utils.UUID1,
|
'image_id': test_utils.UUID1,
|
||||||
|
'member': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
}
|
}
|
||||||
self_href = ('/v2/images/%s/access/%s' %
|
self_href = ('/v2/images/%s/access/%s' %
|
||||||
(test_utils.UUID1, test_utils.TENANT1))
|
(test_utils.UUID1, test_utils.TENANT1))
|
||||||
expected = {
|
expected = {
|
||||||
'access': {
|
'access': {
|
||||||
'image_id': test_utils.UUID1,
|
|
||||||
'tenant_id': test_utils.TENANT1,
|
'tenant_id': test_utils.TENANT1,
|
||||||
'can_share': False,
|
'can_share': False,
|
||||||
'links': [
|
'links': [
|
||||||
|
Loading…
Reference in New Issue
Block a user