Check policies for image import operation in API

This patch enforces policy checks required for importing/copying image
data to store in API layer.

Partially-Implements: blueprint policy-refactor

Change-Id: I18a5187d80bf76c0dc6f22dd8c96a8ffa0f46dc1
This commit is contained in:
Abhishek Kekane 2021-08-15 20:01:54 +00:00
parent 3e2474676d
commit 6c87a18d4c
7 changed files with 205 additions and 54 deletions

View File

@ -32,7 +32,6 @@ from six.moves import http_client as http
import six.moves.urllib.parse as urlparse
import webob.exc
from glance.api import authorization
from glance.api import common
from glance.api import policy
from glance.api.v2 import policy as api_policy
@ -168,8 +167,10 @@ class ImagesController(object):
def _enforce_import_lock(self, req, image):
admin_context = req.context.elevated()
admin_image_repo = self.gateway.get_repo(admin_context)
admin_task_repo = self.gateway.get_task_repo(admin_context)
admin_image_repo = self.gateway.get_repo(
admin_context, authorization_layer=False)
admin_task_repo = self.gateway.get_task_repo(
admin_context, authorization_layer=False)
other_task = image.extra_properties['os_glance_import_task']
expiry = datetime.timedelta(minutes=60)
@ -313,9 +314,12 @@ class ImagesController(object):
@utils.mutating
def import_image(self, req, image_id, body):
ctxt = req.context
image_repo = self.gateway.get_repo(ctxt)
task_factory = self.gateway.get_task_factory(ctxt)
task_repo = self.gateway.get_task_repo(ctxt)
image_repo = self.gateway.get_repo(ctxt,
authorization_layer=False)
task_factory = self.gateway.get_task_factory(
ctxt, authorization_layer=False)
task_repo = self.gateway.get_task_repo(
ctxt, authorization_layer=False)
import_method = body.get('method').get('name')
uri = body.get('method').get('uri')
all_stores_must_succeed = body.get('all_stores_must_succeed', True)
@ -355,12 +359,15 @@ class ImagesController(object):
# NOTE(danms): For copy-image only, we check policy to decide
# if the user should be able to do this. Otherwise, we forbid
# the import if the user is not the owner.
api_pol = api_policy.ImageAPIPolicy(req.context, image,
enforcer=self.policy)
if import_method == 'copy-image':
self.policy.enforce(ctxt, 'copy_image',
dict(policy.ImageTarget(image)))
elif not authorization.is_image_mutable(ctxt, image):
raise webob.exc.HTTPForbidden(
explanation=_("Operation not permitted"))
api_pol.copy_image()
else:
# NOTE(abhishekk): We need to perform ownership check on image
# so that non-admin or non-owner can not import data to image
api_pol.modify_image()
if 'os_glance_import_task' in image.extra_properties:
# NOTE(danms): This will raise exception.Conflict if the
@ -433,7 +440,7 @@ class ImagesController(object):
admin_context = None
executor_factory = self.gateway.get_task_executor_factory(
ctxt, admin_context=admin_context)
ctxt, admin_context=admin_context, authorization_layer=False)
if (import_method == 'web-download' and
not utils.validate_import_uri(uri)):

View File

@ -265,6 +265,9 @@ class ImageAPIPolicy(APIPolicyBase):
if not CONF.enforce_secure_rbac:
check_is_image_mutable(self._context, self._image)
def copy_image(self):
self._enforce('copy_image')
class MetadefAPIPolicy(APIPolicyBase):
def __init__(self, context, md_resource=None, target=None, enforcer=None):

View File

@ -1710,7 +1710,7 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return self.api_request('PATCH', url, headers=headers,
json=list(patches))
def _import_copy(self, image_id, stores):
def _import_copy(self, image_id, stores, headers=None):
"""Do an import of image_id to the given stores."""
body = {'method': {'name': 'copy-image'},
'stores': stores,
@ -1718,9 +1718,10 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return self.api_post(
'/v2/images/%s/import' % image_id,
headers=headers,
json=body)
def _import_direct(self, image_id, stores):
def _import_direct(self, image_id, stores, headers=None):
"""Do an import of image_id to the given stores."""
body = {'method': {'name': 'glance-direct'},
'stores': stores,
@ -1728,9 +1729,10 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return self.api_post(
'/v2/images/%s/import' % image_id,
headers=headers,
json=body)
def _import_web_download(self, image_id, stores, url):
def _import_web_download(self, image_id, stores, url, headers=None):
"""Do an import of image_id to the given stores."""
body = {'method': {'name': 'web-download',
'uri': url},
@ -1739,6 +1741,7 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return self.api_post(
'/v2/images/%s/import' % image_id,
headers=headers,
json=body)
def _create_and_upload(self, data_iter=None, expected_code=204,
@ -1770,11 +1773,18 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return image['id']
def _create_and_stage(self, data_iter=None, expected_code=204):
def _create_and_stage(self, data_iter=None, expected_code=204,
visibility=None):
data = {
'name': 'foo',
'container_format': 'bare',
'disk_format': 'raw',
}
if visibility:
data['visibility'] = visibility
resp = self.api_post('/v2/images',
json={'name': 'foo',
'container_format': 'bare',
'disk_format': 'raw'})
json=data)
image = jsonutils.loads(resp.text)
if data_iter:
@ -1805,12 +1815,14 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
return image
def _create_and_import(self, stores=[], data_iter=None, expected_code=202):
def _create_and_import(self, stores=[], data_iter=None, expected_code=202,
visibility=None):
"""Create an image, stage data, and import into the given stores.
:returns: image_id
"""
image_id = self._create_and_stage(data_iter=data_iter)
image_id = self._create_and_stage(data_iter=data_iter,
visibility=visibility)
resp = self._import_direct(image_id, stores)
self.assertEqual(expected_code, resp.status_code)

View File

@ -724,3 +724,124 @@ class TestImagesPolicy(functional.SynchronousAPIBase):
path = "/v2/stores/store2/%s" % image_id
response = self.api_delete(path, headers=headers)
self.assertEqual(403, response.status_code)
def test_copy_image(self):
self.start_server()
# create image using import
image_id = self._create_and_import(
stores=['store1'], visibility='public')
# Make sure you can copy image to another store
self.set_policy_rules({
'copy_image': 'role:admin',
'get_image': '',
'modify_image': ''
})
store_to_copy = ["store2"]
response = self._import_copy(image_id, store_to_copy)
self.assertEqual(202, response.status_code)
self._wait_for_import(image_id)
self.assertEqual('success', self._get_latest_task(image_id)['status'])
# Now disable copy image and see you will get 403 Forbidden
store_to_copy = ["store3"]
self.set_policy_rules({
'copy_image': '!',
'get_image': '',
'modify_image': ''
})
response = self._import_copy(image_id, store_to_copy)
self.assertEqual(403, response.status_code)
# Verify that non-admin but member of same project can not copy image
self.set_policy_rules({
'copy_image': 'role:admin',
'get_image': '',
'modify_image': ''
})
headers = self._headers({'X-Roles': 'member'})
response = self._import_copy(image_id, store_to_copy,
headers=headers)
self.assertEqual(403, response.status_code)
# Verify that non-owner can not copy image
self.set_policy_rules({
'copy_image': 'role:admin',
'get_image': '',
'modify_image': ''
})
headers = self._headers({
'X-Roles': 'member',
'X-Project-Id': 'fake-project-id'
})
response = self._import_copy(image_id, store_to_copy,
headers=headers)
self.assertEqual(403, response.status_code)
# Now disable copy image and get_image and see you will get
# 404 NotFound
self.set_policy_rules({
'copy_image': '!',
'get_image': '!',
'modify_image': ''
})
store_to_copy = ["store3"]
print(self.policy.rules.items())
response = self._import_copy(image_id, store_to_copy)
self.assertEqual(404, response.status_code)
def test_import_glance_direct(self):
self.start_server()
# create image and stage data to it
image_id = self._create_and_stage(visibility='public')
# Make sure you can import using glance-direct
self.set_policy_rules({
'get_image': '',
'communitize_image': '',
'add_image': '',
'modify_image': ''
})
store_to_import = ['store1']
response = self._import_direct(image_id, store_to_import)
self.assertEqual(202, response.status_code)
self._wait_for_import(image_id)
self.assertEqual('success', self._get_latest_task(image_id)['status'])
# Make sure you can import data to image using non-admin role
image_id = self._create_and_stage(visibility='community')
headers = self._headers({'X-Roles': 'member'})
response = self._import_direct(image_id, store_to_import,
headers=headers)
self.assertEqual(202, response.status_code)
self._wait_for_import(image_id)
self.assertEqual('success', self._get_latest_task(image_id)['status'])
# Make sure you can not import data to image using non-admin role of
# different project
image_id = self._create_and_stage(visibility='community')
# Make sure you will get 403 Forbidden
self.set_policy_rules({
'get_image': '',
'modify_image': '!'
})
headers = self._headers({
'X-Roles': 'member',
'X-Project-Id': 'fake-project-id'
})
response = self._import_direct(image_id, store_to_import,
headers=headers)
self.assertEqual(403, response.status_code)
# disabling both get_image and modify_image should return 404 NotFound
self.set_policy_rules({
'get_image': '!',
'modify_image': '!'
})
headers = self._headers({
'X-Roles': 'member',
'X-Project-Id': 'fake-project-id'
})
response = self._import_direct(image_id, store_to_import,
headers=headers)
self.assertEqual(404, response.status_code)

View File

@ -47,7 +47,8 @@ class TestImageImportLocking(functional.SynchronousAPIBase):
# Set up a fake data pipeline that will stall until we are ready
# to unblock it
def slow_fake_set_data(data_iter, backend=None, set_active=True):
def slow_fake_set_data(data_iter, size=None, backend=None,
set_active=True):
me = str(uuid.uuid4())
while state['want_run'] == True:
LOG.info('fake_set_data running %s' % me)
@ -61,7 +62,7 @@ class TestImageImportLocking(functional.SynchronousAPIBase):
# Turn on the delayed data pipeline and start a copy-image
# import which will hang out for a while
with mock.patch('glance.domain.proxy.Image.set_data') as mock_sd:
with mock.patch('glance.location.ImageProxy.set_data') as mock_sd:
mock_sd.side_effect = slow_fake_set_data
resp = self._import_copy(image_id, ['store2'])

View File

@ -841,7 +841,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(container_format=None)
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -851,7 +851,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(disk_format=None)
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -861,7 +861,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='queued')
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -871,7 +871,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage()
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -881,7 +881,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage()
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -892,7 +892,7 @@ class TestImagesController(base.IsolatedUnitTest):
def test_image_import_raises_bad_request(self, mock_gpt, mock_spa):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='uploading')
# NOTE(abhishekk): Due to
# https://bugs.launchpad.net/glance/+bug/1712463 taskflow is not
@ -907,7 +907,7 @@ class TestImagesController(base.IsolatedUnitTest):
def test_image_import_invalid_uri_filtering(self):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='queued')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.import_image, request, UUID4,
@ -922,7 +922,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request(
'/v2/images/%s/import' % UUID4)
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='uploading')
mock_get.return_value.extra_properties['os_glance_stage_host'] = (
'https://glance-worker1.openstack.org')
@ -986,7 +986,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request(
'/v2/images/%s/import' % UUID4)
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='uploading')
mock_get.return_value.extra_properties['os_glance_stage_host'] = (
'https://glance-worker1.openstack.org')
@ -1078,7 +1078,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request(
'/v2/images/%s/import' % UUID4)
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='queued')
mock_get.return_value.extra_properties['os_glance_stage_host'] = (
'https://glance-worker1.openstack.org')
@ -3339,7 +3339,7 @@ class TestImagesController(base.IsolatedUnitTest):
self.assertIsNone(pos)
@mock.patch('glance.db.simple.api.image_set_property_atomic')
@mock.patch.object(glance.api.authorization.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.domain.TaskExecutorFactory, 'new_task_executor')
@mock.patch('glance.api.common.get_thread_pool')
@mock.patch('glance.quota.keystone.enforce_image_size_total')
@ -3348,7 +3348,7 @@ class TestImagesController(base.IsolatedUnitTest):
request = unit_test_utils.get_fake_request()
image = FakeImage(status='uploading')
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = image
output = self.controller.import_image(
request, UUID4, {'method': {'name': 'glance-direct'}})
@ -3370,7 +3370,7 @@ class TestImagesController(base.IsolatedUnitTest):
mock_nt.return_value.run, mock_nte.return_value)
@mock.patch.object(glance.domain.TaskFactory, 'new_task')
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
def test_image_import_not_allowed(self, mock_get, mock_new_task):
# NOTE(danms): FakeImage is owned by utils.TENANT1. Try to do the
# import as TENANT2 and we should get an HTTPForbidden
@ -3384,7 +3384,7 @@ class TestImagesController(base.IsolatedUnitTest):
# a task
mock_new_task.assert_not_called()
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
@mock.patch('glance.quota.keystone.enforce_image_size_total')
def test_image_import_quota_fail(self, mock_enforce, mock_get):
request = unit_test_utils.get_fake_request()
@ -3398,7 +3398,7 @@ class TestImagesController(base.IsolatedUnitTest):
@mock.patch('glance.db.simple.api.image_set_property_atomic')
@mock.patch('glance.context.RequestContext.elevated')
@mock.patch.object(glance.domain.TaskFactory, 'new_task')
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
def test_image_import_copy_allowed_by_policy(self, mock_get,
mock_new_task,
mock_elevated,
@ -3423,7 +3423,8 @@ class TestImagesController(base.IsolatedUnitTest):
# Make sure we passed an admin context to our task executor factory
mock_tef.assert_called_once_with(
request.context,
admin_context=mock_elevated.return_value)
admin_context=mock_elevated.return_value,
authorization_layer=False)
expected_input = {'image_id': UUID4,
'import_req': mock.ANY,
@ -3442,7 +3443,7 @@ class TestImagesController(base.IsolatedUnitTest):
self.test_image_import_copy_allowed_by_policy,
allowed=False)
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
def test_image_import_locked(self, mock_get):
task = test_tasks_resource._db_fixture(test_tasks_resource.UUID1,
status='pending')
@ -3463,8 +3464,8 @@ class TestImagesController(base.IsolatedUnitTest):
@mock.patch('glance.db.simple.api.image_set_property_atomic')
@mock.patch('glance.db.simple.api.image_delete_property_atomic')
@mock.patch.object(glance.api.authorization.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
def test_image_import_locked_by_reaped_task(self, mock_get, mock_nt,
mock_dpi, mock_spi):
image = FakeImage(status='uploading')
@ -3485,11 +3486,11 @@ class TestImagesController(base.IsolatedUnitTest):
mock_spi.assert_called_once_with(image.id, 'os_glance_import_task',
'mytask')
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'save')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'save')
@mock.patch('glance.db.simple.api.image_set_property_atomic')
@mock.patch('glance.db.simple.api.image_delete_property_atomic')
@mock.patch.object(glance.api.authorization.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.api.authorization.ImageRepoProxy, 'get')
@mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
@mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
def test_image_import_locked_by_bustable_task(self, mock_get, mock_nt,
mock_dpi, mock_spi,
mock_save,
@ -6057,7 +6058,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
request.headers['x-image-meta-store'] = 'dummy'
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='uploading')
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image,
@ -6068,7 +6069,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(disk_format=None)
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -6078,7 +6079,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='queued')
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -6088,7 +6089,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage()
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID4,
@ -6099,7 +6100,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request.headers['x-image-meta-store'] = 'fast'
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage()
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.import_image, request, UUID7,
@ -6110,7 +6111,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.side_effect = exception.NotFound
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.import_image, request, UUID1,
@ -6125,7 +6126,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
'status': 'active'},
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
with mock.patch.object(self.store,
'get_store_from_store_identifier'):
mock_get.return_value = FakeImage(id=UUID7, status='active',
@ -6138,7 +6139,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
request = unit_test_utils.get_fake_request()
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(status='uploading')
self.assertRaises(webob.exc.HTTPConflict,
self.controller.import_image, request, UUID1,
@ -6159,7 +6160,7 @@ class TestMultiImagesController(base.MultiIsolatedUnitTest):
'metadata': {'store': 'fast'},
'status': 'active'},
with mock.patch.object(
glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
glance.notifier.ImageRepoProxy, 'get') as mock_get:
mock_get.return_value = FakeImage(id=UUID7, status='active',
locations=locations)

View File

@ -471,6 +471,12 @@ class APIImagePolicy(APIPolicyBase):
self.policy.reactivate_image()
self.assertFalse(m.called)
def test_copy_image(self):
self.policy.copy_image()
self.enforcer.enforce.assert_called_once_with(self.context,
'copy_image',
mock.ANY)
class TestMetadefAPIPolicy(APIPolicyBase):
def setUp(self):