Merge "Update v1/v2 images APIs to set store ACLs."

This commit is contained in:
Jenkins 2012-08-02 18:37:58 +00:00 committed by Gerrit Code Review
commit 4c4f62751b
11 changed files with 145 additions and 14 deletions

View File

@ -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')

View File

@ -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())

View File

@ -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"""

View File

@ -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')

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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."))

View File

@ -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'

View File

@ -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()

View File

@ -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'],