Merge "Update v1/v2 images APIs to set store ACLs."
This commit is contained in:
commit
4c4f62751b
@ -20,6 +20,7 @@ import webob.exc
|
|||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
import glance.openstack.common.log as logging
|
import glance.openstack.common.log as logging
|
||||||
from glance import registry
|
from glance import registry
|
||||||
|
import glance.store as store
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -61,3 +62,24 @@ class BaseController(object):
|
|||||||
raise webob.exc.HTTPNotFound(
|
raise webob.exc.HTTPNotFound(
|
||||||
msg, request=request, content_type='text/plain')
|
msg, request=request, content_type='text/plain')
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
def update_store_acls(self, req, image_id, location_uri, public=False):
|
||||||
|
if location_uri:
|
||||||
|
try:
|
||||||
|
read_tenants = []
|
||||||
|
write_tenants = []
|
||||||
|
members = registry.get_image_members(req.context, image_id)
|
||||||
|
if members:
|
||||||
|
for member in members:
|
||||||
|
if member['can_share']:
|
||||||
|
write_tenants.append(member['member_id'])
|
||||||
|
else:
|
||||||
|
read_tenants.append(member['member_id'])
|
||||||
|
store.set_acls(req.context, location_uri, public=public,
|
||||||
|
read_tenants=read_tenants,
|
||||||
|
write_tenants=write_tenants)
|
||||||
|
except exception.UnknownScheme:
|
||||||
|
msg = _("Store for image_id not found: %s") % image_id
|
||||||
|
raise HTTPBadRequest(explanation=msg,
|
||||||
|
request=req,
|
||||||
|
content_type='text/plain')
|
||||||
|
@ -617,7 +617,8 @@ class Controller(controller.BaseController):
|
|||||||
image data.
|
image data.
|
||||||
"""
|
"""
|
||||||
self._enforce(req, 'add_image')
|
self._enforce(req, 'add_image')
|
||||||
if image_meta.get('is_public'):
|
is_public = image_meta.get('is_public')
|
||||||
|
if is_public:
|
||||||
self._enforce(req, 'publicize_image')
|
self._enforce(req, 'publicize_image')
|
||||||
|
|
||||||
image_meta = self._reserve(req, image_meta)
|
image_meta = self._reserve(req, image_meta)
|
||||||
@ -625,6 +626,10 @@ class Controller(controller.BaseController):
|
|||||||
|
|
||||||
image_meta = self._handle_source(req, id, image_meta, image_data)
|
image_meta = self._handle_source(req, id, image_meta, image_data)
|
||||||
|
|
||||||
|
location_uri = image_meta.get('location')
|
||||||
|
if location_uri:
|
||||||
|
self.update_store_acls(req, id, location_uri, public=is_public)
|
||||||
|
|
||||||
# Prevent client from learning the location, as it
|
# Prevent client from learning the location, as it
|
||||||
# could contain security credentials
|
# could contain security credentials
|
||||||
image_meta.pop('location', None)
|
image_meta.pop('location', None)
|
||||||
@ -642,7 +647,8 @@ class Controller(controller.BaseController):
|
|||||||
:retval Returns the updated image information as a mapping
|
:retval Returns the updated image information as a mapping
|
||||||
"""
|
"""
|
||||||
self._enforce(req, 'modify_image')
|
self._enforce(req, 'modify_image')
|
||||||
if image_meta.get('is_public'):
|
is_public = image_meta.get('is_public')
|
||||||
|
if is_public:
|
||||||
self._enforce(req, 'publicize_image')
|
self._enforce(req, 'publicize_image')
|
||||||
|
|
||||||
orig_image_meta = self.get_image_meta_or_404(req, id)
|
orig_image_meta = self.get_image_meta_or_404(req, id)
|
||||||
@ -671,6 +677,12 @@ class Controller(controller.BaseController):
|
|||||||
reactivating = orig_status != 'queued' and location
|
reactivating = orig_status != 'queued' and location
|
||||||
activating = orig_status == 'queued' and (location or image_data)
|
activating = orig_status == 'queued' and (location or image_data)
|
||||||
|
|
||||||
|
# Make image public in the backend store (if implemented)
|
||||||
|
orig_or_updated_loc = location or orig_image_meta.get('location', None)
|
||||||
|
if orig_or_updated_loc:
|
||||||
|
self.update_store_acls(req, id, orig_or_updated_loc,
|
||||||
|
public=is_public)
|
||||||
|
|
||||||
if reactivating:
|
if reactivating:
|
||||||
msg = _("Attempted to update Location field for an image "
|
msg = _("Attempted to update Location field for an image "
|
||||||
"not in queued status.")
|
"not in queued status.")
|
||||||
@ -691,6 +703,7 @@ class Controller(controller.BaseController):
|
|||||||
if activating:
|
if activating:
|
||||||
image_meta = self._handle_source(req, id, image_meta,
|
image_meta = self._handle_source(req, id, image_meta,
|
||||||
image_data)
|
image_data)
|
||||||
|
|
||||||
except exception.Invalid, e:
|
except exception.Invalid, e:
|
||||||
msg = (_("Failed to update image metadata. Got error: %(e)s")
|
msg = (_("Failed to update image metadata. Got error: %(e)s")
|
||||||
% locals())
|
% locals())
|
||||||
|
@ -18,16 +18,16 @@
|
|||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
|
from glance.api.v1 import controller
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
import glance.openstack.common.log as logging
|
import glance.openstack.common.log as logging
|
||||||
from glance import registry
|
from glance import registry
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
class Controller(controller.BaseController):
|
||||||
|
|
||||||
def _check_can_access_image_members(self, context):
|
def _check_can_access_image_members(self, context):
|
||||||
if context.owner is None and not context.is_admin:
|
if context.owner is None and not context.is_admin:
|
||||||
@ -68,6 +68,7 @@ class Controller(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
registry.delete_member(req.context, image_id, id)
|
registry.delete_member(req.context, image_id, id)
|
||||||
|
self._update_store_acls(req, image_id)
|
||||||
except exception.NotFound, e:
|
except exception.NotFound, e:
|
||||||
msg = "%s" % e
|
msg = "%s" % e
|
||||||
LOG.debug(msg)
|
LOG.debug(msg)
|
||||||
@ -105,6 +106,7 @@ class Controller(object):
|
|||||||
can_share = bool(body['member']['can_share'])
|
can_share = bool(body['member']['can_share'])
|
||||||
try:
|
try:
|
||||||
registry.add_member(req.context, image_id, id, can_share)
|
registry.add_member(req.context, image_id, id, can_share)
|
||||||
|
self._update_store_acls(req, image_id)
|
||||||
except exception.Invalid, e:
|
except exception.Invalid, e:
|
||||||
msg = "%s" % e
|
msg = "%s" % e
|
||||||
LOG.debug(msg)
|
LOG.debug(msg)
|
||||||
@ -135,6 +137,7 @@ class Controller(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
registry.replace_members(req.context, image_id, body)
|
registry.replace_members(req.context, image_id, body)
|
||||||
|
self._update_store_acls(req, image_id)
|
||||||
except exception.Invalid, e:
|
except exception.Invalid, e:
|
||||||
msg = "%s" % e
|
msg = "%s" % e
|
||||||
LOG.debug(msg)
|
LOG.debug(msg)
|
||||||
@ -175,6 +178,12 @@ class Controller(object):
|
|||||||
raise webob.exc.HTTPForbidden(msg)
|
raise webob.exc.HTTPForbidden(msg)
|
||||||
return dict(shared_images=members)
|
return dict(shared_images=members)
|
||||||
|
|
||||||
|
def _update_store_acls(self, req, image_id):
|
||||||
|
image_meta = self.get_image_meta_or_404(req, image_id)
|
||||||
|
location_uri = image_meta.get('location')
|
||||||
|
public = image_meta.get('is_public')
|
||||||
|
self.update_store_acls(req, image_id, location_uri, public)
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
"""Image members resource factory method"""
|
"""Image members resource factory method"""
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
# Copyright 2012 Red Hat Inc.
|
||||||
|
# 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 webob.exc
|
||||||
|
|
||||||
|
from glance.common import exception
|
||||||
|
from glance.store import set_acls
|
||||||
|
|
||||||
|
|
||||||
|
def update_image_read_acl(req, db_api, image):
|
||||||
|
"""Helper function to set ACL permissions on images in the image store"""
|
||||||
|
location_uri = image['location']
|
||||||
|
public = image['is_public']
|
||||||
|
image_id = image['id']
|
||||||
|
if location_uri:
|
||||||
|
try:
|
||||||
|
read_tenants = []
|
||||||
|
write_tenants = []
|
||||||
|
members = db_api.image_member_find(req.context,
|
||||||
|
image_id=image_id)
|
||||||
|
for member in members:
|
||||||
|
if not member['deleted']:
|
||||||
|
if member['can_share']:
|
||||||
|
write_tenants.append(member['member'])
|
||||||
|
else:
|
||||||
|
read_tenants.append(member['member'])
|
||||||
|
set_acls(req.context, location_uri, public=public,
|
||||||
|
read_tenants=read_tenants,
|
||||||
|
write_tenants=write_tenants)
|
||||||
|
except exception.UnknownScheme:
|
||||||
|
msg = _("Store for image_id not found: %s") % image_id
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg,
|
||||||
|
request=req,
|
||||||
|
content_type='text/plain')
|
@ -17,6 +17,7 @@ import json
|
|||||||
|
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
|
import glance.api.v2 as v2
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
@ -73,13 +74,27 @@ class Controller(object):
|
|||||||
raise webob.exc.HTTPForbidden(msg)
|
raise webob.exc.HTTPForbidden(msg)
|
||||||
|
|
||||||
access_record['image_id'] = image_id
|
access_record['image_id'] = image_id
|
||||||
return self.db_api.image_member_create(req.context, access_record)
|
member = self.db_api.image_member_create(req.context, access_record)
|
||||||
|
|
||||||
|
v2.update_image_read_acl(req, self.db_api, image)
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
@utils.mutating
|
@utils.mutating
|
||||||
def delete(self, req, image_id, tenant_id):
|
def delete(self, req, image_id, tenant_id):
|
||||||
#TODO(bcwaldon): Refactor these methods so we don't need to explicitly
|
#TODO(bcwaldon): Refactor these methods so we don't need to explicitly
|
||||||
# retrieve a session object here
|
# retrieve a session object here
|
||||||
session = self.db_api.get_session()
|
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()
|
||||||
|
|
||||||
members = self.db_api.image_member_find(req.context,
|
members = self.db_api.image_member_find(req.context,
|
||||||
image_id=image_id,
|
image_id=image_id,
|
||||||
member=tenant_id,
|
member=tenant_id,
|
||||||
@ -90,6 +105,7 @@ class Controller(object):
|
|||||||
raise webob.exc.HTTPNotFound()
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
self.db_api.image_member_delete(req.context, member, session=session)
|
self.db_api.image_member_delete(req.context, member, session=session)
|
||||||
|
v2.update_image_read_acl(req, self.db_api, image)
|
||||||
|
|
||||||
|
|
||||||
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
from glance.api import common
|
from glance.api import common
|
||||||
|
import glance.api.v2 as v2
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
@ -39,13 +40,15 @@ class ImageDataController(object):
|
|||||||
|
|
||||||
@utils.mutating
|
@utils.mutating
|
||||||
def upload(self, req, image_id, data, size):
|
def upload(self, req, image_id, data, size):
|
||||||
self._get_image(req.context, image_id)
|
image = self._get_image(req.context, image_id)
|
||||||
try:
|
try:
|
||||||
location, size, checksum = self.store_api.add_to_backend(
|
location, size, checksum = self.store_api.add_to_backend(
|
||||||
req.context, 'file', image_id, data, size)
|
req.context, 'file', image_id, data, size)
|
||||||
except exception.Duplicate:
|
except exception.Duplicate:
|
||||||
raise webob.exc.HTTPConflict()
|
raise webob.exc.HTTPConflict()
|
||||||
|
|
||||||
|
v2.update_image_read_acl(req, self.db_api, image)
|
||||||
|
|
||||||
values = {'location': location, 'size': size, 'checksum': checksum}
|
values = {'location': location, 'size': size, 'checksum': checksum}
|
||||||
self.db_api.image_update(req.context, image_id, values)
|
self.db_api.image_update(req.context, image_id, values)
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import urllib
|
|||||||
|
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
|
import glance.api.v2 as v2
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
@ -78,6 +79,8 @@ class ImagesController(object):
|
|||||||
else:
|
else:
|
||||||
image['tags'] = []
|
image['tags'] = []
|
||||||
|
|
||||||
|
v2.update_image_read_acl(req, self.db_api, image)
|
||||||
|
|
||||||
return self._normalize_properties(dict(image))
|
return self._normalize_properties(dict(image))
|
||||||
|
|
||||||
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
||||||
@ -128,6 +131,8 @@ class ImagesController(object):
|
|||||||
|
|
||||||
image = self._normalize_properties(dict(image))
|
image = self._normalize_properties(dict(image))
|
||||||
|
|
||||||
|
v2.update_image_read_acl(req, self.db_api, image)
|
||||||
|
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
self.db_api.image_tag_set_all(req.context, image_id, tags)
|
self.db_api.image_tag_set_all(req.context, image_id, tags)
|
||||||
image['tags'] = tags
|
image['tags'] = tags
|
||||||
|
@ -296,3 +296,16 @@ def schedule_delete_from_backend(uri, context, image_id, **kwargs):
|
|||||||
def add_to_backend(context, scheme, image_id, data, size):
|
def add_to_backend(context, scheme, image_id, data, size):
|
||||||
store = get_store_from_scheme(context, scheme)
|
store = get_store_from_scheme(context, scheme)
|
||||||
return store.add(image_id, data, size)
|
return store.add(image_id, data, size)
|
||||||
|
|
||||||
|
|
||||||
|
def set_acls(context, location_uri, public=False, read_tenants=[],
|
||||||
|
write_tenants=[]):
|
||||||
|
scheme = get_store_from_location(location_uri)
|
||||||
|
store = get_store_from_scheme(context, scheme)
|
||||||
|
try:
|
||||||
|
store.set_acls(location.get_location_from_uri(location_uri),
|
||||||
|
public=public,
|
||||||
|
read_tenants=read_tenants,
|
||||||
|
write_tenants=write_tenants)
|
||||||
|
except NotImplementedError:
|
||||||
|
LOG.debug(_("Skipping store.set_acls... not implemented."))
|
||||||
|
@ -57,7 +57,8 @@ class FakeDB(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def init_db():
|
def init_db():
|
||||||
images = [
|
images = [
|
||||||
{'id': UUID1, 'owner': TENANT1, 'location': UUID1},
|
{'id': UUID1, 'owner': TENANT1,
|
||||||
|
'location': 'swift+http://storeurl.com/container/%s' % UUID1},
|
||||||
{'id': UUID2, 'owner': TENANT1},
|
{'id': UUID2, 'owner': TENANT1},
|
||||||
]
|
]
|
||||||
[simple_db.image_create(None, image) for image in images]
|
[simple_db.image_create(None, image) for image in images]
|
||||||
@ -85,7 +86,7 @@ class FakeDB(object):
|
|||||||
class FakeStoreAPI(object):
|
class FakeStoreAPI(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.data = {
|
self.data = {
|
||||||
UUID1: ('XXX', 3),
|
'swift+http://storeurl.com/container/%s' % UUID1: ('XXX', 3),
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_stores(self):
|
def create_stores(self):
|
||||||
@ -93,8 +94,6 @@ class FakeStoreAPI(object):
|
|||||||
|
|
||||||
def get_from_backend(self, context, location):
|
def get_from_backend(self, context, location):
|
||||||
try:
|
try:
|
||||||
#NOTE(bcwaldon): This fake API is store-agnostic, so we only
|
|
||||||
# care about location being some unique string
|
|
||||||
return self.data[location]
|
return self.data[location]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise exception.NotFound()
|
raise exception.NotFound()
|
||||||
@ -103,7 +102,8 @@ class FakeStoreAPI(object):
|
|||||||
return self.get_from_backend(context, location)[1]
|
return self.get_from_backend(context, location)[1]
|
||||||
|
|
||||||
def add_to_backend(self, context, scheme, image_id, data, size):
|
def add_to_backend(self, context, scheme, image_id, data, size):
|
||||||
if image_id in self.data:
|
for location in self.data.keys():
|
||||||
|
if image_id in location:
|
||||||
raise exception.Duplicate()
|
raise exception.Duplicate()
|
||||||
self.data[image_id] = (data, size or len(data))
|
self.data[image_id] = (data, size or len(data))
|
||||||
checksum = 'Z'
|
checksum = 'Z'
|
||||||
|
@ -21,6 +21,7 @@ import glance.api.v2.image_access
|
|||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
import glance.schema
|
import glance.schema
|
||||||
|
import glance.store
|
||||||
import glance.tests.unit.utils as unit_test_utils
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
import glance.tests.utils as test_utils
|
import glance.tests.utils as test_utils
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ class TestImageAccessController(test_utils.BaseTestCase):
|
|||||||
super(TestImageAccessController, self).setUp()
|
super(TestImageAccessController, self).setUp()
|
||||||
self.db = unit_test_utils.FakeDB()
|
self.db = unit_test_utils.FakeDB()
|
||||||
self.controller = glance.api.v2.image_access.Controller(self.db)
|
self.controller = glance.api.v2.image_access.Controller(self.db)
|
||||||
|
glance.store.create_stores()
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
req = unit_test_utils.get_fake_request()
|
req = unit_test_utils.get_fake_request()
|
||||||
|
@ -25,6 +25,7 @@ from glance.openstack.common import cfg
|
|||||||
import glance.schema
|
import glance.schema
|
||||||
import glance.tests.unit.utils as unit_test_utils
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
import glance.tests.utils as test_utils
|
import glance.tests.utils as test_utils
|
||||||
|
import glance.store
|
||||||
|
|
||||||
|
|
||||||
DATETIME = datetime.datetime(2012, 5, 16, 15, 27, 36, 325355)
|
DATETIME = datetime.datetime(2012, 5, 16, 15, 27, 36, 325355)
|
||||||
@ -52,6 +53,7 @@ class TestImagesController(test_utils.BaseTestCase):
|
|||||||
self.db = unit_test_utils.FakeDB()
|
self.db = unit_test_utils.FakeDB()
|
||||||
self._create_images()
|
self._create_images()
|
||||||
self.controller = glance.api.v2.images.ImagesController(self.db)
|
self.controller = glance.api.v2.images.ImagesController(self.db)
|
||||||
|
glance.store.create_stores()
|
||||||
|
|
||||||
def _create_images(self):
|
def _create_images(self):
|
||||||
self.db.reset()
|
self.db.reset()
|
||||||
@ -59,7 +61,7 @@ class TestImagesController(test_utils.BaseTestCase):
|
|||||||
{
|
{
|
||||||
'id': UUID1,
|
'id': UUID1,
|
||||||
'owner': TENANT1,
|
'owner': TENANT1,
|
||||||
'location': UUID1,
|
'location': 'swift+http://storeurl.com/container/%s' % UUID1,
|
||||||
'name': '1',
|
'name': '1',
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'size': 256,
|
'size': 256,
|
||||||
@ -362,7 +364,7 @@ class TestImagesController(test_utils.BaseTestCase):
|
|||||||
'name': 'image-2',
|
'name': 'image-2',
|
||||||
'owner': TENANT1,
|
'owner': TENANT1,
|
||||||
'size': 256,
|
'size': 256,
|
||||||
'location': UUID1,
|
'location': 'swift+http://storeurl.com/container/%s' % UUID1,
|
||||||
'status': 'queued',
|
'status': 'queued',
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'tags': ['ping', 'pong'],
|
'tags': ['ping', 'pong'],
|
||||||
|
Loading…
Reference in New Issue
Block a user